Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509117
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
27 KB
Referenced Files
None
Subscribers
None
View Options
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't send messages to a user that you've blocked.
</span>
);
} else if (isMember) {
content = (
<span className={css.explanation}>
You don't have permission to send messages.
</span>
);
} else if (defaultMembersAreVoiced && canJoin) {
content = null;
} else {
content = (
<span className={css.explanation}>
You don'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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment