Kaynağa Gözat

[RN] Add connection indicator

master
Bettenbuk Zoltan 6 yıl önce
ebeveyn
işleme
2b4ace75ae
34 değiştirilmiş dosya ile 368 ekleme ve 198 silme
  1. 9
    0
      css/_font.scss
  2. BIN
      fonts/jitsi.eot
  3. 3
    0
      fonts/jitsi.svg
  4. BIN
      fonts/jitsi.ttf
  5. BIN
      fonts/jitsi.woff
  6. 1
    1
      fonts/selection.json
  7. 2
    2
      modules/UI/videolayout/SmallVideo.js
  8. 14
    5
      react/features/base/react/components/native/BaseIndicator.js
  9. 1
    0
      react/features/base/react/components/native/index.js
  10. 28
    0
      react/features/base/react/components/native/indicatorstyles.js
  11. 1
    1
      react/features/base/react/components/web/BaseIndicator.js
  12. 3
    0
      react/features/base/react/components/web/index.js
  13. 173
    0
      react/features/connection-indicator/components/AbstractConnectionIndicator.js
  14. 3
    0
      react/features/connection-indicator/components/index.native.js
  15. 3
    0
      react/features/connection-indicator/components/index.web.js
  16. 62
    0
      react/features/connection-indicator/components/native/ConnectionIndicator.js
  17. 2
    0
      react/features/connection-indicator/components/native/index.js
  18. 9
    0
      react/features/connection-indicator/components/native/styles.js
  19. 17
    151
      react/features/connection-indicator/components/web/ConnectionIndicator.js
  20. 3
    0
      react/features/connection-indicator/components/web/index.js
  21. 2
    0
      react/features/connection-indicator/index.js
  22. 0
    2
      react/features/connection-indicator/statsEmitter.js
  23. 1
    1
      react/features/filmstrip/components/native/AudioMutedIndicator.js
  24. 1
    1
      react/features/filmstrip/components/native/DominantSpeakerIndicator.js
  25. 1
    1
      react/features/filmstrip/components/native/ModeratorIndicator.js
  26. 1
    2
      react/features/filmstrip/components/native/RaisedHandIndicator.js
  27. 14
    1
      react/features/filmstrip/components/native/Thumbnail.js
  28. 1
    1
      react/features/filmstrip/components/native/VideoMutedIndicator.js
  29. 8
    23
      react/features/filmstrip/components/native/styles.js
  30. 1
    1
      react/features/filmstrip/components/web/AudioMutedIndicator.js
  31. 1
    1
      react/features/filmstrip/components/web/DominantSpeakerIndicator.js
  32. 1
    1
      react/features/filmstrip/components/web/ModeratorIndicator.js
  33. 1
    2
      react/features/filmstrip/components/web/RaisedHandIndicator.js
  34. 1
    1
      react/features/filmstrip/components/web/VideoMutedIndicator.js

+ 9
- 0
css/_font.scss Dosyayı Görüntüle

25
     -moz-osx-font-smoothing: grayscale;
25
     -moz-osx-font-smoothing: grayscale;
26
 }
26
 }
27
 
27
 
28
+.icon-signal_cellular_0:before {
29
+  content: "\e901";
30
+}
31
+.icon-signal_cellular_1:before {
32
+  content: "\e902";
33
+}
34
+.icon-signal_cellular_2:before {
35
+  content: "\e907";
36
+}
28
 .icon-phone:before {
37
 .icon-phone:before {
29
   content: "\e0cd";
38
   content: "\e0cd";
30
 }
39
 }

BIN
fonts/jitsi.eot Dosyayı Görüntüle


+ 3
- 0
fonts/jitsi.svg Dosyayı Görüntüle

28
 <glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
28
 <glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
29
 <glyph unicode="&#xe8b6;" glyph-name="search" d="M406 426c106 0 192 86 192 192s-86 192-192 192-192-86-192-192 86-192 192-192zM662 426l212-212-64-64-212 212v34l-12 12c-48-42-112-66-180-66-154 0-278 122-278 276s124 278 278 278 276-124 276-278c0-68-24-132-66-180l12-12h34z" />
29
 <glyph unicode="&#xe8b6;" glyph-name="search" d="M406 426c106 0 192 86 192 192s-86 192-192 192-192-86-192-192 86-192 192-192zM662 426l212-212-64-64-212 212v34l-12 12c-48-42-112-66-180-66-154 0-278 122-278 276s124 278 278 278 276-124 276-278c0-68-24-132-66-180l12-12h34z" />
30
 <glyph unicode="&#xe900;" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
30
 <glyph unicode="&#xe900;" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
