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 10KB

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