move some actions to redux in hooks
This commit is contained in:
parent
0e4d7ec21a
commit
23e9a6d31c
|
@ -22,41 +22,28 @@ import {
|
|||
isImageAttachment,
|
||||
isVideo,
|
||||
} from '../../../ts/types/Attachment';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
|
||||
import { getIncrement } from '../../util/timer';
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
import _ from 'lodash';
|
||||
import { animation, contextMenu, Item, Menu } from 'react-contexify';
|
||||
import { contextMenu, Menu } from 'react-contexify';
|
||||
import uuid from 'uuid';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { MessageMetadata } from './message/MessageMetadata';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { MessageRegularProps } from '../../models/messageType';
|
||||
import {
|
||||
addSenderAsModerator,
|
||||
removeSenderFromModerator,
|
||||
} from '../../interactions/messageInteractions';
|
||||
import { updateUserDetailsModal } from '../../state/ducks/modalDialog';
|
||||
import { MessageInteraction } from '../../interactions';
|
||||
import autoBind from 'auto-bind';
|
||||
import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer';
|
||||
import { ClickToTrustSender } from './message/ClickToTrustSender';
|
||||
import { getMessageById } from '../../data/data';
|
||||
import { deleteMessagesById, replyToMessage } from '../../interactions/conversationInteractions';
|
||||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../state/reducer';
|
||||
import { getSelectedMessageIds } from '../../state/selectors/conversations';
|
||||
import {
|
||||
PropsForAttachment,
|
||||
PropsForMessage,
|
||||
showLightBox,
|
||||
showMessageDetailsView,
|
||||
toggleSelectedMessageId,
|
||||
} from '../../state/ducks/conversations';
|
||||
import { showLightBox, toggleSelectedMessageId } from '../../state/ducks/conversations';
|
||||
import { saveAttachmentToDisk } from '../../util/attachmentsUtil';
|
||||
import { LightBoxOptions } from '../session/conversation/SessionConversation';
|
||||
import { pushUnblockToSend } from '../../session/utils/Toast';
|
||||
import { MessageContextMenu } from './MessageContextMenu';
|
||||
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
|
@ -194,7 +181,6 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
conversationType,
|
||||
direction,
|
||||
quote,
|
||||
multiSelectMode,
|
||||
isTrustedForAttachmentDownload,
|
||||
} = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
|
@ -234,16 +220,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
withContentBelow={withContentBelow}
|
||||
bottomOverlay={!collapseMetadata}
|
||||
onError={this.handleImageError}
|
||||
onClickAttachment={(attachment: AttachmentTypeWithPath) => {
|
||||
if (multiSelectMode) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
} else {
|
||||
void onClickAttachment({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onClickAttachment={this.onClickOnImageGrid}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -286,17 +263,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
<div
|
||||
role="button"
|
||||
className="module-message__generic-attachment__icon"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const messageTimestamp = this.props.timestamp || this.props.serverTimestamp || 0;
|
||||
void saveAttachmentToDisk({
|
||||
attachment: firstAttachment,
|
||||
messageTimestamp,
|
||||
messageSender: this.props.authorPhoneNumber,
|
||||
conversationId: this.props.convoId,
|
||||
});
|
||||
}}
|
||||
onClick={this.onClickOnGenericAttachment}
|
||||
>
|
||||
{extension ? (
|
||||
<div className="module-message__generic-attachment__icon__extension">
|
||||
|
@ -414,22 +381,11 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public renderQuote() {
|
||||
const {
|
||||
conversationType,
|
||||
direction,
|
||||
quote,
|
||||
isPublic,
|
||||
convoId,
|
||||
id,
|
||||
multiSelectMode,
|
||||
} = this.props;
|
||||
const { conversationType, direction, quote, isPublic, convoId } = this.props;
|
||||
|
||||
if (!quote || !quote.authorPhoneNumber || !quote.messageId) {
|
||||
return null;
|
||||
}
|
||||
const quoteId = _.toNumber(quote.messageId);
|
||||
const { authorPhoneNumber, referencedMessageNotFound } = quote;
|
||||
|
||||
const withContentAbove = conversationType === 'group' && direction === 'incoming';
|
||||
|
||||
const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber);
|
||||
|
@ -438,20 +394,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
|
||||
return (
|
||||
<Quote
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (multiSelectMode && id) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
|
||||
return;
|
||||
}
|
||||
void this.props.onQuoteClick?.({
|
||||
quoteAuthor: authorPhoneNumber,
|
||||
quoteId,
|
||||
referencedMessageNotFound,
|
||||
});
|
||||
}}
|
||||
onClick={this.onQuoteClick}
|
||||
text={quote.text}
|
||||
attachment={quote.attachment}
|
||||
isIncoming={direction === 'incoming'}
|
||||
|
@ -497,15 +440,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
avatarPath={authorAvatarPath}
|
||||
name={userName}
|
||||
size={AvatarSize.S}
|
||||
onAvatarClick={() => {
|
||||
window.inboxStore?.dispatch(
|
||||
updateUserDetailsModal({
|
||||
conversationId: authorPhoneNumber,
|
||||
userName,
|
||||
authorAvatarPath,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onAvatarClick={this.onMessageAvatarClick}
|
||||
pubkey={authorPhoneNumber}
|
||||
/>
|
||||
{isPublic && isAdmin && (
|
||||
|
@ -562,142 +497,6 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderContextMenu() {
|
||||
const {
|
||||
attachments,
|
||||
authorPhoneNumber,
|
||||
convoId,
|
||||
direction,
|
||||
status,
|
||||
isDeletable,
|
||||
id,
|
||||
isPublic,
|
||||
isOpenGroupV2,
|
||||
weAreAdmin,
|
||||
isAdmin,
|
||||
text,
|
||||
} = this.props;
|
||||
|
||||
const showRetry = status === 'error' && direction === 'outgoing';
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
|
||||
const onContextMenuShown = () => {
|
||||
window.contextMenuShown = true;
|
||||
};
|
||||
|
||||
const onContextMenuHidden = () => {
|
||||
// This function will called before the click event
|
||||
// on the message would trigger (and I was unable to
|
||||
// prevent propagation in this case), so use a short timeout
|
||||
setTimeout(() => {
|
||||
window.contextMenuShown = false;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const onShowDetail = async () => {
|
||||
const found = await getMessageById(this.props.id);
|
||||
if (found) {
|
||||
const messageDetailsProps = await found.getPropsForMessageDetail();
|
||||
window.inboxStore?.dispatch(showMessageDetailsView(messageDetailsProps));
|
||||
} else {
|
||||
window.log.warn(`Message ${this.props.id} not found in db`);
|
||||
}
|
||||
};
|
||||
|
||||
const selectMessageText = window.i18n('selectMessage');
|
||||
const deleteMessageText = window.i18n('deleteMessage');
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id={this.ctxMenuID}
|
||||
onShown={onContextMenuShown}
|
||||
onHidden={onContextMenuHidden}
|
||||
animation={animation.fade}
|
||||
>
|
||||
{!multipleAttachments && attachments && attachments[0] ? (
|
||||
<Item
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
const messageTimestamp = this.props.timestamp || this.props.serverTimestamp || 0;
|
||||
void saveAttachmentToDisk({
|
||||
attachment: attachments[0],
|
||||
messageTimestamp,
|
||||
messageSender: this.props.authorPhoneNumber,
|
||||
conversationId: this.props.convoId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{window.i18n('downloadAttachment')}
|
||||
</Item>
|
||||
) : null}
|
||||
|
||||
<Item
|
||||
onClick={() => {
|
||||
MessageInteraction.copyBodyToClipboard(text);
|
||||
}}
|
||||
>
|
||||
{window.i18n('copyMessage')}
|
||||
</Item>
|
||||
<Item onClick={this.onReplyPrivate}>{window.i18n('replyToMessage')}</Item>
|
||||
<Item onClick={onShowDetail}>{window.i18n('moreInformation')}</Item>
|
||||
{showRetry ? (
|
||||
<Item
|
||||
onClick={async () => {
|
||||
const found = await getMessageById(id);
|
||||
if (found) {
|
||||
await found.retrySend();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{window.i18n('resend')}
|
||||
</Item>
|
||||
) : null}
|
||||
{isDeletable ? (
|
||||
<>
|
||||
<Item
|
||||
onClick={() => {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
}}
|
||||
>
|
||||
{selectMessageText}
|
||||
</Item>
|
||||
<Item
|
||||
onClick={() => {
|
||||
void deleteMessagesById([id], convoId, false);
|
||||
}}
|
||||
>
|
||||
{deleteMessageText}
|
||||
</Item>
|
||||
</>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic ? (
|
||||
<Item
|
||||
onClick={() => {
|
||||
MessageInteraction.banUser(authorPhoneNumber, convoId);
|
||||
}}
|
||||
>
|
||||
{window.i18n('banUser')}
|
||||
</Item>
|
||||
) : null}
|
||||
{weAreAdmin && isOpenGroupV2 ? (
|
||||
<Item
|
||||
onClick={() => {
|
||||
MessageInteraction.unbanUser(authorPhoneNumber, convoId);
|
||||
}}
|
||||
>
|
||||
{window.i18n('unbanUser')}
|
||||
</Item>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic && !isAdmin ? (
|
||||
<Item onClick={this.onAddModerator}>{window.i18n('addAsModerator')}</Item>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic && isAdmin ? (
|
||||
<Item onClick={this.onRemoveFromModerator}>{window.i18n('removeFromModerators')}</Item>
|
||||
) : null}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
public getWidth(): number | undefined {
|
||||
const { attachments, previews } = this.props;
|
||||
|
||||
|
@ -824,26 +623,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
expiring ? 'module-message--expired' : null
|
||||
)}
|
||||
role="button"
|
||||
onClick={event => {
|
||||
const selection = window.getSelection();
|
||||
// Text is being selected
|
||||
if (selection && selection.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
|
||||
// User clicked on message body
|
||||
const target = event.target as HTMLDivElement;
|
||||
if (
|
||||
(!multiSelectMode && target.className === 'text-selectable') ||
|
||||
window.contextMenuShown
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
}
|
||||
}}
|
||||
onClick={this.onClickOnMessageOuterContainer}
|
||||
>
|
||||
{this.renderError(isIncoming)}
|
||||
|
||||
|
@ -856,19 +636,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
width: isShowingImage ? width : undefined,
|
||||
}}
|
||||
role="button"
|
||||
onClick={event => {
|
||||
const selection = window.getSelection();
|
||||
// Text is being selected
|
||||
if (selection && selection.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
|
||||
// User clicked on message body
|
||||
const target = event.target as HTMLDivElement;
|
||||
if (target.className === 'text-selectable' || window.contextMenuShown) {
|
||||
return;
|
||||
}
|
||||
}}
|
||||
onClick={this.onClickOnMessageInnerContainer}
|
||||
>
|
||||
{this.renderAuthor()}
|
||||
{this.renderQuote()}
|
||||
|
@ -876,19 +644,40 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
{this.renderPreview()}
|
||||
{this.renderText()}
|
||||
<MessageMetadata
|
||||
{..._.omit(
|
||||
this.props,
|
||||
'onDeleteMessage',
|
||||
'onReply',
|
||||
'onClickAttachment',
|
||||
'onDownload',
|
||||
'onQuoteClick'
|
||||
)}
|
||||
direction={this.props.direction}
|
||||
id={this.props.id}
|
||||
timestamp={this.props.timestamp}
|
||||
collapseMetadata={this.props.collapseMetadata}
|
||||
expirationLength={this.props.expirationLength}
|
||||
isAdmin={this.props.isAdmin}
|
||||
serverTimestamp={this.props.serverTimestamp}
|
||||
isPublic={this.props.isPublic}
|
||||
status={this.props.status}
|
||||
expirationTimestamp={this.props.expirationTimestamp}
|
||||
text={this.props.text}
|
||||
isShowingImage={this.isShowingImage()}
|
||||
/>
|
||||
</div>
|
||||
{this.renderError(!isIncoming)}
|
||||
{this.renderContextMenu()}
|
||||
|
||||
<MessageContextMenu
|
||||
authorPhoneNumber={this.props.authorPhoneNumber}
|
||||
convoId={this.props.convoId}
|
||||
contextMenuId={this.ctxMenuID}
|
||||
direction={this.props.direction}
|
||||
isBlocked={this.props.isBlocked}
|
||||
isDeletable={this.props.isDeletable}
|
||||
messageId={this.props.id}
|
||||
text={this.props.text}
|
||||
timestamp={this.props.timestamp}
|
||||
serverTimestamp={this.props.serverTimestamp}
|
||||
attachments={this.props.attachments}
|
||||
isAdmin={this.props.isAdmin}
|
||||
isOpenGroupV2={this.props.isOpenGroupV2}
|
||||
isPublic={this.props.isPublic}
|
||||
status={this.props.status}
|
||||
weAreAdmin={this.props.weAreAdmin}
|
||||
/>
|
||||
</div>
|
||||
</InView>
|
||||
);
|
||||
|
@ -910,6 +699,28 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private onQuoteClick(e: any) {
|
||||
const { quote, multiSelectMode, id } = this.props;
|
||||
if (!quote) {
|
||||
window.log.warn('onQuoteClick: quote not valid');
|
||||
return;
|
||||
}
|
||||
const quoteId = _.toNumber(quote.messageId);
|
||||
const { authorPhoneNumber, referencedMessageNotFound } = quote;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (multiSelectMode && id) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
|
||||
return;
|
||||
}
|
||||
void this.props.onQuoteClick?.({
|
||||
quoteAuthor: authorPhoneNumber,
|
||||
quoteId,
|
||||
referencedMessageNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
private renderAuthor() {
|
||||
const {
|
||||
authorName,
|
||||
|
@ -944,21 +755,83 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private onReplyPrivate(e: any) {
|
||||
if (this.props.isBlocked) {
|
||||
pushUnblockToSend();
|
||||
private onMessageAvatarClick() {
|
||||
const userName =
|
||||
this.props.authorName || this.props.authorProfileName || this.props.authorPhoneNumber;
|
||||
|
||||
window.inboxStore?.dispatch(
|
||||
updateUserDetailsModal({
|
||||
conversationId: this.props.authorPhoneNumber,
|
||||
userName,
|
||||
authorAvatarPath: this.props.authorAvatarPath,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onClickOnImageGrid(attachment: AttachmentTypeWithPath) {
|
||||
const { multiSelectMode, id } = this.props;
|
||||
|
||||
if (multiSelectMode) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
} else {
|
||||
void onClickAttachment({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onClickOnGenericAttachment(e: any) {
|
||||
const { timestamp, serverTimestamp, authorPhoneNumber, attachments, convoId } = this.props;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
if (!attachments?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
void replyToMessage(this.props.id);
|
||||
const firstAttachment = attachments[0];
|
||||
|
||||
const messageTimestamp = timestamp || serverTimestamp || 0;
|
||||
void saveAttachmentToDisk({
|
||||
attachment: firstAttachment,
|
||||
messageTimestamp,
|
||||
messageSender: authorPhoneNumber,
|
||||
conversationId: convoId,
|
||||
});
|
||||
}
|
||||
|
||||
private async onAddModerator() {
|
||||
await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId);
|
||||
private onClickOnMessageOuterContainer(event: any) {
|
||||
const { multiSelectMode, id } = this.props;
|
||||
const selection = window.getSelection();
|
||||
// Text is being selected
|
||||
if (selection && selection.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
|
||||
// User clicked on message body
|
||||
const target = event.target as HTMLDivElement;
|
||||
if ((!multiSelectMode && target.className === 'text-selectable') || window.contextMenuShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveFromModerator() {
|
||||
await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId);
|
||||
private onClickOnMessageInnerContainer(event: any) {
|
||||
const selection = window.getSelection();
|
||||
// Text is being selected
|
||||
if (selection && selection.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
|
||||
// User clicked on message body
|
||||
const target = event.target as HTMLDivElement;
|
||||
if (target.className === 'text-selectable' || window.contextMenuShown) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AttachmentTypeWithPath } from '../../types/Attachment';
|
||||
import _ from 'lodash';
|
||||
import { animation, Item, Menu } from 'react-contexify';
|
||||
|
||||
import { MessageInteraction } from '../../interactions';
|
||||
import { getMessageById } from '../../data/data';
|
||||
import { deleteMessagesById, replyToMessage } from '../../interactions/conversationInteractions';
|
||||
import { showMessageDetailsView, toggleSelectedMessageId } from '../../state/ducks/conversations';
|
||||
import { saveAttachmentToDisk } from '../../util/attachmentsUtil';
|
||||
import {
|
||||
addSenderAsModerator,
|
||||
removeSenderFromModerator,
|
||||
} from '../../interactions/messageInteractions';
|
||||
import { MessageDeliveryStatus, MessageModelType } from '../../models/messageType';
|
||||
import { pushUnblockToSend } from '../../session/utils/Toast';
|
||||
|
||||
export type PropsForMessageContextMenu = {
|
||||
messageId: string;
|
||||
authorPhoneNumber: string;
|
||||
direction: MessageModelType;
|
||||
timestamp: number;
|
||||
serverTimestamp?: number;
|
||||
convoId: string;
|
||||
isPublic?: boolean;
|
||||
isBlocked: boolean;
|
||||
attachments?: Array<AttachmentTypeWithPath>;
|
||||
status?: MessageDeliveryStatus | null;
|
||||
isOpenGroupV2?: boolean;
|
||||
isDeletable: boolean;
|
||||
text: string | null;
|
||||
isAdmin?: boolean;
|
||||
weAreAdmin?: boolean;
|
||||
contextMenuId: string;
|
||||
};
|
||||
|
||||
export const MessageContextMenu = (props: PropsForMessageContextMenu) => {
|
||||
const {
|
||||
attachments,
|
||||
authorPhoneNumber,
|
||||
convoId,
|
||||
direction,
|
||||
status,
|
||||
isDeletable,
|
||||
messageId,
|
||||
contextMenuId,
|
||||
isPublic,
|
||||
isOpenGroupV2,
|
||||
weAreAdmin,
|
||||
isAdmin,
|
||||
text,
|
||||
serverTimestamp,
|
||||
timestamp,
|
||||
isBlocked,
|
||||
} = props;
|
||||
const showRetry = status === 'error' && direction === 'outgoing';
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
|
||||
const onContextMenuShown = useCallback(() => {
|
||||
window.contextMenuShown = true;
|
||||
}, []);
|
||||
|
||||
const onContextMenuHidden = useCallback(() => {
|
||||
// This function will called before the click event
|
||||
// on the message would trigger (and I was unable to
|
||||
// prevent propagation in this case), so use a short timeout
|
||||
setTimeout(() => {
|
||||
window.contextMenuShown = false;
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onShowDetail = async () => {
|
||||
const found = await getMessageById(messageId);
|
||||
if (found) {
|
||||
const messageDetailsProps = await found.getPropsForMessageDetail();
|
||||
window.inboxStore?.dispatch(showMessageDetailsView(messageDetailsProps));
|
||||
} else {
|
||||
window.log.warn(`Message ${messageId} not found in db`);
|
||||
}
|
||||
};
|
||||
|
||||
const selectMessageText = window.i18n('selectMessage');
|
||||
const deleteMessageText = window.i18n('deleteMessage');
|
||||
|
||||
const addModerator = useCallback(() => {
|
||||
void addSenderAsModerator(authorPhoneNumber, convoId);
|
||||
}, [authorPhoneNumber, convoId]);
|
||||
|
||||
const removeModerator = useCallback(() => {
|
||||
void removeSenderFromModerator(authorPhoneNumber, convoId);
|
||||
}, [authorPhoneNumber, convoId]);
|
||||
|
||||
const onReply = useCallback(() => {
|
||||
if (isBlocked) {
|
||||
pushUnblockToSend();
|
||||
return;
|
||||
}
|
||||
void replyToMessage(messageId);
|
||||
}, [isBlocked, messageId]);
|
||||
|
||||
const saveAttachment = useCallback(
|
||||
(e: any) => {
|
||||
e.stopPropagation();
|
||||
if (!attachments?.length) {
|
||||
return;
|
||||
}
|
||||
const messageTimestamp = timestamp || serverTimestamp || 0;
|
||||
void saveAttachmentToDisk({
|
||||
attachment: attachments[0],
|
||||
messageTimestamp,
|
||||
messageSender: authorPhoneNumber,
|
||||
conversationId: convoId,
|
||||
});
|
||||
},
|
||||
[convoId, authorPhoneNumber, timestamp, serverTimestamp, convoId, attachments]
|
||||
);
|
||||
|
||||
const copyText = useCallback(() => {
|
||||
MessageInteraction.copyBodyToClipboard(text);
|
||||
}, [text]);
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
const found = await getMessageById(messageId);
|
||||
if (found) {
|
||||
await found.retrySend();
|
||||
}
|
||||
}, [messageId]);
|
||||
|
||||
const onBan = useCallback(() => {
|
||||
MessageInteraction.banUser(authorPhoneNumber, convoId);
|
||||
}, [authorPhoneNumber, convoId]);
|
||||
|
||||
const onUnban = useCallback(() => {
|
||||
MessageInteraction.unbanUser(authorPhoneNumber, convoId);
|
||||
}, [authorPhoneNumber, convoId]);
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
window.inboxStore?.dispatch(toggleSelectedMessageId(messageId));
|
||||
}, [messageId]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
void deleteMessagesById([messageId], convoId, false);
|
||||
}, [convoId, messageId]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id={contextMenuId}
|
||||
onShown={onContextMenuShown}
|
||||
onHidden={onContextMenuHidden}
|
||||
animation={animation.fade}
|
||||
>
|
||||
{!multipleAttachments && attachments && attachments[0] ? (
|
||||
<Item onClick={saveAttachment}>{window.i18n('downloadAttachment')}</Item>
|
||||
) : null}
|
||||
|
||||
<Item onClick={copyText}>{window.i18n('copyMessage')}</Item>
|
||||
<Item onClick={onReply}>{window.i18n('replyToMessage')}</Item>
|
||||
<Item onClick={onShowDetail}>{window.i18n('moreInformation')}</Item>
|
||||
{showRetry ? <Item onClick={onRetry}>{window.i18n('resend')}</Item> : null}
|
||||
{isDeletable ? (
|
||||
<>
|
||||
<Item onClick={onSelect}>{selectMessageText}</Item>
|
||||
<Item onClick={onDelete}>{deleteMessageText}</Item>
|
||||
</>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null}
|
||||
{weAreAdmin && isOpenGroupV2 ? (
|
||||
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic && !isAdmin ? (
|
||||
<Item onClick={addModerator}>{window.i18n('addAsModerator')}</Item>
|
||||
) : null}
|
||||
{weAreAdmin && isPublic && isAdmin ? (
|
||||
<Item onClick={removeModerator}>{window.i18n('removeFromModerators')}</Item>
|
||||
) : null}
|
||||
</Menu>
|
||||
);
|
||||
};
|
|
@ -299,7 +299,7 @@ export const QuoteReferenceWarning = (props: any) => {
|
|||
};
|
||||
|
||||
export const Quote = (props: QuotePropsWithListener) => {
|
||||
const [imageBroken, setImageBroken] = useState(false);
|
||||
const [_imageBroken, setImageBroken] = useState(false);
|
||||
|
||||
const handleImageErrorBound = null;
|
||||
|
||||
|
@ -316,33 +316,31 @@ export const Quote = (props: QuotePropsWithListener) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote-container',
|
||||
withContentAbove ? 'module-quote-container--with-content-above' : null
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
className={classNames(
|
||||
'module-quote-container',
|
||||
withContentAbove ? 'module-quote-container--with-content-above' : null
|
||||
'module-quote',
|
||||
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
|
||||
!onClick ? 'module-quote--no-click' : null,
|
||||
withContentAbove ? 'module-quote--with-content-above' : null,
|
||||
referencedMessageNotFound ? 'module-quote--with-reference-warning' : null
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
className={classNames(
|
||||
'module-quote',
|
||||
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
|
||||
!onClick ? 'module-quote--no-click' : null,
|
||||
withContentAbove ? 'module-quote--with-content-above' : null,
|
||||
referencedMessageNotFound ? 'module-quote--with-reference-warning' : null
|
||||
)}
|
||||
>
|
||||
<div className="module-quote__primary">
|
||||
<QuoteAuthor {...props} />
|
||||
<QuoteGenericFile {...props} />
|
||||
<QuoteText {...props} />
|
||||
</div>
|
||||
<QuoteIconContainer {...props} handleImageErrorBound={handleImageErrorBound} />
|
||||
<div className="module-quote__primary">
|
||||
<QuoteAuthor {...props} />
|
||||
<QuoteGenericFile {...props} />
|
||||
<QuoteText {...props} />
|
||||
</div>
|
||||
<QuoteReferenceWarning {...props} />
|
||||
<QuoteIconContainer {...props} handleImageErrorBound={handleImageErrorBound} />
|
||||
</div>
|
||||
</>
|
||||
<QuoteReferenceWarning {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,9 +8,7 @@ import styled, { DefaultTheme, useTheme } from 'styled-components';
|
|||
import { MessageDeliveryStatus, MessageModelType } from '../../../models/messageType';
|
||||
|
||||
type Props = {
|
||||
disableMenu?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isDeletable: boolean;
|
||||
text?: string | null;
|
||||
id: string;
|
||||
collapseMetadata?: boolean;
|
||||
|
|
|
@ -106,6 +106,12 @@ export class SessionInboxView extends React.Component<any, State> {
|
|||
messageDetailProps: undefined,
|
||||
selectedMessageIds: [],
|
||||
selectedConversation: undefined,
|
||||
areMoreMessagesBeingFetched: false,
|
||||
showScrollButton: false,
|
||||
animateQuotedMessageId: undefined,
|
||||
lightBox: undefined,
|
||||
nextMessageToPlay: undefined,
|
||||
quotedMessage: undefined,
|
||||
},
|
||||
user: {
|
||||
ourNumber: UserUtils.getOurPubKeyStrFromCache(),
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { getShowScrollButton } from '../../state/selectors/conversations';
|
||||
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
|
||||
type Props = {
|
||||
onClick?: () => any;
|
||||
show?: boolean;
|
||||
};
|
||||
|
||||
const SessionScrollButtonDiv = styled.div`
|
||||
|
@ -18,9 +19,11 @@ const SessionScrollButtonDiv = styled.div`
|
|||
export const SessionScrollButton = (props: Props) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
|
||||
const show = useSelector(getShowScrollButton);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.show && (
|
||||
{show && (
|
||||
<SessionScrollButtonDiv theme={themeContext}>
|
||||
<SessionIconButton
|
||||
iconType={SessionIconType.Chevron}
|
||||
|
|
|
@ -45,9 +45,14 @@ import {
|
|||
getItemById,
|
||||
hasLinkPreviewPopupBeenDisplayed,
|
||||
} from '../../../data/data';
|
||||
import { getQuotedMessage } from '../../../state/selectors/conversations';
|
||||
import {
|
||||
getQuotedMessage,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationKey,
|
||||
} from '../../../state/selectors/conversations';
|
||||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../../state/reducer';
|
||||
import { getTheme } from '../../../state/selectors/theme';
|
||||
|
||||
export interface ReplyingToMessageProps {
|
||||
convoId: string;
|
||||
|
@ -76,19 +81,13 @@ interface Props {
|
|||
|
||||
onLoadVoiceNoteView: any;
|
||||
onExitVoiceNoteView: any;
|
||||
isBlocked: boolean;
|
||||
isPrivate: boolean;
|
||||
isKickedFromGroup: boolean;
|
||||
left: boolean;
|
||||
selectedConversationKey: string;
|
||||
selectedConversation: ReduxConversationType | undefined;
|
||||
isPublic: boolean;
|
||||
quotedMessageProps?: ReplyingToMessageProps;
|
||||
stagedAttachments: Array<StagedAttachmentType>;
|
||||
clearAttachments: () => any;
|
||||
removeAttachment: (toRemove: AttachmentType) => void;
|
||||
onChoseAttachments: (newAttachments: Array<File>) => void;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -304,13 +303,15 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
sendVoiceMessage={this.sendVoiceMessage}
|
||||
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
|
||||
onExitVoiceNoteView={this.onExitVoiceNoteView}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private isTypingEnabled(): boolean {
|
||||
const { isBlocked, isKickedFromGroup, left, isPrivate } = this.props;
|
||||
if (!this.props.selectedConversation) {
|
||||
return false;
|
||||
}
|
||||
const { isBlocked, isKickedFromGroup, left } = this.props.selectedConversation;
|
||||
|
||||
return !(isBlocked || isKickedFromGroup || left);
|
||||
}
|
||||
|
@ -326,7 +327,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
iconType={SessionIconType.CirclePlus}
|
||||
iconSize={SessionIconSize.Large}
|
||||
onClick={this.onChooseAttachment}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -344,7 +344,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
iconType={SessionIconType.Microphone}
|
||||
iconSize={SessionIconSize.Huge}
|
||||
onClick={this.onLoadVoiceNoteView}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -364,7 +363,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
iconType={SessionIconType.Emoji}
|
||||
iconSize={SessionIconSize.Large}
|
||||
onClick={this.toggleEmojiPanel}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
<div className="send-message-button">
|
||||
|
@ -373,7 +371,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
iconSize={SessionIconSize.Large}
|
||||
iconRotation={90}
|
||||
onClick={this.onSendMessage}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -391,7 +388,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
private renderTextArea() {
|
||||
const { i18n } = window;
|
||||
const { message } = this.state;
|
||||
const { isKickedFromGroup, left, isPrivate, isBlocked, theme } = this.props;
|
||||
|
||||
if (!this.props.selectedConversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isKickedFromGroup, left, isPrivate, isBlocked } = this.props.selectedConversation;
|
||||
const messagePlaceHolder = isKickedFromGroup
|
||||
? i18n('youGotKickedFromGroup')
|
||||
: left
|
||||
|
@ -471,11 +473,15 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
if (!query) {
|
||||
overridenQuery = '';
|
||||
}
|
||||
if (this.props.isPublic) {
|
||||
if (!this.props.selectedConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.selectedConversation.isPublic) {
|
||||
this.fetchUsersForOpenGroup(overridenQuery, callback);
|
||||
return;
|
||||
}
|
||||
if (!this.props.isPrivate) {
|
||||
if (!this.props.selectedConversation.isPrivate) {
|
||||
this.fetchUsersForClosedGroup(overridenQuery, callback);
|
||||
return;
|
||||
}
|
||||
|
@ -799,13 +805,17 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
const messagePlaintext = cleanMentions(this.parseEmojis(this.state.message));
|
||||
|
||||
const { isBlocked, isPrivate, left, isKickedFromGroup } = this.props;
|
||||
const { selectedConversation } = this.props;
|
||||
|
||||
if (isBlocked && isPrivate) {
|
||||
if (!selectedConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedConversation.isBlocked && selectedConversation.isPrivate) {
|
||||
ToastUtils.pushUnblockToSend();
|
||||
return;
|
||||
}
|
||||
if (isBlocked && !isPrivate) {
|
||||
if (selectedConversation.isBlocked && !selectedConversation.isPrivate) {
|
||||
ToastUtils.pushUnblockToSendGroup();
|
||||
return;
|
||||
}
|
||||
|
@ -816,11 +826,11 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!isPrivate && left) {
|
||||
if (!selectedConversation.isPrivate && selectedConversation.left) {
|
||||
ToastUtils.pushYouLeftTheGroup();
|
||||
return;
|
||||
}
|
||||
if (!isPrivate && isKickedFromGroup) {
|
||||
if (!selectedConversation.isPrivate && selectedConversation.isKickedFromGroup) {
|
||||
ToastUtils.pushYouLeftTheGroup();
|
||||
return;
|
||||
}
|
||||
|
@ -992,6 +1002,9 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
quotedMessageProps: getQuotedMessage(state),
|
||||
selectedConversation: getSelectedConversation(state),
|
||||
selectedConversationKey: getSelectedConversationKey(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -235,13 +235,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
</div>
|
||||
|
||||
<SessionCompositionBox
|
||||
isBlocked={selectedConversation.isBlocked}
|
||||
left={selectedConversation.left}
|
||||
isKickedFromGroup={selectedConversation.isKickedFromGroup}
|
||||
isPrivate={selectedConversation.isPrivate}
|
||||
isPublic={selectedConversation.isPublic}
|
||||
selectedConversationKey={selectedConversationKey}
|
||||
selectedConversation={selectedConversation}
|
||||
sendMessage={sendMessageFn}
|
||||
stagedAttachments={stagedAttachments}
|
||||
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
|
||||
|
@ -249,7 +242,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
clearAttachments={this.clearAttachments}
|
||||
removeAttachment={this.removeAttachment}
|
||||
onChoseAttachments={this.onChoseAttachments}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -305,15 +297,8 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
// ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
private onKeyDown(event: any) {
|
||||
const messageContainer = this.messageContainerRef.current;
|
||||
if (!messageContainer) {
|
||||
return;
|
||||
}
|
||||
const selectionMode = !!this.props.selectedMessages.length;
|
||||
const recordingMode = this.state.showRecordingView;
|
||||
const pageHeight = messageContainer.clientHeight;
|
||||
const arrowScrollPx = 50;
|
||||
const pageScrollPx = pageHeight * 0.8;
|
||||
if (event.key === 'Escape') {
|
||||
// EXIT MEDIA VIEW
|
||||
if (recordingMode) {
|
||||
|
@ -328,19 +313,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
window.inboxStore?.dispatch(resetSelectedMessageIds());
|
||||
}
|
||||
break;
|
||||
// Scrolling
|
||||
case 'ArrowUp':
|
||||
messageContainer.scrollBy(0, -arrowScrollPx);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
messageContainer.scrollBy(0, arrowScrollPx);
|
||||
break;
|
||||
case 'PageUp':
|
||||
messageContainer.scrollBy(0, -pageScrollPx);
|
||||
break;
|
||||
case 'PageDown':
|
||||
messageContainer.scrollBy(0, pageScrollPx);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ import {
|
|||
PropsForExpirationTimer,
|
||||
PropsForGroupInvitation,
|
||||
PropsForGroupUpdate,
|
||||
quotedMessageToAnimate,
|
||||
ReduxConversationType,
|
||||
setNextMessageToPlay,
|
||||
showScrollToBottomButton,
|
||||
SortedMessageModelProps,
|
||||
} from '../../../state/ducks/conversations';
|
||||
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
|
||||
|
@ -34,18 +37,16 @@ import { DataExtractionNotification } from '../../conversation/DataExtractionNot
|
|||
import { StateType } from '../../../state/reducer';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import {
|
||||
areMoreMessagesBeingFetched,
|
||||
getMessagesOfSelectedConversation,
|
||||
getNextMessageToPlayIndex,
|
||||
getQuotedMessageToAnimate,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationKey,
|
||||
getShowScrollButton,
|
||||
isMessageSelectionMode,
|
||||
} from '../../../state/selectors/conversations';
|
||||
|
||||
interface State {
|
||||
showScrollButton: boolean;
|
||||
animateQuotedMessageId?: string;
|
||||
nextMessageToPlay: number | undefined;
|
||||
}
|
||||
|
||||
export type SessionMessageListProps = {
|
||||
messageContainerRef: React.RefObject<any>;
|
||||
};
|
||||
|
@ -55,6 +56,8 @@ type Props = SessionMessageListProps & {
|
|||
messagesProps: Array<SortedMessageModelProps>;
|
||||
|
||||
conversation?: ReduxConversationType;
|
||||
showScrollButton: boolean;
|
||||
animateQuotedMessageId: string | undefined;
|
||||
};
|
||||
|
||||
const UnreadIndicator = (props: { messageId: string; show: boolean }) => (
|
||||
|
@ -124,26 +127,27 @@ const GenericMessageItem = (props: {
|
|||
messageProps: SortedMessageModelProps;
|
||||
playableMessageIndex?: number;
|
||||
showUnreadIndicator: boolean;
|
||||
scrollToQuoteMessage: (options: QuoteClickOptions) => Promise<void>;
|
||||
playNextMessage?: (value: number) => void;
|
||||
}) => {
|
||||
const multiSelectMode = useSelector(isMessageSelectionMode);
|
||||
// const selectedConversation = useSelector(getSelectedConversationKey) as string;
|
||||
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
|
||||
const nextMessageToPlay = useSelector(getNextMessageToPlayIndex);
|
||||
|
||||
const messageId = props.messageId;
|
||||
|
||||
console.warn('FIXME audric');
|
||||
|
||||
// const onQuoteClick = props.messageProps.propsForMessage.quote
|
||||
// ? this.scrollToQuoteMessage
|
||||
// : async () => {};
|
||||
const onQuoteClick = props.messageProps.propsForMessage.quote
|
||||
? props.scrollToQuoteMessage
|
||||
: undefined;
|
||||
|
||||
const regularProps: MessageRegularProps = {
|
||||
...props.messageProps.propsForMessage,
|
||||
// firstMessageOfSeries,
|
||||
firstMessageOfSeries: props.messageProps.firstMessageOfSeries,
|
||||
multiSelectMode,
|
||||
// isQuotedMessageToAnimate: messageId === this.state.animateQuotedMessageId,
|
||||
// nextMessageToPlay: this.state.nextMessageToPlay,
|
||||
// playNextMessage: this.playNextMessage,
|
||||
// onQuoteClick,
|
||||
isQuotedMessageToAnimate: messageId === quotedMessageToAnimate,
|
||||
nextMessageToPlay,
|
||||
playNextMessage: props.playNextMessage,
|
||||
onQuoteClick,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -152,7 +156,6 @@ const GenericMessageItem = (props: {
|
|||
{...regularProps}
|
||||
playableMessageIndex={props.playableMessageIndex}
|
||||
multiSelectMode={multiSelectMode}
|
||||
// onQuoteClick={onQuoteClick}
|
||||
key={messageId}
|
||||
/>
|
||||
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
|
||||
|
@ -160,8 +163,13 @@ const GenericMessageItem = (props: {
|
|||
);
|
||||
};
|
||||
|
||||
const MessageList = ({ hasNextPage: boolean, isNextPageLoading, list, loadNextPage }) => {
|
||||
const MessageList = (props: {
|
||||
scrollToQuoteMessage: (options: QuoteClickOptions) => Promise<void>;
|
||||
playNextMessage?: (value: number) => void;
|
||||
}) => {
|
||||
const messagesProps = useSelector(getMessagesOfSelectedConversation);
|
||||
const isFetchingMore = useSelector(areMoreMessagesBeingFetched);
|
||||
|
||||
let playableMessageIndex = 0;
|
||||
|
||||
return (
|
||||
|
@ -177,7 +185,6 @@ const MessageList = ({ hasNextPage: boolean, isNextPageLoading, list, loadNextPa
|
|||
// AND we are not scrolled all the way to the bottom
|
||||
// THEN, show the unread banner for the current message
|
||||
const showUnreadIndicator = Boolean(messageProps.firstUnread);
|
||||
console.warn('&& this.getScrollOffsetBottomPx() !== 0');
|
||||
|
||||
if (groupNotificationProps) {
|
||||
return (
|
||||
|
@ -238,6 +245,8 @@ const MessageList = ({ hasNextPage: boolean, isNextPageLoading, list, loadNextPa
|
|||
messageId={messageProps.propsForMessage.id}
|
||||
messageProps={messageProps}
|
||||
showUnreadIndicator={showUnreadIndicator}
|
||||
scrollToQuoteMessage={props.scrollToQuoteMessage}
|
||||
playNextMessage={props.playNextMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -245,21 +254,18 @@ const MessageList = ({ hasNextPage: boolean, isNextPageLoading, list, loadNextPa
|
|||
);
|
||||
};
|
||||
|
||||
class SessionMessagesListInner extends React.Component<Props, State> {
|
||||
class SessionMessagesListInner extends React.Component<Props> {
|
||||
private scrollOffsetBottomPx: number = Number.MAX_VALUE;
|
||||
private ignoreScrollEvents: boolean;
|
||||
private timeoutResetQuotedScroll: NodeJS.Timeout | null = null;
|
||||
private debouncedHandleScroll: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showScrollButton: false,
|
||||
nextMessageToPlay: undefined,
|
||||
};
|
||||
autoBind(this);
|
||||
|
||||
this.ignoreScrollEvents = true;
|
||||
this.debouncedHandleScroll = _.throttle(this.handleScroll, 500);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -277,7 +283,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, _prevState: State) {
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
const isSameConvo = prevProps.conversationKey === this.props.conversationKey;
|
||||
const messageLengthChanged = prevProps.messagesProps.length !== this.props.messagesProps.length;
|
||||
if (
|
||||
|
@ -288,13 +294,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
this.scrollOffsetBottomPx = Number.MAX_VALUE;
|
||||
this.ignoreScrollEvents = true;
|
||||
this.setupTimeoutResetQuotedHighlightedMessage(true);
|
||||
this.setState(
|
||||
{
|
||||
showScrollButton: false,
|
||||
animateQuotedMessageId: undefined,
|
||||
},
|
||||
this.scrollToUnread
|
||||
);
|
||||
this.scrollToUnread();
|
||||
} else {
|
||||
// if we got new message for this convo, and we are scrolled to bottom
|
||||
if (isSameConvo && messageLengthChanged) {
|
||||
|
@ -318,7 +318,6 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
const { conversationKey, conversation } = this.props;
|
||||
const { showScrollButton } = this.state;
|
||||
|
||||
if (!conversationKey || !conversation) {
|
||||
return null;
|
||||
|
@ -334,7 +333,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className="messages-container"
|
||||
onScroll={this.handleScroll}
|
||||
onScroll={this.debouncedHandleScroll}
|
||||
ref={this.props.messageContainerRef}
|
||||
>
|
||||
<TypingBubble
|
||||
|
@ -345,13 +344,12 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
key="typing-bubble"
|
||||
/>
|
||||
|
||||
<MessageList />
|
||||
|
||||
<SessionScrollButton
|
||||
show={showScrollButton}
|
||||
onClick={this.scrollToBottom}
|
||||
key="scroll-down-button"
|
||||
<MessageList
|
||||
scrollToQuoteMessage={this.scrollToQuoteMessage}
|
||||
playNextMessage={this.playNextMessage}
|
||||
/>
|
||||
|
||||
<SessionScrollButton onClick={this.scrollToBottom} key="scroll-down-button" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -385,7 +383,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
* Sets the targeted index for the next
|
||||
* @param index index of message that just completed
|
||||
*/
|
||||
private readonly playNextMessage = (index: any) => {
|
||||
private playNextMessage(index: any) {
|
||||
const { messagesProps } = this.props;
|
||||
let nextIndex: number | undefined = index - 1;
|
||||
|
||||
|
@ -393,9 +391,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
const latestMessagePlayed = index <= 0 || messagesProps.length < index - 1;
|
||||
if (latestMessagePlayed) {
|
||||
nextIndex = undefined;
|
||||
this.setState({
|
||||
nextMessageToPlay: nextIndex,
|
||||
});
|
||||
window.inboxStore?.dispatch(setNextMessageToPlay(nextIndex));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -407,10 +403,8 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
nextIndex = undefined;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
nextMessageToPlay: nextIndex,
|
||||
});
|
||||
};
|
||||
window.inboxStore?.dispatch(setNextMessageToPlay(nextIndex));
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~ SCROLLING METHODS ~~~~~~~~~~~~~
|
||||
|
@ -438,12 +432,12 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
const scrollOffsetPc = this.scrollOffsetBottomPx / clientHeight;
|
||||
|
||||
// Scroll button appears if you're more than 75% scrolled up
|
||||
if (scrollOffsetPc > scrollButtonViewShowLimit && !this.state.showScrollButton) {
|
||||
this.setState({ showScrollButton: true });
|
||||
if (scrollOffsetPc > scrollButtonViewShowLimit && !this.props.showScrollButton) {
|
||||
window.inboxStore?.dispatch(showScrollToBottomButton(true));
|
||||
}
|
||||
// Scroll button disappears if you're more less than 40% scrolled up
|
||||
if (scrollOffsetPc < scrollButtonViewHideLimit && this.state.showScrollButton) {
|
||||
this.setState({ showScrollButton: false });
|
||||
if (scrollOffsetPc < scrollButtonViewHideLimit && this.props.showScrollButton) {
|
||||
window.inboxStore?.dispatch(showScrollToBottomButton(false));
|
||||
}
|
||||
|
||||
// Scrolled to bottom
|
||||
|
@ -513,26 +507,24 @@ class SessionMessagesListInner extends React.Component<Props, State> {
|
|||
if (clearOnly) {
|
||||
return;
|
||||
}
|
||||
if (this.state.animateQuotedMessageId !== undefined) {
|
||||
if (this.props.animateQuotedMessageId !== undefined) {
|
||||
this.timeoutResetQuotedScroll = global.setTimeout(() => {
|
||||
this.setState({ animateQuotedMessageId: undefined });
|
||||
window.inboxStore?.dispatch(quotedMessageToAnimate(undefined));
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToMessage(messageId: string, smooth: boolean = false) {
|
||||
const topUnreadMessage = document.getElementById(messageId);
|
||||
topUnreadMessage?.scrollIntoView({
|
||||
const messageElementDom = document.getElementById(messageId);
|
||||
messageElementDom?.scrollIntoView({
|
||||
behavior: smooth ? 'smooth' : 'auto',
|
||||
block: 'center',
|
||||
});
|
||||
|
||||
// we consider that a `smooth` set to true, means it's a quoted message, so highlight this message on the UI
|
||||
if (smooth) {
|
||||
this.setState(
|
||||
{ animateQuotedMessageId: messageId },
|
||||
this.setupTimeoutResetQuotedHighlightedMessage
|
||||
);
|
||||
window.inboxStore?.dispatch(quotedMessageToAnimate(messageId));
|
||||
this.setupTimeoutResetQuotedHighlightedMessage;
|
||||
}
|
||||
|
||||
const messageContainer = this.props.messageContainerRef.current;
|
||||
|
@ -633,6 +625,8 @@ const mapStateToProps = (state: StateType) => {
|
|||
conversationKey: getSelectedConversationKey(state),
|
||||
conversation: getSelectedConversation(state),
|
||||
messagesProps: getMessagesOfSelectedConversation(state),
|
||||
showScrollButton: getShowScrollButton(state),
|
||||
animateQuotedMessageId: getQuotedMessageToAnimate(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
|
|||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton';
|
||||
import { Constants } from '../../../session';
|
||||
import { ToastUtils } from '../../../session/utils';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import autoBind from 'auto-bind';
|
||||
import MicRecorder from 'mic-recorder-to-mp3';
|
||||
|
||||
|
@ -14,7 +13,6 @@ interface Props {
|
|||
onExitVoiceNoteView: any;
|
||||
onLoadVoiceNoteView: any;
|
||||
sendVoiceMessage: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -36,9 +34,6 @@ function getTimestamp(asInt = false) {
|
|||
}
|
||||
|
||||
class SessionRecordingInner extends React.Component<Props, State> {
|
||||
private readonly visualisationRef: React.RefObject<HTMLDivElement>;
|
||||
private readonly visualisationCanvas: React.RefObject<HTMLCanvasElement>;
|
||||
private readonly playbackCanvas: React.RefObject<HTMLCanvasElement>;
|
||||
private recorder: any;
|
||||
private audioBlobMp3?: Blob;
|
||||
private audioElement?: HTMLAudioElement | null;
|
||||
|
@ -49,9 +44,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
autoBind(this);
|
||||
|
||||
// Refs
|
||||
this.visualisationRef = React.createRef();
|
||||
this.visualisationCanvas = React.createRef();
|
||||
this.playbackCanvas = React.createRef();
|
||||
|
||||
const now = getTimestamp();
|
||||
const updateTimerInterval = global.setInterval(this.timerUpdate, 500);
|
||||
|
@ -127,7 +119,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
iconSize={SessionIconSize.Medium}
|
||||
iconColor={Constants.UI.COLORS.DANGER_ALT}
|
||||
onClick={actionPauseFn}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
{actionPauseAudio && (
|
||||
|
@ -135,7 +126,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
iconType={SessionIconType.Pause}
|
||||
iconSize={SessionIconSize.Medium}
|
||||
onClick={actionPauseFn}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
{actionPlayAudio && (
|
||||
|
@ -143,7 +133,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
iconType={SessionIconType.Play}
|
||||
iconSize={SessionIconSize.Medium}
|
||||
onClick={this.playAudio}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -151,16 +140,10 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
<SessionIconButton
|
||||
iconType={SessionIconType.Microphone}
|
||||
iconSize={SessionIconSize.Huge}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="session-recording--visualisation" ref={this.visualisationRef}>
|
||||
{!isRecording && <canvas ref={this.playbackCanvas} />}
|
||||
{isRecording && <canvas ref={this.visualisationCanvas} />}
|
||||
</div>
|
||||
|
||||
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
|
||||
{displayTimeString}
|
||||
{isRecording && <div className="session-recording--timer-light" />}
|
||||
|
@ -173,7 +156,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
iconSize={SessionIconSize.Large}
|
||||
iconRotation={90}
|
||||
onClick={this.onSendVoiceMessage}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -343,4 +325,4 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export const SessionRecording = withTheme(SessionRecordingInner);
|
||||
export const SessionRecording = SessionRecordingInner;
|
||||
|
|
|
@ -49,13 +49,12 @@ import {
|
|||
uploadLinkPreviewsV2,
|
||||
uploadQuoteThumbnailsV2,
|
||||
} from '../session/utils/AttachmentsV2';
|
||||
import { acceptOpenGroupInvitation } from '../interactions/messageInteractions';
|
||||
import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
|
||||
import { getV2OpenGroupRoom } from '../data/opengroups';
|
||||
import { getMessageController } from '../session/messages';
|
||||
import { isUsFromCache } from '../session/utils/User';
|
||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||
import { AttachmentType, AttachmentTypeWithPath } from '../types/Attachment';
|
||||
import { AttachmentTypeWithPath } from '../types/Attachment';
|
||||
|
||||
export class MessageModel extends Backbone.Model<MessageAttributes> {
|
||||
constructor(attributes: MessageAttributesOptionals) {
|
||||
|
@ -80,6 +79,8 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
void this.setToExpire();
|
||||
autoBind(this);
|
||||
|
||||
this.dispatchMessageUpdate = _.debounce(this.dispatchMessageUpdate, 500);
|
||||
|
||||
window.contextMenuShown = false;
|
||||
|
||||
this.getProps();
|
||||
|
@ -99,12 +100,8 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
return messageProps;
|
||||
}
|
||||
|
||||
// Keep props ready
|
||||
public generateProps(): MessageModelProps {
|
||||
const messageProps = this.getProps();
|
||||
window.inboxStore?.dispatch(conversationActions.messageChanged(messageProps));
|
||||
|
||||
return messageProps;
|
||||
private dispatchMessageUpdate() {
|
||||
window.inboxStore?.dispatch(conversationActions.messageChanged(this.getProps()));
|
||||
}
|
||||
|
||||
public idForLogging() {
|
||||
|
@ -1082,7 +1079,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
|
||||
perfStart(`messageCommit-${this.attributes.id}`);
|
||||
const id = await saveMessage(this.attributes);
|
||||
this.generateProps();
|
||||
this.dispatchMessageUpdate();
|
||||
perfEnd(`messageCommit-${this.attributes.id}`, 'messageCommit');
|
||||
|
||||
return id;
|
||||
|
|
|
@ -246,7 +246,7 @@ export interface MessageRegularProps {
|
|||
isUnread: boolean;
|
||||
isQuotedMessageToAnimate?: boolean;
|
||||
isTrustedForAttachmentDownload: boolean;
|
||||
onQuoteClick: (options: QuoteClickOptions) => Promise<void>;
|
||||
onQuoteClick?: (options: QuoteClickOptions) => Promise<void>;
|
||||
|
||||
playableMessageIndex?: number;
|
||||
nextMessageToPlay?: number;
|
||||
|
|
|
@ -233,12 +233,16 @@ export type ConversationsStateType = {
|
|||
conversationLookup: ConversationLookupType;
|
||||
selectedConversation?: string;
|
||||
messages: Array<SortedMessageModelProps>;
|
||||
messageDetailProps: MessagePropsDetails | undefined;
|
||||
messageDetailProps?: MessagePropsDetails;
|
||||
showRightPanel: boolean;
|
||||
selectedMessageIds: Array<string>;
|
||||
lightBox?: LightBoxOptions;
|
||||
quotedMessage?: ReplyingToMessageProps;
|
||||
areMoreMessagesBeingFetched: boolean;
|
||||
|
||||
showScrollButton: boolean;
|
||||
animateQuotedMessageId?: string;
|
||||
nextMessageToPlay?: number;
|
||||
};
|
||||
|
||||
async function getMessages(
|
||||
|
@ -394,6 +398,7 @@ function getEmptyState(): ConversationsStateType {
|
|||
showRightPanel: false,
|
||||
selectedMessageIds: [],
|
||||
areMoreMessagesBeingFetched: false,
|
||||
showScrollButton: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -708,14 +713,21 @@ const conversationsSlice = createSlice({
|
|||
if (state.selectedConversation === action.payload.id) {
|
||||
return state;
|
||||
}
|
||||
state.showRightPanel = false;
|
||||
state.messageDetailProps = undefined;
|
||||
state.selectedMessageIds = [];
|
||||
state.selectedConversation = action.payload.id;
|
||||
state.messages = [];
|
||||
state.quotedMessage = undefined;
|
||||
state.lightBox = undefined;
|
||||
return state;
|
||||
return {
|
||||
conversationLookup: state.conversationLookup,
|
||||
selectedConversation: action.payload.id,
|
||||
areMoreMessagesBeingFetched: false,
|
||||
messages: [],
|
||||
showRightPanel: false,
|
||||
selectedMessageIds: [],
|
||||
lightBox: undefined,
|
||||
messageDetailProps: undefined,
|
||||
quotedMessage: undefined,
|
||||
|
||||
nextMessageToPlay: undefined,
|
||||
showScrollButton: false,
|
||||
animateQuotedMessageId: undefined,
|
||||
};
|
||||
},
|
||||
showLightBox(
|
||||
state: ConversationsStateType,
|
||||
|
@ -724,6 +736,10 @@ const conversationsSlice = createSlice({
|
|||
state.lightBox = action.payload;
|
||||
return state;
|
||||
},
|
||||
showScrollToBottomButton(state: ConversationsStateType, action: PayloadAction<boolean>) {
|
||||
state.showScrollButton = action.payload;
|
||||
return state;
|
||||
},
|
||||
quoteMessage(
|
||||
state: ConversationsStateType,
|
||||
action: PayloadAction<ReplyingToMessageProps | undefined>
|
||||
|
@ -731,6 +747,17 @@ const conversationsSlice = createSlice({
|
|||
state.quotedMessage = action.payload;
|
||||
return state;
|
||||
},
|
||||
quotedMessageToAnimate(
|
||||
state: ConversationsStateType,
|
||||
action: PayloadAction<string | undefined>
|
||||
) {
|
||||
state.animateQuotedMessageId = action.payload;
|
||||
return state;
|
||||
},
|
||||
setNextMessageToPlay(state: ConversationsStateType, action: PayloadAction<number | undefined>) {
|
||||
state.nextMessageToPlay = action.payload;
|
||||
return state;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder: any) => {
|
||||
// Add reducers for additional action types here, and handle loading state as needed
|
||||
|
@ -750,22 +777,6 @@ const conversationsSlice = createSlice({
|
|||
return state;
|
||||
}
|
||||
);
|
||||
builder.addCase(
|
||||
fetchMessagesForConversation.fulfilled,
|
||||
(state: ConversationsStateType, action: any) => {
|
||||
// this is called once the messages are loaded from the db for the currently selected conversation
|
||||
const { messagesProps, conversationKey } = action.payload as FetchedMessageResults;
|
||||
// double check that this update is for the shown convo
|
||||
if (conversationKey === state.selectedConversation) {
|
||||
return {
|
||||
...state,
|
||||
messages: messagesProps,
|
||||
areMoreMessagesBeingFetched: false,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
);
|
||||
builder.addCase(fetchMessagesForConversation.pending, (state: ConversationsStateType) => {
|
||||
state.areMoreMessagesBeingFetched = true;
|
||||
});
|
||||
|
@ -800,4 +811,7 @@ export const {
|
|||
toggleSelectedMessageId,
|
||||
showLightBox,
|
||||
quoteMessage,
|
||||
showScrollToBottomButton,
|
||||
quotedMessageToAnimate,
|
||||
setNextMessageToPlay,
|
||||
} = actions;
|
||||
|
|
|
@ -297,7 +297,22 @@ export const getQuotedMessage = createSelector(
|
|||
(state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage
|
||||
);
|
||||
|
||||
export const areMoreMessagesBeingFetched = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): boolean => state.areMoreMessagesBeingFetched || false
|
||||
);
|
||||
|
||||
export const areMoreMessagesLoading = createSlice(getConversations,
|
||||
(state: ConversationsStateType): boolean => state.
|
||||
);
|
||||
export const getShowScrollButton = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): boolean => state.showScrollButton || false
|
||||
);
|
||||
|
||||
export const getQuotedMessageToAnimate = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): string | undefined => state.animateQuotedMessageId || undefined
|
||||
);
|
||||
|
||||
export const getNextMessageToPlayIndex = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): number | undefined => state.nextMessageToPlay || undefined
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue