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

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