31
+<glyph unicode="&#xe901;" glyph-name="signal_cellular_0" d="M938 938v-852h-852zM854 732l-562-562h562v562z" />
32
+<glyph unicode="&#xe902;" glyph-name="signal_cellular_1" d="M86 86l852 852v-256h-170v-596h-682zM854 86v84h84v-84h-84zM854 256v342h84v-342h-84z" />
31
 <glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
33
 <glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
32
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
34
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
33
 <glyph unicode="&#xe905;" glyph-name="hangup" d="M512 640c-68 0-134-10-196-30v-132c0-16-10-34-24-40-42-20-80-46-114-78-8-8-18-12-30-12s-22 4-30 12l-106 106c-8 8-12 18-12 30s4 22 12 30c130 124 306 200 500 200s370-76 500-200c8-8 12-18 12-30s-4-22-12-30l-106-106c-8-8-18-12-30-12s-22 4-30 12c-34 32-72 58-114 78-14 6-24 20-24 38v132c-62 20-128 32-196 32z" />
35
 <glyph unicode="&#xe905;" glyph-name="hangup" d="M512 640c-68 0-134-10-196-30v-132c0-16-10-34-24-40-42-20-80-46-114-78-8-8-18-12-30-12s-22 4-30 12l-106 106c-8 8-12 18-12 30s4 22 12 30c130 124 306 200 500 200s370-76 500-200c8-8 12-18 12-30s-4-22-12-30l-106-106c-8-8-18-12-30-12s-22 4-30 12c-34 32-72 58-114 78-14 6-24 20-24 38v132c-62 20-128 32-196 32z" />
34
 <glyph unicode="&#xe906;" glyph-name="chat" d="M854 342v512h-684v-598l86 86h598zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
36
 <glyph unicode="&#xe906;" glyph-name="chat" d="M854 342v512h-684v-598l86 86h598zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
37
+<glyph unicode="&#xe907;" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
35
 <glyph unicode="&#xe908;" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
38
 <glyph unicode="&#xe908;" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
36
 <glyph unicode="&#xe909;" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
39
 <glyph unicode="&#xe909;" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
37
 <glyph unicode="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
40
 <glyph unicode="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />

BIN
fonts/jitsi.ttf Dosyayı Görüntüle


BIN
fonts/jitsi.woff Dosyayı Görüntüle


+ 1
- 1
fonts/selection.json
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 2
- 2
modules/UI/videolayout/SmallVideo.js Dosyayı Görüntüle

836
                                 isLocalVideo = { this.isLocal }
836
                                 isLocalVideo = { this.isLocal }
837
                                 enableStatsDisplay
837
                                 enableStatsDisplay
838
                                     = { !interfaceConfig.filmStripOnly }
838
                                     = { !interfaceConfig.filmStripOnly }
839
+                                participantId = { this.id }
839
                                 statsPopoverPosition
840
                                 statsPopoverPosition
840
-                                    = { statsPopoverPosition }
841
-                                userID = { this.id } />
841
+                                    = { statsPopoverPosition } />
842
                             : null }
842
                             : null }
843
                         <RaisedHandIndicator
843
                         <RaisedHandIndicator
844
                             iconSize = { iconSize }
844
                             iconSize = { iconSize }

react/features/filmstrip/components/native/BaseIndicator.js → react/features/base/react/components/native/BaseIndicator.js Dosyayı Görüntüle

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 import { View } from 'react-native';
4
 import { View } from 'react-native';
5
 
5
 
6
-import { Icon } from '../../../base/font-icons';
6
+import { Icon } from '../../../font-icons';
7
+import { type StyleType } from '../../../styles';
7
 
8
 
8
-import styles from './styles';
9
+import styles from './indicatorstyles';
9
 
10
 
10
 type Props = {
11
 type Props = {
11
 
12
 
17
     /**
18
     /**
18
      * The name of the icon to be used as the indicator.
19
      * The name of the icon to be used as the indicator.
19
      */
20
      */
20
-    icon: string
21
+    icon: string,
22
+
23
+    /**
24
+     * Additional style to be applied to the icon element.
25
+     */
26
+    iconStyle: StyleType
21
 };
27
 };
22
 
28
 
23
 /**
29
 /**
31
      * @inheritdoc
37
      * @inheritdoc
32
      */
38
      */
33
     render() {
39
     render() {
34
-        const { highlight, icon } = this.props;
40
+        const { highlight, icon, iconStyle } = this.props;
35
 
41
 
36
         return (
42
         return (
37
             <View style = { highlight ? styles.highlightedIndicator : null }>
43
             <View style = { highlight ? styles.highlightedIndicator : null }>
38
                 <Icon
44
                 <Icon
39
                     name = { icon }
45
                     name = { icon }
40
-                    style = { styles.indicator } />
46
+                    style = { [
47
+                        styles.indicator,
48
+                        iconStyle
49
+                    ] } />
41
             </View>
50
             </View>
42
         );
51
         );
43
     }
52
     }

