Page MenuHomePhabricator

No OneTemporary

diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js
index d71cef502..0a3d1b981 100644
--- a/web/chat/chat-input-bar.react.js
+++ b/web/chat/chat-input-bar.react.js
@@ -1,577 +1,578 @@
// @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 {
userStoreSearchIndex,
relativeMemberInfoSelectorForMembersOfThread,
} from 'lib/selectors/user-selectors';
import { localIDPrefix, trimMessage } from 'lib/shared/message-utils';
import SearchIndex from 'lib/shared/search-index';
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 { RelativeMemberInfo } from 'lib/types/thread-types';
import { type UserInfos } from 'lib/types/user-types';
import {
type DispatchActionPromise,
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils';
import Button from '../components/button.react';
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';
import MentionSuggestionTooltip from './mention-suggestion-tooltip.react';
import { mentionRegex } from './mention-utils';
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<ThreadJoinPayload>,
+userSearchIndex: SearchIndex,
+threadMembers: $ReadOnlyArray<RelativeMemberInfo>,
+typeaheadMatchedStrings: ?TypeaheadMatchedStrings,
};
export type TypeaheadMatchedStrings = {
+entireText: string,
+textBeforeAtSymbol: string,
+usernamePrefix: string,
};
class ChatInputBar extends React.PureComponent<Props> {
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 ||
(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<PendingMultimediaUpload>,
) {
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 = (
<LoadingIndicator
status={this.props.joinThreadLoadingStatus}
size="medium"
color="white"
/>
);
} else {
buttonContent = (
<>
<SWMansionIcon icon="plus" size={24} />
<p className={css.joinButtonText}>Join Chat</p>
</>
);
}
joinButton = (
<div className={css.joinButtonContainer}>
<Button
variant="filled"
buttonColor={{ backgroundColor: `#${this.props.threadInfo.color}` }}
onClick={this.onClickJoin}
>
{buttonContent}
</Button>
</div>
);
}
const { pendingUploads, cancelPendingUpload } = this.props.inputState;
const multimediaPreviews = pendingUploads.map(pendingUpload => (
<Multimedia
uri={pendingUpload.uri}
+ type={pendingUpload.mediaType}
pendingUpload={pendingUpload}
remove={cancelPendingUpload}
multimediaCSSClass={css.multimedia}
multimediaImageCSSClass={css.multimediaImage}
key={pendingUpload.localID}
/>
));
const previews =
multimediaPreviews.length > 0 ? (
<div className={css.previews}>{multimediaPreviews}</div>
) : 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 = (
<a onClick={this.onSend} className={css.sendButton}>
<SWMansionIcon
icon="send-2"
size={22}
color={`#${this.props.threadInfo.color}`}
/>
</a>
);
}
if (
threadHasPermission(this.props.threadInfo, threadPermissions.VOICED) ||
(this.props.threadCreationInProgress && defaultMembersAreVoiced)
) {
content = (
<div className={css.inputBarWrapper}>
<a className={css.multimediaUpload} onClick={this.onMultimediaClick}>
<input
type="file"
onChange={this.onMultimediaFileChange}
ref={this.multimediaInputRef}
accept={allowedMimeTypeString}
multiple
/>
<SWMansionIcon
icon="image-1"
size={22}
color={`#${this.props.threadInfo.color}`}
disableFill
/>
</a>
<div className={css.inputBarTextInput}>
<textarea
rows="1"
placeholder="Type your message"
value={this.props.inputState.draft}
onChange={this.onChangeMessageText}
onKeyDown={this.onKeyDown}
onClick={this.onClickTextarea}
onSelect={this.onSelectTextarea}
ref={this.textareaRef}
autoFocus
/>
</div>
{sendButton}
</div>
);
} else if (
threadFrozenDueToViewerBlock(
this.props.threadInfo,
this.props.viewerID,
this.props.userInfos,
) &&
threadActualMembers(this.props.threadInfo.members).length === 2
) {
content = (
<span className={css.explanation}>
You can&apos;t send messages to a user that you&apos;ve blocked.
</span>
);
} else if (isMember) {
content = (
<span className={css.explanation}>
You don&apos;t have permission to send messages.
</span>
);
} else if (defaultMembersAreVoiced && canJoin) {
content = null;
} else {
content = (
<span className={css.explanation}>
You don&apos;t have permission to send messages.
</span>
);
}
let typeaheadTooltip;
if (this.props.typeaheadMatchedStrings && this.textarea) {
typeaheadTooltip = (
<MentionSuggestionTooltip
inputState={this.props.inputState}
textarea={this.textarea}
userSearchIndex={this.props.userSearchIndex}
threadMembers={this.props.threadMembers}
viewerID={this.props.viewerID}
matchedStrings={this.props.typeaheadMatchedStrings}
/>
);
}
return (
<div className={css.inputBar}>
{joinButton}
{previews}
{content}
{typeaheadTooltip}
</div>
);
}
textareaRef = (textarea: ?HTMLTextAreaElement) => {
this.textarea = textarea;
if (textarea) {
textarea.focus();
}
};
onChangeMessageText = (event: SyntheticEvent<HTMLTextAreaElement>) => {
this.props.inputState.setDraft(event.currentTarget.value);
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
onClickTextarea = (event: SyntheticEvent<HTMLTextAreaElement>) => {
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
onSelectTextarea = (event: SyntheticEvent<HTMLTextAreaElement>) => {
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
focusAndUpdateText = (text: string) => {
// We need to call focus() first on Safari, otherwise the cursor
// ends up at the start instead of the end for some reason
const { textarea } = this;
invariant(textarea, 'textarea should be set');
textarea.focus();
// We reset the textarea to an empty string at the start so that the cursor
// always ends up at the end, even if the text doesn't actually change
textarea.value = '';
const currentText = this.props.inputState.draft;
if (!currentText.startsWith(text)) {
const prependedText = text.concat(currentText);
this.props.inputState.setDraft(prependedText);
textarea.value = prependedText;
} else {
textarea.value = currentText;
}
// The above strategies make sure the cursor is at the end,
// but we also need to make sure that we're scrolled to the bottom
textarea.scrollTop = textarea.scrollHeight;
};
onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
this.send();
}
};
onSend = (event: SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.send();
};
send() {
let { nextLocalID } = this.props;
const text = trimMessage(this.props.inputState.draft);
if (text) {
this.dispatchTextMessageAction(text, nextLocalID);
nextLocalID++;
}
if (this.props.inputState.pendingUploads.length > 0) {
this.props.inputState.createMultimediaMessage(
nextLocalID,
this.props.threadInfo,
);
}
}
dispatchTextMessageAction(text: string, nextLocalID: number) {
this.props.inputState.setDraft('');
const localID = `${localIDPrefix}${nextLocalID}`;
const creatorID = this.props.viewerID;
invariant(creatorID, 'should have viewer ID in order to send a message');
this.props.inputState.sendTextMessage(
{
type: messageTypes.TEXT,
localID,
threadID: this.props.threadInfo.id,
text,
creatorID,
time: Date.now(),
},
this.props.threadInfo,
);
}
multimediaInputRef = (multimediaInput: ?HTMLInputElement) => {
this.multimediaInput = multimediaInput;
};
onMultimediaClick = () => {
if (this.multimediaInput) {
this.multimediaInput.click();
}
};
onMultimediaFileChange = async (
event: SyntheticInputEvent<HTMLInputElement>,
) => {
const result = await this.props.inputState.appendFiles([
...event.target.files,
]);
if (!result && this.multimediaInput) {
this.multimediaInput.value = '';
}
};
onClickJoin = () => {
this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
};
async joinAction() {
const query = this.props.calendarQuery();
return await this.props.joinThread({
threadID: this.props.threadInfo.id,
calendarQuery: {
startDate: query.startDate,
endDate: query.endDate,
filters: [
...query.filters,
{ type: 'threads', threadIDs: [this.props.threadInfo.id] },
],
},
});
}
}
const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
joinThreadActionTypes,
);
const createThreadLoadingStatusSelector = createLoadingStatusSelector(
newThreadActionTypes,
);
const ConnectedChatInputBar: React.ComponentType<BaseProps> = React.memo<BaseProps>(
function ConnectedChatInputBar(props) {
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const nextLocalID = useSelector(state => state.nextLocalID);
const isThreadActive = useSelector(
state => props.threadInfo.id === state.navInfo.activeChatThreadID,
);
const userInfos = useSelector(state => state.userStore.userInfos);
const joinThreadLoadingStatus = useSelector(
joinThreadLoadingStatusSelector,
);
const createThreadLoadingStatus = useSelector(
createThreadLoadingStatusSelector,
);
const threadCreationInProgress = createThreadLoadingStatus === 'loading';
const calendarQuery = useSelector(nonThreadCalendarQuery);
const dispatchActionPromise = useDispatchActionPromise();
const callJoinThread = useServerCall(joinThread);
const userSearchIndex = useSelector(userStoreSearchIndex);
const threadMembers = useSelector(
relativeMemberInfoSelectorForMembersOfThread(props.threadInfo.id),
);
const inputSliceEndingAtCursor = React.useMemo(
() =>
props.inputState.draft.slice(0, props.inputState.textCursorPosition),
[props.inputState.draft, props.inputState.textCursorPosition],
);
// we only try to match if there is end of text or whitespace after cursor
const typeaheadRegexMatches = React.useMemo(
() =>
inputSliceEndingAtCursor.length === props.inputState.draft.length ||
/\s/.test(props.inputState.draft[props.inputState.textCursorPosition])
? inputSliceEndingAtCursor.match(mentionRegex)
: null,
[
inputSliceEndingAtCursor,
props.inputState.textCursorPosition,
props.inputState.draft,
],
);
const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = React.useMemo(
() =>
typeaheadRegexMatches !== null
? {
entireText: typeaheadRegexMatches[0],
textBeforeAtSymbol: typeaheadRegexMatches[1],
usernamePrefix: typeaheadRegexMatches[2],
}
: null,
[typeaheadRegexMatches],
);
return (
<ChatInputBar
{...props}
viewerID={viewerID}
joinThreadLoadingStatus={joinThreadLoadingStatus}
threadCreationInProgress={threadCreationInProgress}
calendarQuery={calendarQuery}
nextLocalID={nextLocalID}
isThreadActive={isThreadActive}
userInfos={userInfos}
dispatchActionPromise={dispatchActionPromise}
joinThread={callJoinThread}
userSearchIndex={userSearchIndex}
threadMembers={threadMembers}
typeaheadMatchedStrings={typeaheadMatchedStrings}
/>
);
},
);
export default ConnectedChatInputBar;
diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js
index 282149510..d4b42952e 100644
--- a/web/chat/multimedia-message.react.js
+++ b/web/chat/multimedia-message.react.js
@@ -1,81 +1,82 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import { messageTypes } from 'lib/types/message-types';
import { type ThreadInfo } from 'lib/types/thread-types';
import { type InputState, InputStateContext } from '../input/input-state';
import Multimedia from '../media/multimedia.react';
import css from './chat-message-list.css';
import ComposedMessage from './composed-message.react';
import sendFailed from './multimedia-message-send-failed';
type BaseProps = {
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
type Props = {
...BaseProps,
// withInputState
+inputState: ?InputState,
};
class MultimediaMessage extends React.PureComponent<Props> {
render() {
const { item, inputState } = this.props;
invariant(
item.messageInfo.type === messageTypes.IMAGES ||
item.messageInfo.type === messageTypes.MULTIMEDIA,
'MultimediaMessage should only be used for multimedia messages',
);
const { localID, media } = item.messageInfo;
invariant(inputState, 'inputState should be set in MultimediaMessage');
const pendingUploads = localID ? inputState.assignedUploads[localID] : null;
const multimedia = [];
for (const singleMedia of media) {
const pendingUpload = pendingUploads
? pendingUploads.find(upload => upload.localID === singleMedia.id)
: null;
multimedia.push(
<Multimedia
uri={singleMedia.uri}
+ type={singleMedia.type}
pendingUpload={pendingUpload}
multimediaCSSClass={css.multimedia}
multimediaImageCSSClass={css.multimediaImage}
key={singleMedia.id}
/>,
);
}
invariant(multimedia.length > 0, 'should be at least one multimedia...');
const content =
multimedia.length > 1 ? (
<div className={css.imageGrid}>{multimedia}</div>
) : (
multimedia
);
return (
<ComposedMessage
item={item}
threadInfo={this.props.threadInfo}
sendFailed={sendFailed(item, inputState)}
fixedWidth={multimedia.length > 1}
borderRadius={16}
>
{content}
</ComposedMessage>
);
}
}
const ConnectedMultimediaMessage: React.ComponentType<BaseProps> = React.memo<BaseProps>(
function ConnectedMultimediaMessage(props) {
const inputState = React.useContext(InputStateContext);
return <MultimediaMessage {...props} inputState={inputState} />;
},
);
export default ConnectedMultimediaMessage;
diff --git a/web/media/media.css b/web/media/media.css
index 4ca4ce173..9a8ff940b 100644
--- a/web/media/media.css
+++ b/web/media/media.css
@@ -1,102 +1,104 @@
span.clickable {
cursor: pointer;
}
span.multimedia {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: top;
}
span.multimedia > .multimediaImage {
position: relative;
min-height: 50px;
min-width: 50px;
}
-span.multimedia > .multimediaImage > img {
+span.multimedia > .multimediaImage > img,
+span.multimedia > .multimediaImage > video {
max-height: 200px;
max-width: 100%;
}
span.multimedia > .multimediaImage svg.removeUpload {
display: none;
position: absolute;
top: 3px;
right: 3px;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: rgba(34, 34, 34, 0.67);
}
span.multimedia:hover > .multimediaImage svg.removeUpload {
display: inherit;
}
span.multimedia > svg.uploadError {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: #dd2222;
}
span.multimedia > svg.progressIndicator {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
width: 50px;
height: 50px;
}
:global(.CircularProgressbar-background) {
fill: #666 !important;
}
:global(.CircularProgressbar-text) {
fill: #fff !important;
}
:global(.CircularProgressbar-path) {
stroke: #fff !important;
}
:global(.CircularProgressbar-trail) {
stroke: transparent !important;
}
div.multimediaModalOverlay {
position: fixed;
left: 0;
top: 0;
z-index: 4;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
overflow: auto;
padding: 10px;
box-sizing: border-box;
display: flex;
justify-content: center;
}
-div.multimediaModalOverlay > img {
+div.multimediaModalOverlay > img,
+div.multimediaModalOverlay > video {
object-fit: scale-down;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
}
svg.closeMultimediaModal {
position: absolute;
cursor: pointer;
top: 15px;
right: 15px;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: rgba(34, 34, 34, 0.67);
height: 36px;
width: 36px;
}
diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js
index 4f0fbe4f8..14979e0c2 100644
--- a/web/media/multimedia.react.js
+++ b/web/media/multimedia.react.js
@@ -1,127 +1,151 @@
// @flow
import classNames from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
import { CircularProgressbar } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
import {
XCircle as XCircleIcon,
AlertCircle as AlertCircleIcon,
} from 'react-feather';
import { useModalContext } from 'lib/components/modal-provider.react';
+import type { MediaType } from 'lib/types/media-types';
import Button from '../components/button.react';
import { type PendingMultimediaUpload } from '../input/input-state';
import css from './media.css';
import MultimediaModal from './multimedia-modal.react';
type BaseProps = {
+uri: string,
+ +type: MediaType,
+pendingUpload?: ?PendingMultimediaUpload,
+remove?: (uploadID: string) => void,
+multimediaCSSClass: string,
+multimediaImageCSSClass: string,
};
type Props = {
...BaseProps,
+pushModal: (modal: React.Node) => void,
};
class Multimedia extends React.PureComponent<Props> {
componentDidUpdate(prevProps: Props) {
const { uri, pendingUpload } = this.props;
if (uri === prevProps.uri) {
return;
}
if (
(!pendingUpload || pendingUpload.uriIsReal) &&
(!prevProps.pendingUpload || !prevProps.pendingUpload.uriIsReal)
) {
URL.revokeObjectURL(prevProps.uri);
}
}
render(): React.Node {
let progressIndicator, errorIndicator, removeButton;
- const { pendingUpload, remove } = this.props;
+ const {
+ pendingUpload,
+ remove,
+ type,
+ uri,
+ multimediaImageCSSClass,
+ multimediaCSSClass,
+ } = this.props;
if (pendingUpload) {
const { progressPercent, failed } = pendingUpload;
if (progressPercent !== 0 && progressPercent !== 1) {
const outOfHundred = Math.floor(progressPercent * 100);
const text = `${outOfHundred}%`;
progressIndicator = (
<CircularProgressbar
value={outOfHundred}
text={text}
background
backgroundPadding={6}
className={css.progressIndicator}
/>
);
}
if (failed) {
errorIndicator = (
<AlertCircleIcon className={css.uploadError} size={36} />
);
}
if (remove) {
removeButton = (
<Button onClick={this.remove}>
<XCircleIcon className={css.removeUpload} />
</Button>
);
}
}
const imageContainerClasses = [
css.multimediaImage,
- this.props.multimediaImageCSSClass,
+ multimediaImageCSSClass,
];
imageContainerClasses.push(css.clickable);
- const containerClasses = [css.multimedia, this.props.multimediaCSSClass];
- return (
- <span className={classNames(containerClasses)}>
+ let mediaNode;
+ if (type === 'photo') {
+ mediaNode = (
<Button
className={classNames(imageContainerClasses)}
onClick={this.onClick}
>
- <img src={this.props.uri} />
+ <img src={uri} />
{removeButton}
</Button>
+ );
+ } else {
+ mediaNode = (
+ <div className={classNames(imageContainerClasses)}>
+ <video controls>
+ <source src={uri} />
+ </video>
+ </div>
+ );
+ }
+
+ const containerClasses = [css.multimedia, multimediaCSSClass];
+ return (
+ <span className={classNames(containerClasses)}>
+ {mediaNode}
{progressIndicator}
{errorIndicator}
</span>
);
}
remove: (event: SyntheticEvent<HTMLElement>) => void = event => {
event.stopPropagation();
const { remove, pendingUpload } = this.props;
invariant(
remove && pendingUpload,
'Multimedia cannot be removed as either remove or pendingUpload ' +
'are unspecified',
);
remove(pendingUpload.localID);
};
onClick: () => void = () => {
const { pushModal, uri } = this.props;
pushModal(<MultimediaModal uri={uri} />);
};
}
function ConnectedMultimediaContainer(props: BaseProps): React.Node {
const modalContext = useModalContext();
return <Multimedia {...props} pushModal={modalContext.pushModal} />;
}
export default ConnectedMultimediaContainer;

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:14 AM (3 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2683173
Default Alt Text
(27 KB)

Event Timeline