diff --git a/web/app.react.js b/web/app.react.js index efab05d57..906ce3149 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,402 +1,402 @@ // @flow import 'basscss/css/basscss.min.css'; import './theme.css'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import classnames from 'classnames'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { WagmiConfig } from 'wagmi'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, } from 'lib/actions/entry-actions.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ModalProvider, useModalContext, } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { registerConfig } from 'lib/utils/config.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { AlchemyENSCacheProvider, wagmiConfig } from 'lib/utils/wagmi-utils.js'; import QrCodeLogin from './account/qr-code-login.react.js'; import AppThemeWrapper from './app-theme-wrapper.react.js'; import WebEditThreadAvatarProvider from './avatars/web-edit-thread-avatar-provider.react.js'; import Calendar from './calendar/calendar.react.js'; import Chat from './chat/chat.react.js'; import { EditModalProvider } from './chat/edit-message-provider.js'; import { TooltipProvider } from './chat/tooltip-provider.js'; import NavigationArrows from './components/navigation-arrows.react.js'; import { initOpaque } from './crypto/opaque-utils.js'; import electron from './electron.js'; import InputStateContainer from './input/input-state-container.react.js'; import InviteLinkHandler from './invite-links/invite-link-handler.react.js'; import InviteLinksRefresher from './invite-links/invite-links-refresher.react.js'; import LoadingIndicator from './loading-indicator.react.js'; import { MenuProvider } from './menu-provider.react.js'; import UpdateModalHandler from './modals/update-modal.react.js'; import SettingsSwitcher from './navigation-panels/settings-switcher.react.js'; import Topbar from './navigation-panels/topbar.react.js'; import useBadgeHandler from './push-notif/badge-handler.react.js'; import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js'; import { updateNavInfoActionType } from './redux/action-types.js'; import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler.js'; import DisconnectedBar from './redux/disconnected-bar.js'; import FocusHandler from './redux/focus-handler.react.js'; import { persistConfig } from './redux/persist.js'; import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js'; import { useSelector } from './redux/redux-utils.js'; import VisibilityHandler from './redux/visibility-handler.react.js'; import history from './router-history.js'; import { MessageSearchStateProvider } from './search/message-search-state-provider.react.js'; import { createTunnelbrokerInitMessage } from './selectors/tunnelbroker-selectors.js'; import AccountSettings from './settings/account-settings.react.js'; import DangerZone from './settings/danger-zone.react.js'; import KeyserverSelectionList from './settings/keyserver-selection-list.react.js'; import CommunityPicker from './sidebar/community-picker.react.js'; import Splash from './splash/splash.react.js'; import './typography.css'; import css from './style.css'; import { type NavInfo } from './types/nav-types.js'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js'; initOpaque(); // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS, // so we disable the autoAddCss logic and import the CSS file. Otherwise every // icon flashes huge for a second before the CSS is loaded. import '@fortawesome/fontawesome-svg-core/styles.css'; faConfig.autoAddCss = false; registerConfig({ // We can't securely cache credentials on web, so we have no way to recover // from a cookie invalidation resolveInvalidatedCookie: null, setSessionIDOnRequest: true, // Never reset the calendar range calendarRangeInactivityLimit: null, platformDetails: { platform: electron?.platform ?? 'web', codeVersion: 43, stateVersion: persistConfig.version, }, }); type BaseProps = { +location: { +pathname: string, ... }, }; type Props = { ...BaseProps, // Redux state +navInfo: NavInfo, +entriesLoadingStatus: LoadingStatus, +loggedIn: boolean, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { const { navInfo, location: { pathname }, loggedIn, } = this.props; const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (pathname !== newURL) { history.replace(newURL); } } componentDidUpdate(prevProps: Props) { const { navInfo, location: { pathname }, loggedIn, } = this.props; if (!_isEqual(navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.push(newURL); } } else if (pathname !== prevProps.location.pathname) { const urlInfo = infoFromURL(pathname); const newNavInfo = navInfoFromURL(urlInfo, { navInfo }); if (!_isEqual(newNavInfo)(navInfo)) { this.props.dispatch({ type: updateNavInfoActionType, payload: newNavInfo, }); } } else if (loggedIn !== prevProps.loggedIn) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.replace(newURL); } } if (loggedIn !== prevProps.loggedIn) { electron?.clearHistory(); } } onWordmarkClicked = () => { this.props.dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat' }, }); }; render(): React.Node { let content; if (this.props.loggedIn) { content = ( <> {this.renderMainContent()} {this.props.modals} ); } else { content = ( <> {this.renderLoginPage()} {this.props.modals} ); } return ( {content} ); } onHeaderDoubleClick = (): void => electron?.doubleClickTopBar(); stopDoubleClickPropagation: ?(SyntheticEvent) => void = electron ? e => e.stopPropagation() : null; renderLoginPage(): React.Node { const { loginMethod } = this.props.navInfo; if (loginMethod === 'qr-code') { return ; } return ; } renderMainContent(): React.Node { const mainContent = this.getMainContentWithSwitcher(); let navigationArrows = null; if (electron) { navigationArrows = ; } const headerClasses = classnames({ [css.header]: true, [css['electron-draggable']]: electron, }); const wordmarkClasses = classnames({ [css.wordmark]: true, [css['electron-non-draggable']]: electron, [css['wordmark-macos']]: electron?.platform === 'macos', }); return (

Comm

{navigationArrows}
{mainContent}
); } getMainContentWithSwitcher(): React.Node { const { tab, settingsSection } = this.props.navInfo; - let mainContent; + let mainContent: React.Node; if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'keyservers') { mainContent = ; } else if (settingsSection === 'danger-zone') { mainContent = ; } return (
{mainContent}
); } if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } const mainContentClass = classnames( css['main-content-container'], css['main-content-container-column'], ); return (
{mainContent}
); } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( function ConnectedApp(props) { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const navInfo = useSelector(state => state.navInfo); const fetchEntriesLoadingStatus = useSelector( fetchEntriesLoadingStatusSelector, ); const updateCalendarQueryLoadingStatus = useSelector( updateCalendarQueryLoadingStatusSelector, ); const entriesLoadingStatus = combineLoadingStatuses( fetchEntriesLoadingStatus, updateCalendarQueryLoadingStatus, ); const loggedIn = useSelector(isLoggedIn); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); useBadgeHandler(); const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( () => modalContext.modals.map(([modal, key]) => ( {modal} )), [modalContext.modals], ); const tunnelbrokerInitMessage = useSelector(createTunnelbrokerInitMessage); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } export default AppWithProvider; diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js index 71ee4850d..dcf18cab6 100644 --- a/web/calendar/filter-panel.react.js +++ b/web/calendar/filter-panel.react.js @@ -1,425 +1,425 @@ // @flow import { faCog, faTimesCircle, faChevronUp, faChevronDown, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import { ChevronsLeft } from 'react-feather'; import Switch from 'react-switch'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react.js'; import { filteredThreadIDsSelector, includeDeletedSelector, } from 'lib/selectors/calendar-filter-selectors.js'; import SearchIndex from 'lib/shared/search-index.js'; import { calendarThreadFilterTypes, type FilterThreadInfo, updateCalendarThreadFilter, clearCalendarThreadFilter, setCalendarDeletedFilter, } from 'lib/types/filter-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './filter-panel.css'; import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useFilterThreadInfos, useFilterThreadSearchIndex, filterThreadIDsBelongingToCommunitySelector, } from '../selectors/calendar-selectors.js'; import { MagnifyingGlass } from '../vectors.react.js'; type Props = { +filterThreadInfos: $ReadOnlyArray, +filterThreadSearchIndex: SearchIndex, +filteredThreadIDs: ?$ReadOnlySet, +filteredCommunityThreadIDs: ?$ReadOnlySet, +includeDeleted: boolean, +dispatch: Dispatch, +pushModal: PushModal, +toggleFilters: (event: SyntheticEvent) => void, }; type State = { +query: string, +searchResults: $ReadOnlyArray, +collapsed: boolean, }; class FilterPanel extends React.PureComponent { state: State = { query: '', searchResults: [], collapsed: false, }; currentlySelected(threadID: string): boolean { if (!this.props.filteredThreadIDs) { return true; } return this.props.filteredThreadIDs.has(threadID); } inCurrentCommunity(threadID: string): boolean { if (!this.props.filteredCommunityThreadIDs) { return true; } return this.props.filteredCommunityThreadIDs.has(threadID); } render(): React.Node { const filterThreadInfos = this.state.query ? this.state.searchResults : this.props.filterThreadInfos; const filterThreadInfosInCurrentCommunity = filterThreadInfos.filter(item => this.inCurrentCommunity(item.threadInfo.id), ); - let filters = []; + let filters: React.Node[] = []; if (!this.state.query || filterThreadInfosInCurrentCommunity.length > 0) { filters.push( , ); } else { filters.push(
No results
, ); } if (!this.state.collapsed) { const options = filterThreadInfosInCurrentCommunity.map( filterThreadInfo => ( ), ); filters = [...filters, ...options]; } let clearQueryButton = null; if (this.state.query) { clearQueryButton = ( ); } return (
{clearQueryButton}
{filters}
); } onToggle = (threadID: string, value: boolean) => { let newThreadIDs; const selectedThreadIDs = this.props.filteredThreadIDs; if (!selectedThreadIDs && value) { // No thread filter exists and thread is being added return; } else if (!selectedThreadIDs) { // No thread filter exists and thread is being removed newThreadIDs = this.props.filterThreadInfos .map(filterThreadInfo => filterThreadInfo.threadInfo.id) .filter(id => id !== threadID); } else if (selectedThreadIDs.has(threadID) && value) { // Thread filter already includes thread being added return; } else if (selectedThreadIDs.has(threadID)) { // Thread being removed from current thread filter newThreadIDs = [...selectedThreadIDs].filter(id => id !== threadID); } else if (!value) { // Thread filter doesn't include thread being removed return; } else if ( selectedThreadIDs.size + 1 === this.props.filterThreadInfos.length ) { // Thread filter exists and thread being added is the only one missing newThreadIDs = null; } else { // Thread filter exists and thread is being added newThreadIDs = [...selectedThreadIDs, threadID]; } this.setFilterThreads(newThreadIDs); }; onToggleAll = (value: boolean) => { if (!value) { this.setFilterThreads([]); return; } const allChats = this.props.filteredCommunityThreadIDs ? Array.from(this.props.filteredCommunityThreadIDs) : null; this.setFilterThreads(allChats); }; onClickOnly = (threadID: string) => { this.setFilterThreads([threadID]); }; setFilterThreads(threadIDs: ?$ReadOnlyArray) { if (!threadIDs) { this.props.dispatch({ type: clearCalendarThreadFilter, }); } else { this.props.dispatch({ type: updateCalendarThreadFilter, payload: { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs, }, }); } } onClickSettings = (threadID: string) => { this.props.pushModal(); }; onChangeQuery = (event: SyntheticEvent) => { const query = event.currentTarget.value; const searchIndex = this.props.filterThreadSearchIndex; const resultIDs = new Set(searchIndex.getSearchResults(query)); const results = this.props.filterThreadInfos.filter(filterThreadInfo => resultIDs.has(filterThreadInfo.threadInfo.id), ); this.setState({ query, searchResults: results, collapsed: false }); }; clearQuery = (event: SyntheticEvent) => { event.preventDefault(); this.setState({ query: '', searchResults: [], collapsed: false }); }; onCollapse = (value: boolean) => { this.setState({ collapsed: value }); }; onChangeIncludeDeleted = (includeDeleted: boolean) => { this.props.dispatch({ type: setCalendarDeletedFilter, payload: { includeDeleted, }, }); }; } type ItemProps = { +filterThreadInfo: FilterThreadInfo, +onToggle: (threadID: string, value: boolean) => void, +onClickOnly: (threadID: string) => void, +onClickSettings: (threadID: string) => void, +selected: boolean, }; class Item extends React.PureComponent { render(): React.Node { const threadInfo = this.props.filterThreadInfo.threadInfo; const beforeCheckStyles = { borderColor: `#${threadInfo.color}` }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: `#${threadInfo.color}` }; afterCheck = (
); } const details = this.props.filterThreadInfo.numVisibleEntries === 1 ? '1 entry' : `${this.props.filterThreadInfo.numVisibleEntries} entries`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle( this.props.filterThreadInfo.threadInfo.id, event.currentTarget.checked, ); }; onClickOnly = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickOnly(this.props.filterThreadInfo.threadInfo.id); }; onClickSettings = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickSettings(this.props.filterThreadInfo.threadInfo.id); }; } type CategoryProps = { +numThreads: number, +onToggle: (value: boolean) => void, +collapsed: boolean, +onCollapse: (value: boolean) => void, +selected: boolean, }; class Category extends React.PureComponent { render(): React.Node { const beforeCheckStyles = { borderColor: 'white' }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: 'white' }; afterCheck = (
); } const icon = this.props.collapsed ? faChevronUp : faChevronDown; const details = this.props.numThreads === 1 ? '1 chat' : `${this.props.numThreads} chats`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle(event.currentTarget.checked); }; onCollapse = (event: SyntheticEvent) => { event.preventDefault(); this.props.onCollapse(!this.props.collapsed); }; } type ConnectedFilterPanelProps = { +toggleFilters: (event: SyntheticEvent) => void, }; const ConnectedFilterPanel: React.ComponentType = React.memo(function ConnectedFilterPanel( props: ConnectedFilterPanelProps, ): React.Node { const filteredThreadIDs = useSelector(filteredThreadIDsSelector); const filteredCommunityThreadIDs = useSelector( filterThreadIDsBelongingToCommunitySelector, ); const filterThreadInfos = useFilterThreadInfos(); const filterThreadSearchIndex = useFilterThreadSearchIndex(); const includeDeleted = useSelector(includeDeletedSelector); const dispatch = useDispatch(); const modalContext = useModalContext(); return ( ); }); export default ConnectedFilterPanel; diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js index b7283a5ab..d9c4b817c 100644 --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -1,688 +1,691 @@ // @flow import invariant from 'invariant'; import _difference from 'lodash/fp/difference.js'; import * as React from 'react'; import { joinThreadActionTypes, useJoinThread, newThreadActionTypes, } from 'lib/actions/thread-actions.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useChatMentionContext, useThreadChatMentionCandidates, } from 'lib/hooks/chat-mention-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userStoreMentionSearchIndex } from 'lib/selectors/user-selectors.js'; import { getMentionTypeaheadUserSuggestions, getTypeaheadRegexMatches, getUserMentionsCandidates, getMentionTypeaheadChatSuggestions, type MentionTypeaheadSuggestionItem, type TypeaheadMatchedStrings, } from 'lib/shared/mention-utils.js'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils.js'; import { threadHasPermission, viewerIsMember, threadFrozenDueToViewerBlock, threadActualMembers, checkIfDefaultMembersAreVoiced, } from 'lib/shared/thread-utils.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import type { MinimallyEncodedThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { type ThreadInfo, type ClientThreadJoinRequest, type ThreadJoinPayload, } from 'lib/types/thread-types.js'; import { type UserInfos } from 'lib/types/user-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import css from './chat-input-bar.css'; import TypeaheadTooltip from './typeahead-tooltip.react.js'; import Button from '../components/button.react.js'; import { type InputState, type PendingMultimediaUpload, } from '../input/input-state.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { allowedMimeTypeString } from '../media/file-utils.js'; import Multimedia from '../media/multimedia.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors.js'; import { webMentionTypeaheadRegex, getMentionTypeaheadTooltipActions, getMentionTypeaheadTooltipButtons, } from '../utils/typeahead-utils.js'; type BaseProps = { +threadInfo: ThreadInfo | MinimallyEncodedThreadInfo, +inputState: InputState, }; type Props = { ...BaseProps, +viewerID: ?string, +joinThreadLoadingStatus: LoadingStatus, +threadCreationInProgress: boolean, +calendarQuery: () => CalendarQuery, +nextLocalID: number, +isThreadActive: boolean, +userInfos: UserInfos, +dispatchActionPromise: DispatchActionPromise, +joinThread: (request: ClientThreadJoinRequest) => Promise, +typeaheadMatchedStrings: ?TypeaheadMatchedStrings, +suggestions: $ReadOnlyArray, +parentThreadInfo: ?ThreadInfo, }; class ChatInputBar extends React.PureComponent { textarea: ?HTMLTextAreaElement; multimediaInput: ?HTMLInputElement; componentDidMount() { this.updateHeight(); if (this.props.isThreadActive) { this.addReplyListener(); } } componentWillUnmount() { if (this.props.isThreadActive) { this.removeReplyListener(); } } componentDidUpdate(prevProps: Props) { if (this.props.isThreadActive && !prevProps.isThreadActive) { this.addReplyListener(); } else if (!this.props.isThreadActive && prevProps.isThreadActive) { this.removeReplyListener(); } const { inputState } = this.props; const prevInputState = prevProps.inputState; if (inputState.draft !== prevInputState.draft) { this.updateHeight(); } if ( inputState.draft !== prevInputState.draft || inputState.textCursorPosition !== prevInputState.textCursorPosition ) { inputState.setTypeaheadState({ canBeVisible: true, }); } const curUploadIDs = ChatInputBar.unassignedUploadIDs( inputState.pendingUploads, ); const prevUploadIDs = ChatInputBar.unassignedUploadIDs( prevInputState.pendingUploads, ); if ( this.multimediaInput && _difference(prevUploadIDs)(curUploadIDs).length > 0 ) { // Whenever a pending upload is removed, we reset the file // HTMLInputElement's value field, so that if the same upload occurs again // the onChange call doesn't get filtered this.multimediaInput.value = ''; } else if ( this.textarea && _difference(curUploadIDs)(prevUploadIDs).length > 0 ) { // Whenever a pending upload is added, we focus the textarea this.textarea.focus(); return; } if ( (this.props.threadInfo.id !== prevProps.threadInfo.id || (inputState.textCursorPosition !== prevInputState.textCursorPosition && this.textarea?.selectionStart === this.textarea?.selectionEnd)) && this.textarea ) { this.textarea.focus(); this.textarea?.setSelectionRange( inputState.textCursorPosition, inputState.textCursorPosition, 'none', ); } } static unassignedUploadIDs( pendingUploads: $ReadOnlyArray, ): Array { return pendingUploads .filter( (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID, ) .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID); } updateHeight() { const textarea = this.textarea; if (textarea) { textarea.style.height = 'auto'; const newHeight = Math.min(textarea.scrollHeight, 150); textarea.style.height = `${newHeight}px`; } } addReplyListener() { invariant( this.props.inputState, 'inputState should be set in addReplyListener', ); this.props.inputState.addReplyListener(this.focusAndUpdateText); } removeReplyListener() { invariant( this.props.inputState, 'inputState should be set in removeReplyListener', ); this.props.inputState.removeReplyListener(this.focusAndUpdateText); } render(): React.Node { const isMember = viewerIsMember(this.props.threadInfo); const canJoin = threadHasPermission( this.props.threadInfo, threadPermissions.JOIN_THREAD, ); let joinButton = null; if (!isMember && canJoin && !this.props.threadCreationInProgress) { let buttonContent; if (this.props.joinThreadLoadingStatus === 'loading') { buttonContent = ( ); } else { buttonContent = ( <>