+ 1
- 0
react/features/base/react/components/native/index.js Dosyayı Görüntüle

2
 
2
 
3
 export { default as AvatarListItem } from './AvatarListItem';
3
 export { default as AvatarListItem } from './AvatarListItem';
4
 export { default as BackButton } from './BackButton';
4
 export { default as BackButton } from './BackButton';
5
+export { default as BaseIndicator } from './BaseIndicator';
5
 export { default as Button } from './Button';
6
 export { default as Button } from './Button';
6
 export { default as Container } from './Container';
7
 export { default as Container } from './Container';
7
 export { default as ForwardButton } from './ForwardButton';
8
 export { default as ForwardButton } from './ForwardButton';

+ 28
- 0
react/features/base/react/components/native/indicatorstyles.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+import { ColorPalette } from '../../../styles';
4
+
5
+export default {
6
+    /**
7
+     * Highlighted indicator additional style.
8
+     */
9
+    highlightedIndicator: {
10
+        backgroundColor: ColorPalette.blue,
11
+        borderRadius: 16,
12
+        padding: 4
13
+    },
14
+
15
+    /**
16
+     * Base indicator style.
17
+     */
18
+    indicator: {
19
+        backgroundColor: ColorPalette.transparent,
20
+        color: ColorPalette.white,
21
+        fontSize: 12,
22
+        textShadowColor: ColorPalette.black,
23
+        textShadowOffset: {
24
+            height: -1,
25
+            width: 0
26
+        }
27
+    }
28
+};

react/features/filmstrip/components/web/BaseIndicator.js → react/features/base/react/components/web/BaseIndicator.js Dosyayı Görüntüle

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 import Tooltip from '@atlaskit/tooltip';
4
 import Tooltip from '@atlaskit/tooltip';
5
 
5
 
6
-import { translate } from '../../../base/i18n';
6
+import { translate } from '../../../i18n';
7
 
7
 
