You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Thumbnail.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // @flow
  2. import React, { PureComponent } from 'react';
  3. import { View } from 'react-native';
  4. import type { Dispatch } from 'redux';
  5. import { ColorSchemeRegistry } from '../../../base/color-scheme';
  6. import { openDialog } from '../../../base/dialog';
  7. import { MEDIA_TYPE, VIDEO_TYPE } from '../../../base/media';
  8. import {
  9. PARTICIPANT_ROLE,
  10. ParticipantView,
  11. getParticipantCount,
  12. isEveryoneModerator,
  13. pinParticipant,
  14. getParticipantByIdOrUndefined,
  15. getLocalParticipant
  16. } from '../../../base/participants';
  17. import { Container } from '../../../base/react';
  18. import { connect } from '../../../base/redux';
  19. import { StyleType } from '../../../base/styles';
  20. import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
  21. import { ConnectionIndicator } from '../../../connection-indicator';
  22. import { DisplayNameLabel } from '../../../display-name';
  23. import { toggleToolboxVisible } from '../../../toolbox/actions.native';
  24. import { RemoteVideoMenu } from '../../../video-menu';
  25. import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
  26. import SharedVideoMenu from '../../../video-menu/components/native/SharedVideoMenu';
  27. import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
  28. import AudioMutedIndicator from './AudioMutedIndicator';
  29. import DominantSpeakerIndicator from './DominantSpeakerIndicator';
  30. import ModeratorIndicator from './ModeratorIndicator';
  31. import RaisedHandIndicator from './RaisedHandIndicator';
  32. import ScreenShareIndicator from './ScreenShareIndicator';
  33. import VideoMutedIndicator from './VideoMutedIndicator';
  34. import styles, { AVATAR_SIZE } from './styles';
  35. /**
  36. * Thumbnail component's property types.
  37. */
  38. type Props = {
  39. /**
  40. * Whether local audio (microphone) is muted or not.
  41. */
  42. _audioMuted: boolean,
  43. /**
  44. * Indicates whether the participant is fake.
  45. */
  46. _isFakeParticipant: boolean,
  47. /**
  48. * Indicates whether the participant is fake.
  49. */
  50. _isScreenShare: boolean,
  51. /**
  52. * Indicates whether the participant is local.
  53. */
  54. _local: boolean,
  55. /**
  56. * Shared video local participant owner.
  57. */
  58. _localVideoOwner: boolean,
  59. /**
  60. * The ID of the participant obtain from the participant object in Redux.
  61. *
  62. * NOTE: Generally it should be the same as the participantID prop except the case where the passed
  63. * participantID doesn't corespond to any of the existing participants.
  64. */
  65. _participantId: string,
  66. /**
  67. * Indicates whether the participant is displayed on the large video.
  68. */
  69. _participantInLargeVideo: boolean,
  70. /**
  71. * Indicates whether the participant is pinned or not.
  72. */
  73. _pinned: boolean,
  74. /**
  75. * Whether to show the dominant speaker indicator or not.
  76. */
  77. _renderDominantSpeakerIndicator: boolean,
  78. /**
  79. * Whether to show the moderator indicator or not.
  80. */
  81. _renderModeratorIndicator: boolean,
  82. /**
  83. * The color-schemed stylesheet of the feature.
  84. */
  85. _styles: StyleType,
  86. /**
  87. * Indicates whether the participant is video muted.
  88. */
  89. _videoMuted: boolean,
  90. /**
  91. * If true, there will be no color overlay (tint) on the thumbnail
  92. * indicating the participant associated with the thumbnail is displayed on
  93. * large video. By default there will be a tint.
  94. */
  95. disableTint?: boolean,
  96. /**
  97. * Invoked to trigger state changes in Redux.
  98. */
  99. dispatch: Dispatch<any>,
  100. /**
  101. * The height of the thumnail.
  102. */
  103. height: ?number,
  104. /**
  105. * The ID of the participant related to the thumbnail.
  106. */
  107. participantID: ?string,
  108. /**
  109. * Whether to display or hide the display name of the participant in the thumbnail.
  110. */
  111. renderDisplayName: ?boolean,
  112. /**
  113. * If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
  114. */
  115. tileView?: boolean
  116. };
  117. /**
  118. * React component for video thumbnail.
  119. */
  120. class Thumbnail extends PureComponent<Props> {
  121. /**
  122. * Creates new Thumbnail component.
  123. *
  124. * @param {Props} props - The props of the component.
  125. * @returns {Thumbnail}
  126. */
  127. constructor(props: Props) {
  128. super(props);
  129. this._onClick = this._onClick.bind(this);
  130. this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
  131. }
  132. _onClick: () => void;
  133. /**
  134. * Thumbnail click handler.
  135. *
  136. * @returns {void}
  137. */
  138. _onClick() {
  139. const { _participantId, _pinned, dispatch, tileView } = this.props;
  140. if (tileView) {
  141. dispatch(toggleToolboxVisible());
  142. } else {
  143. dispatch(pinParticipant(_pinned ? null : _participantId));
  144. }
  145. }
  146. _onThumbnailLongPress: () => void;
  147. /**
  148. * Thumbnail long press handler.
  149. *
  150. * @returns {void}
  151. */
  152. _onThumbnailLongPress() {
  153. const { _participantId, _local, _isFakeParticipant, _localVideoOwner, dispatch } = this.props;
  154. if (_local) {
  155. dispatch(openDialog(ConnectionStatusComponent, {
  156. participantID: _participantId
  157. }));
  158. } else if (_isFakeParticipant) {
  159. if (_localVideoOwner) {
  160. dispatch(openDialog(SharedVideoMenu, {
  161. _participantId
  162. }));
  163. }
  164. } else {
  165. dispatch(openDialog(RemoteVideoMenu, {
  166. participantId: _participantId
  167. }));
  168. }
  169. }
  170. /**
  171. * Implements React's {@link Component#render()}.
  172. *
  173. * @inheritdoc
  174. * @returns {ReactElement}
  175. */
  176. render() {
  177. const {
  178. _audioMuted: audioMuted,
  179. _isScreenShare: isScreenShare,
  180. _isFakeParticipant,
  181. _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
  182. _renderModeratorIndicator: renderModeratorIndicator,
  183. _participantId: participantId,
  184. _participantInLargeVideo: participantInLargeVideo,
  185. _pinned,
  186. _styles,
  187. _videoMuted: videoMuted,
  188. disableTint,
  189. height,
  190. renderDisplayName,
  191. tileView
  192. } = this.props;
  193. const styleOverrides = tileView ? {
  194. aspectRatio: SQUARE_TILE_ASPECT_RATIO,
  195. flex: 0,
  196. height,
  197. maxHeight: null,
  198. maxWidth: null,
  199. width: null
  200. } : null;
  201. return (
  202. <Container
  203. onClick = { this._onClick }
  204. onLongPress = { this._onThumbnailLongPress }
  205. style = { [
  206. styles.thumbnail,
  207. _pinned && !tileView ? _styles.thumbnailPinned : null,
  208. styleOverrides
  209. ] }
  210. touchFeedback = { false }>
  211. <ParticipantView
  212. avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
  213. disableVideo = { isScreenShare || _isFakeParticipant }
  214. participantId = { participantId }
  215. style = { _styles.participantViewStyle }
  216. tintEnabled = { participantInLargeVideo && !disableTint }
  217. tintStyle = { _styles.activeThumbnailTint }
  218. zOrder = { 1 } />
  219. {
  220. renderDisplayName
  221. && <Container style = { styles.displayNameContainer }>
  222. <DisplayNameLabel participantId = { participantId } />
  223. </Container>
  224. }
  225. { renderModeratorIndicator
  226. && <View style = { styles.moderatorIndicatorContainer }>
  227. <ModeratorIndicator />
  228. </View>
  229. }
  230. {
  231. !_isFakeParticipant
  232. && <View
  233. style = { [
  234. styles.thumbnailTopIndicatorContainer,
  235. styles.thumbnailTopLeftIndicatorContainer
  236. ] }>
  237. <RaisedHandIndicator participantId = { participantId } />
  238. { renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
  239. </View>
  240. }
  241. {
  242. !_isFakeParticipant
  243. && <View
  244. style = { [
  245. styles.thumbnailTopIndicatorContainer,
  246. styles.thumbnailTopRightIndicatorContainer
  247. ] }>
  248. <ConnectionIndicator participantId = { participantId } />
  249. </View>
  250. }
  251. {
  252. !_isFakeParticipant
  253. && <Container style = { styles.thumbnailIndicatorContainer }>
  254. { audioMuted && <AudioMutedIndicator /> }
  255. { videoMuted && <VideoMutedIndicator /> }
  256. { isScreenShare && <ScreenShareIndicator /> }
  257. </Container>
  258. }
  259. </Container>
  260. );
  261. }
  262. }
  263. /**
  264. * Function that maps parts of Redux state tree into component props.
  265. *
  266. * @param {Object} state - Redux state.
  267. * @param {Props} ownProps - Properties of component.
  268. * @returns {Object}
  269. */
  270. function _mapStateToProps(state, ownProps) {
  271. // We need read-only access to the state of features/large-video so that the
  272. // filmstrip doesn't render the video of the participant who is rendered on
  273. // the stage i.e. as a large video.
  274. const largeVideo = state['features/large-video'];
  275. const { ownerId } = state['features/shared-video'];
  276. const tracks = state['features/base/tracks'];
  277. const { participantID } = ownProps;
  278. const participant = getParticipantByIdOrUndefined(state, participantID);
  279. const localParticipantId = getLocalParticipant(state).id;
  280. const id = participant?.id;
  281. const audioTrack
  282. = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
  283. const videoTrack
  284. = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
  285. const videoMuted = !videoTrack || videoTrack.muted;
  286. const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
  287. const participantCount = getParticipantCount(state);
  288. const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
  289. const _isEveryoneModerator = isEveryoneModerator(state);
  290. const renderModeratorIndicator = !_isEveryoneModerator
  291. && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
  292. const participantInLargeVideo = id === largeVideo.participantId;
  293. return {
  294. _audioMuted: audioTrack?.muted ?? true,
  295. _isScreenShare: isScreenShare,
  296. _isFakeParticipant: participant?.isFakeParticipant,
  297. _local: participant?.local,
  298. _localVideoOwner: Boolean(ownerId === localParticipantId),
  299. _participantInLargeVideo: participantInLargeVideo,
  300. _participantId: id,
  301. _pinned: participant?.pinned,
  302. _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
  303. _renderModeratorIndicator: renderModeratorIndicator,
  304. _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
  305. _videoMuted: videoMuted
  306. };
  307. }
  308. export default connect(_mapStateToProps)(Thumbnail);