diff --git a/web/chat/chat-input-bar.css b/web/chat/chat-input-bar.css index 4454e729c..4313ea27e 100644 --- a/web/chat/chat-input-bar.css +++ b/web/chat/chat-input-bar.css @@ -1,113 +1,105 @@ div.inputBar { display: flex; flex-direction: column; border-top: 1px solid var(--border-color); } div.inputBarWrapper { padding: 16px; display: flex; flex-direction: row; align-items: center; } div.inputBarTextInput { display: flex; background: var(--text-input-bg); border-radius: 4px; padding: 8px; align-items: center; flex-grow: 1; } div.inputBarTextInput > textarea { flex: 1; display: flex; border: none; resize: none; background: var(--text-input-bg); color: var(--fg); } div.inputBarTextInput > textarea:focus { outline: none; } div.joinButtonContainer { background: var(--join-bg); padding-top: 8px; padding-bottom: 8px; display: flex; flex-direction: row; width: 100%; justify-content: center; } button.joinButton { display: flex; align-items: center; border: none; padding: 12px 24px; border-radius: 4px; color: var(--fg); font-size: var(--m-font-16); font-weight: var(--semi-bold); cursor: pointer; } button.joinButton { display: flex; border-radius: 4px; justify-content: center; cursor: pointer; } p.joinButtonText { font-size: var(--m-font-16); font-weight: var(--semi-bold); color: var(--fg); padding-left: 8px; } span.explanation { color: var(--permission-color); text-align: center; padding-top: 20px; padding-bottom: 8px; } a.multimediaUpload { cursor: pointer; position: relative; padding-right: 12px; } a.multimediaUpload > input[type='file'] { visibility: hidden; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: 0; padding: 0; } - -a.multimediaUpload > svg { - color: var(--color-disabled); -} a.multimediaUpload:hover > svg { - color: var(--fg); + opacity: 0.8; } div.previews { display: flex; overflow-x: auto; white-space: nowrap; } div.previews > span.multimedia { margin: 10px; } div.previews > span.multimedia > span.multimediaImage > img { max-height: 200px; max-width: 200px; } a.sendButton { padding: 8px 0 0 10px; cursor: pointer; } - -a.sendButton svg { - color: var(--fg); -} diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js index 88f821903..8d6a2aaf4 100644 --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -1,473 +1,482 @@ // @flow import invariant from 'invariant'; import _difference from 'lodash/fp/difference'; import * as React from 'react'; import { joinThreadActionTypes, joinThread, newThreadActionTypes, } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils'; import { threadHasPermission, viewerIsMember, threadFrozenDueToViewerBlock, threadActualMembers, checkIfDefaultMembersAreVoiced, } from 'lib/shared/thread-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import { messageTypes } from 'lib/types/message-types'; import { type ThreadInfo, threadPermissions, type ClientThreadJoinRequest, type ThreadJoinPayload, } from 'lib/types/thread-types'; import { type UserInfos } from 'lib/types/user-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { type InputState, type PendingMultimediaUpload, } from '../input/input-state'; import LoadingIndicator from '../loading-indicator.react'; import { allowedMimeTypeString } from '../media/file-utils'; import Multimedia from '../media/multimedia.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-input-bar.css'; type BaseProps = { +threadInfo: ThreadInfo, +inputState: InputState, }; type Props = { ...BaseProps, // Redux state +viewerID: ?string, +joinThreadLoadingStatus: LoadingStatus, +threadCreationInProgress: boolean, +calendarQuery: () => CalendarQuery, +nextLocalID: number, +isThreadActive: boolean, +userInfos: UserInfos, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +joinThread: (request: ClientThreadJoinRequest) => Promise, }; 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(); } 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 && this.textarea) { this.textarea.focus(); } } static unassignedUploadIDs( pendingUploads: $ReadOnlyArray, ) { 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() { 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 Thread

); } joinButton = (
); } const { pendingUploads, cancelPendingUpload } = this.props.inputState; const multimediaPreviews = pendingUploads.map(pendingUpload => ( )); 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 = (
- +