8
 /**
8
 /**
9
  * The type of the React {@code Component} props of {@link BaseIndicator}.
9
  * The type of the React {@code Component} props of {@link BaseIndicator}.

+ 3
- 0
react/features/base/react/components/web/index.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+export { default as BaseIndicator } from './BaseIndicator';
1
 export { default as Button } from './Button';
4
 export { default as Button } from './Button';
2
 export { default as Container } from './Container';
5
 export { default as Container } from './Container';
3
 export { default as Image } from './Image';
6
 export { default as Image } from './Image';

+ 173
- 0
react/features/connection-indicator/components/AbstractConnectionIndicator.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import statsEmitter from '../statsEmitter';
6
+
7
+declare var interfaceConfig: Object;
8
+
9
+/**
10
+ * The connection quality percentage that must be reached to be considered of
11
+ * good quality and can result in the connection indicator being hidden.
12
+ *
13
+ * @type {number}
14
+ */
15
+export const INDICATOR_DISPLAY_THRESHOLD = 30;
16
+
17
+/**
18
+ * The type of the React {@code Component} props of {@link ConnectionIndicator}.
19
+ */
20
+export type Props = {
21
+
22
+    /**
23
+     * The ID of the participant associated with the displayed connection indication and
24
+     * stats.
25
+     */
26
+    participantId: string
27
+};
28
+
29
+/**
30
+ * The type of the React {@code Component} state of {@link ConnectionIndicator}.
31
+ */
32
+export type State = {
33
+
34
+    /**
35
+     * Whether or not a CSS class should be applied to the root for hiding the
36
+     * connection indicator. By default the indicator should start out hidden
37
+     * because the current connection status is not known at mount.
38
+     */
39
+    showIndicator: boolean,
40
+
41
+    /**
42
+     * Cache of the stats received from subscribing to stats emitting. The keys
43
+     * should be the name of the stat. With each stat update, updates stats are
44
+     * mixed in with cached stats and a new stats object is set in state.
45
+     */
46
+    stats: Object
47
+};
48
+
49
+/**
50
+ * Implements a React {@link Component} which displays the current connection
51
+ * quality.
52
+ *
53
+ * @extends {Component}
54
+ */
55
+export default class AbstractConnectionIndicator<P: Props, S: State> extends Component<P, S> {
56
+    /**
57
+     * The timeout for automatically hiding the indicator.
58
+     */
59
+    autoHideTimeout: ?TimeoutID
60
+
61
+    /**
62
+     * Initializes a new {@code ConnectionIndicator} instance.
63
+     *
64
+     * @param {P} props - The read-only properties with which the new
65
+     * instance is to be initialized.
66
+     */
67
+    constructor(props: P) {
68
+        super(props);
69
+
70
+        // Bind event handlers so they are only bound once for every instance.
71
+        this._onStatsUpdated = this._onStatsUpdated.bind(this);
72
+    }
73
+
74
+    /**
75
+     * Starts listening for stat updates.
76
+     *
77
+     * @inheritdoc
78
+     * returns {void}
79
+     */
80
+    componentDidMount() {
81
+        statsEmitter.subscribeToClientStats(
82
+            this.props.participantId, this._onStatsUpdated);
83
+    }
84
+
85
+    /**
86
+     * Updates which user's stats are being listened to.
87
+     *
88
+     * @inheritdoc
89
+     * returns {void}
90
+     */
91
+    componentDidUpdate(prevProps: Props) {
92
+        if (prevProps.participantId !== this.props.participantId) {
93
+            statsEmitter.unsubscribeToClientStats(
94
+                prevProps.participantId, this._onStatsUpdated);
95
+            statsEmitter.subscribeToClientStats(
96
+                this.props.participantId, this._onStatsUpdated);
97
+        }
98
+    }
99
+
100
+    /**
101
+     * Cleans up any queued processes, which includes listening for new stats
102
+     * and clearing any timeout to hide the indicator.
103
+     *
104
+     * @private
105
+     * @returns {void}
106
+     */
107
+    componentWillUnmount() {
108
+        statsEmitter.unsubscribeToClientStats(
109
+            this.props.participantId, this._onStatsUpdated);
110
+
111
+        clearTimeout(this.autoHideTimeout);
112
+    }
113
+
114
+    _onStatsUpdated: (Object) => void;
115
+
116
+    /**
117
+     * Callback invoked when new connection stats associated with the passed in
118
+     * user ID are available. Will update the component's display of current
119
+     * statistics.
120
+     *
121
+     * @param {Object} stats - Connection stats from the library.
122
+     * @private
123
+     * @returns {void}
124
+     */
125
+    _onStatsUpdated(stats = {}) {
126
+        // Rely on React to batch setState actions.
127
+        const { connectionQuality } = stats;
128
+        const newPercentageState = typeof connectionQuality === 'undefined'
129
+            ? {} : { percent: connectionQuality };
130
+        const newStats = Object.assign(
131
+            {},
132
+            this.state.stats,
133
+            stats,
134
+            newPercentageState);
135
+
136
+        this.setState({
137
+            stats: newStats
138
+        });
139
+
140
+        this._updateIndicatorAutoHide(newStats.percent);
141
+    }
142
+
143
+    /**
144
+     * Updates the internal state for automatically hiding the indicator.
145
+     *
146
+     * @param {number} percent - The current connection quality percentage
147
+     * between the values 0 and 100.
148
+     * @private
149
+     * @returns {void}
150
+     */
151
+    _updateIndicatorAutoHide(percent) {
152
+        if (percent < INDICATOR_DISPLAY_THRESHOLD) {
153
+            clearTimeout(this.autoHideTimeout);
154
+            this.autoHideTimeout = undefined;
155
+
156
+            this.setState({
157
+                showIndicator: true
158
+            });
159
+        } else if (this.autoHideTimeout) {
160
+            // This clause is intentionally left blank because no further action
161
+            // is needed if the percent is below the threshold and there is an
162
+            // autoHideTimeout set.
163
+        } else {
164
+            this.autoHideTimeout = setTimeout(() => {
165
+                this.setState({
166
+                    showIndicator: false
167
+                });
168
+            }, typeof interfaceConfig === 'undefined'
169
+                ? 5000
170
+                : interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT);
171
+        }
172
+    }
173
+}

+ 3
- 0
react/features/connection-indicator/components/index.native.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+export * from './native';

+ 3
- 0
react/features/connection-indicator/components/index.web.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+export * from './web';

+ 62
- 0
react/features/connection-indicator/components/native/ConnectionIndicator.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { BaseIndicator } from '../../../base/react';
6
+import { connect } from '../../../base/redux';
7
+
8
+import AbstractConnectionIndicator, {
9
+    type Props,
10
+    type State
11
+} from '../AbstractConnectionIndicator';
12
+
13
+import { CONNECTOR_INDICATOR_COLORS } from './styles';
14
+
15
+/**
16
+ * Implements an indicator to show the quality of the connection of a participant.
17
+ */
18
+class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
19
+    /**
20
+     * Initializes a new {@code ConnectionIndicator} instance.
21
+     *
22
+     * @inheritdoc
23
+     */
24
+    constructor(props: Props) {
25
+        super(props);
26
+
27
+        this.state = {
28
+            autoHideTimeout: undefined,
29
+            showIndicator: false,
30
+            stats: {}
31
+        };
32
+    }
33
+
34
+    /**
35
+     * Implements React's {@link Component#render()}.
36
+     *
37
+     * @inheritdoc
38
+     * @returns {ReactElement}
39
+     */
40
+    render() {
41
+        const { showIndicator, stats } = this.state;
42
+        const { percent } = stats;
43
+
44
+        if (!showIndicator || typeof percent === 'undefined') {
45
+            return null;
46
+        }
47
+
48
+        // Signal level on a scale 0..2
49
+        const signalLevel = Math.floor(percent / 33.4);
50
+
51
+        return (
52
+            <BaseIndicator
53
+                icon = { `signal_cellular_${signalLevel}` }
54
+                iconStyle = {{
55
+                    color: CONNECTOR_INDICATOR_COLORS[signalLevel]
56
+                }} />
57
+        );
58
+    }
59
+
60
+}
61
+
62
+export default connect()(ConnectionIndicator);