Join Chat

); } joinButton = (
); } const { pendingUploads, cancelPendingUpload } = this.props.inputState; const multimediaPreviews = pendingUploads.map(pendingUpload => { const { uri, mediaType, thumbHash, dimensions } = pendingUpload; let mediaSource = { thumbHash, dimensions }; if (mediaType !== 'encrypted_photo' && mediaType !== 'encrypted_video') { mediaSource = { ...mediaSource, type: mediaType, uri, thumbnailURI: null, }; } else { const { encryptionKey } = pendingUpload; invariant( encryptionKey, 'encryptionKey should be set for encrypted media', ); mediaSource = { ...mediaSource, type: mediaType, blobURI: uri, encryptionKey, thumbnailBlobURI: null, thumbnailEncryptionKey: null, }; } return ( ); }); const previews = multimediaPreviews.length > 0 ? (
{multimediaPreviews}
) : null; let content; // If the thread is created by somebody else while the viewer is attempting // to create it, the threadInfo might be modified in-place and won't // list the viewer as a member, which will end up hiding the input. In // this case, we will assume that our creation action will get translated, // into a join and as long as members are voiced, we can show the input. const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced( this.props.threadInfo, ); let sendButton; if (this.props.inputState.draft.length) { sendButton = ( ); } if ( threadHasPermission(this.props.threadInfo, threadPermissions.VOICED) || (this.props.threadCreationInProgress && defaultMembersAreVoiced) ) { content = (