react/features/connection-indicator/components/index.js → react/features/connection-indicator/components/native/index.js Dosyayı Görüntüle

1
+// @flow
2
+
1
 export { default as ConnectionIndicator } from './ConnectionIndicator';
3
 export { default as ConnectionIndicator } from './ConnectionIndicator';

+ 9
- 0
react/features/connection-indicator/components/native/styles.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+import { ColorPalette } from '../../../base/styles';
4
+
5
+export const CONNECTOR_INDICATOR_COLORS = [
6
+    ColorPalette.red,
7
+    ColorPalette.Y200,
8
+    ColorPalette.green
9
+];

react/features/connection-indicator/components/ConnectionIndicator.js → react/features/connection-indicator/components/web/ConnectionIndicator.js Dosyayı Görüntüle

1
-/* @flow */
1
+// @flow
2
 
2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4
 
4
 
5
-import { translate } from '../../base/i18n';
6
-import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
7
-import { Popover } from '../../base/popover';
8
-import { ConnectionStatsTable } from '../../connection-stats';
5
+import { translate } from '../../../base/i18n';
6
+import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
7
+import { Popover } from '../../../base/popover';
8
+import { ConnectionStatsTable } from '../../../connection-stats';
9
 
9
 
10
-import statsEmitter from '../statsEmitter';
10
+import AbstractConnectionIndicator, {
11
+    INDICATOR_DISPLAY_THRESHOLD,
12
+    type Props as AbstractProps,
13
+    type State as AbstractState
14
+} from '../AbstractConnectionIndicator';
11
 
15
 
12
 declare var interfaceConfig: Object;
16
 declare var interfaceConfig: Object;
13
 
17
 
14
-/**
15
- * The connection quality percentage that must be reached to be considered of
16
- * good quality and can result in the connection indicator being hidden.
17
- *
18
- * @type {number}
19
- */
20
-const INDICATOR_DISPLAY_THRESHOLD = 30;
21
-
22
 /**
18
 /**
23
  * An array of display configurations for the connection indicator and its bars.
19
  * An array of display configurations for the connection indicator and its bars.
24
  * The ordering is done specifically for faster iteration to find a matching
20
  * The ordering is done specifically for faster iteration to find a matching
58
 /**
54
 /**
59
  * The type of the React {@code Component} props of {@link ConnectionIndicator}.
55
  * The type of the React {@code Component} props of {@link ConnectionIndicator}.
60
  */
56
  */
61
-type Props = {
57
+type Props = AbstractProps & {
62
 
58
 
63
     /**
59
     /**
64
      * Whether or not the component should ignore setting a visibility class for
60
      * Whether or not the component should ignore setting a visibility class for
97
     /**
93
     /**
98
      * Invoked to obtain translated strings.
94
      * Invoked to obtain translated strings.
99
      */
95
      */
100
-    t: Function,
101
-
102
-    /**
103
-     * The user ID associated with the displayed connection indication and
104
-     * stats.
105
-     */
106
-    userID: string
96
+    t: Function
107
 };
97
 };
108
 
98
 
109
 /**
99
 /**
110
  * The type of the React {@code Component} state of {@link ConnectionIndicator}.
100
  * The type of the React {@code Component} state of {@link ConnectionIndicator}.
111
  */
101
  */
112
-type State = {
113
-
114
-    /**
115
-     * The timeout for automatically hiding the indicator.
116
-     */
117
-    autoHideTimeout: TimeoutID | null,
118
-
119
-    /**
120
-     * Whether or not a CSS class should be applied to the root for hiding the
121
-     * connection indicator. By default the indicator should start out hidden
122
-     * because the current connection status is not known at mount.
123
-     */
124
-    showIndicator: boolean,
102
+type State = AbstractState & {
125
 
103
 
126
     /**
104
     /**
127
      * Whether or not the popover content should display additional statistics.
105
      * Whether or not the popover content should display additional statistics.
128
      */
106
      */
129
-    showMoreStats: boolean,
130
-
131
-    /**
132
-     * Cache of the stats received from subscribing to stats emitting. The keys
133
-     * should be the name of the stat. With each stat update, updates stats are
134
-     * mixed in with cached stats and a new stats object is set in state.
135
-     */
136
-    stats: Object
107
+    showMoreStats: boolean
137
 };
108
 };
138
 
109
 
139
 /**
110
 /**
142
  *
113
  *
143
  * @extends {Component}
114
  * @extends {Component}
144
  */
115
  */
145
-class ConnectionIndicator extends Component<Props, State> {
116
+class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
146
     /**
117
     /**
147
      * Initializes a new {@code ConnectionIndicator} instance.
118
      * Initializes a new {@code ConnectionIndicator} instance.
148
      *
119
      *
153
         super(props);
124
         super(props);
154
 
125
 
155
         this.state = {
126
         this.state = {
156
-            autoHideTimeout: null,
127
+            autoHideTimeout: undefined,
157
             showIndicator: false,
128
             showIndicator: false,
158
             showMoreStats: false,
129
             showMoreStats: false,
159
             stats: {}
130
             stats: {}
160
         };
131
         };
161
 
132
 
162
         // Bind event handlers so they are only bound once for every instance.
133
         // Bind event handlers so they are only bound once for every instance.
163
-        this._onStatsUpdated = this._onStatsUpdated.bind(this);
164
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
134
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
165
     }
135
     }
166
 
136
 
167
-    /**
168
-     * Starts listening for stat updates.
169
-     *
170
-     * @inheritdoc
171
-     * returns {void}
172
-     */
173
-    componentDidMount() {
174
-        statsEmitter.subscribeToClientStats(
175
-            this.props.userID, this._onStatsUpdated);
176
-    }
177
-
178
-    /**
179
-     * Updates which user's stats are being listened to.
180
-     *
181
-     * @inheritdoc
182
-     * returns {void}
183
-     */
184
-    componentDidUpdate(prevProps) {
185
-        if (prevProps.userID !== this.props.userID) {
186
-            statsEmitter.unsubscribeToClientStats(
187
-                prevProps.userID, this._onStatsUpdated);
188
-            statsEmitter.subscribeToClientStats(
189
-                this.props.userID, this._onStatsUpdated);
190
-        }
191
-    }
192
-
193
-    /**
194
-     * Cleans up any queued processes, which includes listening for new stats
195
-     * and clearing any timeout to hide the indicator.
196
-     *
197
-     * @private
198
-     * @returns {void}
199
-     */
200
-    componentWillUnmount() {
201
-        statsEmitter.unsubscribeToClientStats(
202
-            this.props.userID, this._onStatsUpdated);
203
-
204
-        if (this.state.autoHideTimeout) {
205
-            clearTimeout(this.state.autoHideTimeout);
206
-        }
207
-    }
208
-
209
     /**
137
     /**
210
      * Implements React's {@link Component#render()}.
138
      * Implements React's {@link Component#render()}.
211
      *
139
      *
331
             ? 'show-connection-indicator' : 'hide-connection-indicator';
259
             ? 'show-connection-indicator' : 'hide-connection-indicator';
332
     }
260
     }
333
 
261
 
334
-    _onStatsUpdated: (Object) => void;
335
-
336
-    /**
337
-     * Callback invoked when new connection stats associated with the passed in
338
-     * user ID are available. Will update the component's display of current
339
-     * statistics.
340
-     *
341
-     * @param {Object} stats - Connection stats from the library.
342
-     * @private
343
-     * @returns {void}
344
-     */
345
-    _onStatsUpdated(stats = {}) {
346
-        // Rely on React to batch setState actions.
347
-        const { connectionQuality } = stats;
348
-        const newPercentageState = typeof connectionQuality === 'undefined'
349
-            ? {} : { percent: connectionQuality };
350
-        const newStats = Object.assign(
351
-            {},
352
-            this.state.stats,
353
-            stats,
354
-            newPercentageState);
355
-
356
-        this.setState({
357
-            stats: newStats
358
-        });
359
-
360
-        this._updateIndicatorAutoHide(newStats.percent);
361
-    }
362
-
363
     _onToggleShowMore: () => void;
262
     _onToggleShowMore: () => void;
364
 
263
 
365
     /**
264
     /**
459
                 transport = { transport } />
358
                 transport = { transport } />
460
         );
359
         );
461
     }
360
     }
462
-
463
-    /**
464
-     * Updates the internal state for automatically hiding the indicator.
465
-     *
466
-     * @param {number} percent - The current connection quality percentage
467
-     * between the values 0 and 100.
468
-     * @private
469
-     * @returns {void}
470
-     */
471
-    _updateIndicatorAutoHide(percent) {
472
-        if (percent < INDICATOR_DISPLAY_THRESHOLD) {
473
-            if (this.state.autoHideTimeout) {
474
-                clearTimeout(this.state.autoHideTimeout);
475
-            }
476
-
477
-            this.setState({
478
-                autoHideTimeout: null,
479
-                showIndicator: true
480
-            });
481
-        } else if (this.state.autoHideTimeout) {
482
-            // This clause is intentionally left blank because no further action
483
-            // is needed if the percent is below the threshold and there is an
484
-            // autoHideTimeout set.
485
-        } else {
486
-            this.setState({
487
-                autoHideTimeout: setTimeout(() => {
488
-                    this.setState({
489
-                        showIndicator: false
490
-                    });
491
-                }, interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT)
492
-            });
493
-        }
494
-    }
495
 }
361
 }
496
 
362
 
497
 export default translate(ConnectionIndicator);
363
 export default translate(ConnectionIndicator);

+ 3
- 0
react/features/connection-indicator/components/web/index.js Dosyayı Görüntüle

1
+// @flow
2
+
3
+export { default as ConnectionIndicator } from './ConnectionIndicator';

+ 2
- 0
react/features/connection-indicator/index.js Dosyayı Görüntüle

1
+// @flow
2
+
1
 export * from './components';
3
 export * from './components';
2
 
4
 
3
 export { default as statsEmitter } from './statsEmitter';
5
 export { default as statsEmitter } from './statsEmitter';

+ 0
- 2
react/features/connection-indicator/statsEmitter.js Dosyayı Görüntüle

7
     JitsiE2ePingEvents
7
     JitsiE2ePingEvents
8
 } from '../base/lib-jitsi-meet';
8
 } from '../base/lib-jitsi-meet';
9
 
9
 
10
-declare var APP: Object;
11
-
12
 /**
10
 /**
13
  * Contains all the callbacks to be notified when stats are updated.
11
  * Contains all the callbacks to be notified when stats are updated.
14
  *
12
  *

+ 1
- 1
react/features/filmstrip/components/native/AudioMutedIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * Thumbnail badge for displaying the audio mute status of a participant.
8
  * Thumbnail badge for displaying the audio mute status of a participant.

+ 1
- 1
react/features/filmstrip/components/native/DominantSpeakerIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * Thumbnail badge showing that the participant is the dominant speaker in
8
  * Thumbnail badge showing that the participant is the dominant speaker in

+ 1
- 1
react/features/filmstrip/components/native/ModeratorIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * Thumbnail badge showing that the participant is a conference moderator.
8
  * Thumbnail badge showing that the participant is a conference moderator.

+ 1
- 2
react/features/filmstrip/components/native/RaisedHandIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
+import { BaseIndicator } from '../../../base/react';
5
 import { connect } from '../../../base/redux';
6
 import { connect } from '../../../base/redux';
6
 
7
 
7
 import AbstractRaisedHandIndicator, {
8
 import AbstractRaisedHandIndicator, {
9
     _mapStateToProps
10
     _mapStateToProps
10
 } from '../AbstractRaisedHandIndicator';
11
 } from '../AbstractRaisedHandIndicator';
11
 
12
 
12
-import BaseIndicator from './BaseIndicator';
13
-
14
 /**
13
 /**
15
  * Thumbnail badge showing that the participant would like to speak.
14
  * Thumbnail badge showing that the participant would like to speak.
16
  *
15
  *

+ 14
- 1
react/features/filmstrip/components/native/Thumbnail.js Dosyayı Görüntüle

17
 import { connect } from '../../../base/redux';
17
 import { connect } from '../../../base/redux';
18
 import { StyleType } from '../../../base/styles';
18
 import { StyleType } from '../../../base/styles';
19
 import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
19
 import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
20
+import { ConnectionIndicator } from '../../../connection-indicator';
20
 import { DisplayNameLabel } from '../../../display-name';
21
 import { DisplayNameLabel } from '../../../display-name';
21
 import { RemoteVideoMenu } from '../../../remote-video-menu';
22
 import { RemoteVideoMenu } from '../../../remote-video-menu';
22
 
23
 
176
                         <ModeratorIndicator />
177
                         <ModeratorIndicator />
177
                     </View> }
178
                     </View> }
178
 
179
 
179
-                <View style = { styles.thumbnailTopIndicatorContainer }>
180
+                <View
181
+                    style = { [
182
+                        styles.thumbnailTopIndicatorContainer,
183
+                        styles.thumbnailTopLeftIndicatorContainer
184
+                    ] }>
180
                     <RaisedHandIndicator participantId = { participant.id } />
185
                     <RaisedHandIndicator participantId = { participant.id } />
181
                     { participant.dominantSpeaker
186
                     { participant.dominantSpeaker
182
                         && <DominantSpeakerIndicator /> }
187
                         && <DominantSpeakerIndicator /> }
183
                 </View>
188
                 </View>
184
 
189
 
190
+                <View
191
+                    style = { [
192
+                        styles.thumbnailTopIndicatorContainer,
193
+                        styles.thumbnailTopRightIndicatorContainer
194
+                    ] }>
195
+                    <ConnectionIndicator participantId = { participant.id } />
196
+                </View>
197
+
185
                 <Container style = { styles.thumbnailIndicatorContainer }>
198
                 <Container style = { styles.thumbnailIndicatorContainer }>
186
                     { audioMuted
199
                     { audioMuted
187
                         && <AudioMutedIndicator /> }
200
                         && <AudioMutedIndicator /> }

+ 1
- 1
react/features/filmstrip/components/native/VideoMutedIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * Thumbnail badge for displaying the video mute status of a participant.
8
  * Thumbnail badge for displaying the video mute status of a participant.

+ 8
- 23
react/features/filmstrip/components/native/styles.js Dosyayı Görüntüle

14
  * The styles of the feature filmstrip.
14
  * The styles of the feature filmstrip.
15
  */
15
  */
16
 export default {
16
 export default {
17
-    /**
18
-     * Highlighted indicator additional style.
19
-     */
20
-    highlightedIndicator: {
21
-        backgroundColor: ColorPalette.blue,
22
-        borderRadius: 16,
23
-        padding: 4
24
-    },
25
-
26
-    /**
27
-     * Dominant speaker indicator style.
28
-     */
29
-    indicator: {
30
-        backgroundColor: ColorPalette.transparent,
31
-        color: ColorPalette.white,
32
-        fontSize: 12,
33
-        textShadowColor: ColorPalette.black,
34
-        textShadowOffset: {
35
-            height: -1,
36
-            width: 0
37
-        }
38
-    },
39
 
17
 
40
     /**
18
     /**
41
      * The style of the narrow {@link Filmstrip} version which displays
19
      * The style of the narrow {@link Filmstrip} version which displays
121
     },
99
     },
122
 
100
 
123
     thumbnailTopIndicatorContainer: {
101
     thumbnailTopIndicatorContainer: {
124
-        left: 0,
125
         padding: 4,
102
         padding: 4,
126
         position: 'absolute',
103
         position: 'absolute',
127
         top: 0
104
         top: 0
128
     },
105
     },
129
 
106
 
107
+    thumbnailTopLeftIndicatorContainer: {
108
+        left: 0
109
+    },
110
+
111
+    thumbnailTopRightIndicatorContainer: {
112
+        right: 0
113
+    },
114
+
130
     tileView: {
115
     tileView: {
131
         alignSelf: 'center'
116
         alignSelf: 'center'
132
     },
117
     },

+ 1
- 1
react/features/filmstrip/components/web/AudioMutedIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * The type of the React {@code Component} props of {@link AudioMutedIndicator}.
8
  * The type of the React {@code Component} props of {@link AudioMutedIndicator}.

+ 1
- 1
react/features/filmstrip/components/web/DominantSpeakerIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * The type of the React {@code Component} props of
8
  * The type of the React {@code Component} props of

+ 1
- 1
react/features/filmstrip/components/web/ModeratorIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * The type of the React {@code Component} props of {@link ModeratorIndicator}.
8
  * The type of the React {@code Component} props of {@link ModeratorIndicator}.

+ 1
- 2
react/features/filmstrip/components/web/RaisedHandIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
+import { BaseIndicator } from '../../../base/react';
5
 import { connect } from '../../../base/redux';
6
 import { connect } from '../../../base/redux';
6
 
7
 
7
 import AbstractRaisedHandIndicator, {
8
 import AbstractRaisedHandIndicator, {
9
     _mapStateToProps
10
     _mapStateToProps
10
 } from '../AbstractRaisedHandIndicator';
11
 } from '../AbstractRaisedHandIndicator';
11
 
12
 
12
-import BaseIndicator from './BaseIndicator';
13
-
14
 /**
13
 /**
15
  * The type of the React {@code Component} props of {@link RaisedHandIndicator}.
14
  * The type of the React {@code Component} props of {@link RaisedHandIndicator}.
16
  */
15
  */

+ 1
- 1
react/features/filmstrip/components/web/VideoMutedIndicator.js Dosyayı Görüntüle

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
-import BaseIndicator from './BaseIndicator';
5
+import { BaseIndicator } from '../../../base/react';
6
 
6
 
7
 /**
7
 /**
8
  * The type of the React {@code Component} props of {@link VideoMutedIndicator}.
8
  * The type of the React {@code Component} props of {@link VideoMutedIndicator}.

Loading…
İptal
Kaydet