cleanup SessionMessageList p1

This commit is contained in:
Audric Ackermann 2021-07-13 17:00:20 +10:00
parent 399041c5b3
commit 63b81b4c8e
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
18 changed files with 662 additions and 577 deletions

View file

@ -16,7 +16,7 @@ export enum AvatarSize {
}
type Props = {
avatarPath?: string;
avatarPath?: string | null;
name?: string; // display name, profileName or phoneNumber, whatever is set first
pubkey?: string;
size: AvatarSize;

View file

@ -15,8 +15,19 @@ import {
getConversationHeaderTitleProps,
getSelectedConversation,
} from '../../state/selectors/conversations';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useMembersAvatars } from '../../hooks/useMembersAvatar';
import {
closeMessageDetailsView,
openRightPanel,
resetSelectedMessageIds,
} from '../../state/ducks/conversationScreen';
import {
getSelectedMessageIds,
isMessageDetailView,
isMessageSelectionMode,
} from '../../state/selectors/conversationScreen';
import { deleteMessagesById } from '../../interactions/conversationInteractions';
export interface TimerOption {
name: string;
@ -51,7 +62,6 @@ export type ConversationHeaderProps = {
subscriberCount?: number;
expirationSettingName?: string;
// showBackButton: boolean;
notificationForConvo: Array<NotificationForConvoOption>;
currentNotificationSetting: ConversationNotificationSettingType;
hasNickname: boolean;
@ -60,14 +70,6 @@ export type ConversationHeaderProps = {
isKickedFromGroup: boolean;
left: boolean;
// selectionMode: boolean; // is the UI on the message selection mode or not
// onCloseOverlay: () => void;
// onDeleteSelectedMessages: () => void;
// onAvatarClick?: (pubkey: string) => void;
// onGoBack: () => void;
// memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
};
const SelectionOverlay = (props: {
@ -263,21 +265,16 @@ const ConversationHeaderTitle = () => {
);
};
export type ConversationHeaderNonReduxProps = {
showBackButton: boolean;
selectionMode: boolean;
onDeleteSelectedMessages: () => void;
onCloseOverlay: () => void;
onAvatarClick: () => void;
onGoBack: () => void;
};
export const ConversationHeaderWithDetails = (
headerPropsNonRedux: ConversationHeaderNonReduxProps
) => {
export const ConversationHeaderWithDetails = () => {
const headerProps = useSelector(getConversationHeaderProps);
const isSelectionMode = useSelector(isMessageSelectionMode);
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversation = useSelector(getSelectedConversation);
const memberDetails = useMembersAvatars(selectedConversation);
const isMessageDetailOpened = useSelector(isMessageDetailView);
const dispatch = useDispatch();
if (!headerProps) {
return null;
@ -302,34 +299,33 @@ export const ConversationHeaderWithDetails = (
isGroup,
} = headerProps;
const {
onGoBack,
onAvatarClick,
onCloseOverlay,
onDeleteSelectedMessages,
showBackButton,
selectionMode,
} = headerPropsNonRedux;
const triggerId = 'conversation-header';
return (
<div className="module-conversation-header">
<div className="conversation-header--items-wrapper">
<BackButton onGoBack={onGoBack} showBackButton={showBackButton} />
<BackButton
onGoBack={() => {
dispatch(closeMessageDetailsView());
}}
showBackButton={isMessageDetailOpened}
/>
<div className="module-conversation-header__title-container">
<div className="module-conversation-header__title-flex">
<TripleDotsMenu triggerId={triggerId} showBackButton={showBackButton} />
<TripleDotsMenu triggerId={triggerId} showBackButton={isMessageDetailOpened} />
<ConversationHeaderTitle />
</div>
</div>
{!isKickedFromGroup && <ExpirationLength expirationSettingName={expirationSettingName} />}
{!selectionMode && (
{!isSelectionMode && (
<AvatarHeader
onAvatarClick={onAvatarClick}
onAvatarClick={() => {
dispatch(openRightPanel());
}}
phoneNumber={phoneNumber}
showBackButton={showBackButton}
showBackButton={isMessageDetailOpened}
avatarPath={avatarPath}
memberAvatars={memberDetails}
name={name}
@ -354,11 +350,13 @@ export const ConversationHeaderWithDetails = (
/>
</div>
{selectionMode && (
{isSelectionMode && (
<SelectionOverlay
isPublic={isPublic}
onCloseOverlay={onCloseOverlay}
onDeleteSelectedMessages={onDeleteSelectedMessages}
onCloseOverlay={() => dispatch(resetSelectedMessageIds())}
onDeleteSelectedMessages={() => {
void deleteMessagesById(selectedMessageIds, id, true);
}}
/>
)}
</div>

View file

@ -42,6 +42,12 @@ import autoBind from 'auto-bind';
import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer';
import { ClickToTrustSender } from './message/ClickToTrustSender';
import { getMessageById } from '../../data/data';
import { showMessageDetailsView } from '../../state/ducks/conversationScreen';
import { deleteMessagesById } from '../../interactions/conversationInteractions';
import { getSelectedMessage } from '../../state/selectors/search';
import { connect } from 'react-redux';
import { StateType } from '../../state/reducer';
import { getSelectedMessageIds, isMessageSelected } from '../../state/selectors/conversationScreen';
// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
@ -55,12 +61,14 @@ interface State {
const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600;
export class Message extends React.PureComponent<MessageRegularProps, State> {
type Props = MessageRegularProps & { selectedMessages: Array<string> };
class MessageInner extends React.PureComponent<Props, State> {
public expirationCheckInterval: any;
public expiredTimeout: any;
public ctxMenuID: string;
public constructor(props: MessageRegularProps) {
public constructor(props: Props) {
super(props);
autoBind(this);
@ -144,7 +152,6 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
quote,
onClickAttachment,
multiSelectMode,
onSelectMessage,
isTrustedForAttachmentDownload,
} = this.props;
const { imageBroken } = this.state;
@ -186,7 +193,7 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
onError={this.handleImageError}
onClickAttachment={(attachment: AttachmentType) => {
if (multiSelectMode) {
onSelectMessage(id);
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
} else if (onClickAttachment) {
onClickAttachment(attachment);
}
@ -384,7 +391,8 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
e.preventDefault();
e.stopPropagation();
if (multiSelectMode && id) {
this.props.onSelectMessage(id);
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
return;
}
void this.props.onQuoteClick?.({
@ -512,10 +520,7 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
status,
isDeletable,
id,
onSelectMessage,
onDeleteMessage,
onDownload,
onShowDetail,
isPublic,
isOpenGroupV2,
weAreAdmin,
@ -539,6 +544,16 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
}, 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');
@ -586,14 +601,14 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
<>
<Item
onClick={() => {
onSelectMessage(id);
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
}}
>
{selectMessageText}
</Item>
<Item
onClick={() => {
onDeleteMessage(id);
void deleteMessagesById([id], convoId, false);
}}
>
{deleteMessageText}
@ -691,13 +706,22 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
// tslint:disable-next-line: cyclomatic-complexity
public render() {
const { direction, id, selected, multiSelectMode, conversationType, isUnread } = this.props;
const {
direction,
id,
multiSelectMode,
conversationType,
isUnread,
selectedMessages,
} = this.props;
const { expired, expiring } = this.state;
if (expired) {
return null;
}
const selected = selectedMessages.includes(id) || false;
const width = this.getWidth();
const isShowingImage = this.isShowingImage();
@ -762,7 +786,7 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
}
if (id) {
this.props.onSelectMessage(id);
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
}
}}
>
@ -791,7 +815,7 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
}
if (id) {
this.props.onSelectMessage(id);
window.inboxStore?.dispatch(toggleSelectedMessageId(id));
}
}}
>
@ -803,10 +827,8 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
<MessageMetadata
{..._.omit(
this.props,
'onSelectMessage',
'onDeleteMessage',
'onReply',
'onShowDetail',
'onClickAttachment',
'onDownload',
'onQuoteClick'
@ -885,3 +907,16 @@ export class Message extends React.PureComponent<MessageRegularProps, State> {
await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId);
}
}
function toggleSelectedMessageId(id: string): any {
throw new Error('Function not implemented.');
}
const mapStateToProps = (state: StateType) => {
return {
selectedMessages: getSelectedMessageIds(state),
};
};
const smart = connect(mapStateToProps);
export const Message = smart(MessageInner);

View file

@ -6,157 +6,145 @@ import { Avatar, AvatarSize } from '../Avatar';
import { ContactName } from './ContactName';
import { Message } from './Message';
import { MessageRegularProps } from '../../models/messageType';
import { deleteMessagesById } from '../../interactions/conversationInteractions';
import { useSelector } from 'react-redux';
import { getMessageDetailsViewProps } from '../../state/selectors/conversationScreen';
import { ContactPropsMessageDetail } from '../../state/ducks/conversations';
interface Contact {
status: string;
phoneNumber: string;
name?: string;
profileName?: string;
avatarPath?: string;
color: string;
isOutgoingKeyError: boolean;
const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => {
const { avatarPath, phoneNumber, name, profileName } = props.contact;
const userName = name || profileName || phoneNumber;
errors?: Array<Error>;
}
return (
<Avatar avatarPath={avatarPath} name={userName} size={AvatarSize.S} pubkey={phoneNumber} />
);
};
interface Props {
sentAt: number;
receivedAt: number;
const DeleteButtonItem = (props: { message: MessageRegularProps }) => {
const { i18n } = window;
message: MessageRegularProps;
errors: Array<Error>;
contacts: Array<Contact>;
const { message } = props;
onDeleteMessage: (messageId: string) => void;
}
return message.isDeletable ? (
<div className="module-message-detail__delete-button-container">
<button
onClick={() => {
void deleteMessagesById([message.id], message.convoId, true);
}}
className="module-message-detail__delete-button"
>
{i18n('deleteThisMessage')}
</button>
</div>
) : null;
};
export class MessageDetail extends React.Component<Props> {
public renderAvatar(contact: Contact) {
const { avatarPath, phoneNumber, name, profileName } = contact;
const userName = name || profileName || phoneNumber;
const ContactsItem = (props: { contacts: Array<ContactPropsMessageDetail> }) => {
const { contacts } = props;
return (
<Avatar avatarPath={avatarPath} name={userName} size={AvatarSize.S} pubkey={phoneNumber} />
);
if (!contacts || !contacts.length) {
return null;
}
public renderDeleteButton() {
const { i18n } = window;
return (
<div className="module-message-detail__contact-container">
{contacts.map(contact => (
<ContactItem key={contact.phoneNumber} contact={contact} />
))}
</div>
);
};
const { message } = this.props;
const ContactItem = (props: { contact: ContactPropsMessageDetail }) => {
const { contact } = props;
const errors = contact.errors || [];
return message.isDeletable ? (
<div className="module-message-detail__delete-button-container">
<button
onClick={() => {
this.props.onDeleteMessage(message.id);
}}
className="module-message-detail__delete-button"
>
{i18n('deleteThisMessage')}
</button>
</div>
) : null;
}
const statusComponent = !contact.isOutgoingKeyError ? (
<div
className={classNames(
'module-message-detail__contact__status-icon',
`module-message-detail__contact__status-icon--${contact.status}`
)}
/>
) : null;
public renderContact(contact: Contact) {
const errors = contact.errors || [];
const statusComponent = !contact.isOutgoingKeyError ? (
<div
className={classNames(
'module-message-detail__contact__status-icon',
`module-message-detail__contact__status-icon--${contact.status}`
)}
/>
) : null;
return (
<div key={contact.phoneNumber} className="module-message-detail__contact">
{this.renderAvatar(contact)}
<div className="module-message-detail__contact__text">
<div className="module-message-detail__contact__name">
<ContactName
phoneNumber={contact.phoneNumber}
name={contact.name}
profileName={contact.profileName}
shouldShowPubkey={true}
/>
</div>
{errors.map((error, index) => (
<div key={index} className="module-message-detail__contact__error">
{error.message}
</div>
))}
return (
<div key={contact.phoneNumber} className="module-message-detail__contact">
<AvatarItem contact={contact} />
<div className="module-message-detail__contact__text">
<div className="module-message-detail__contact__name">
<ContactName
phoneNumber={contact.phoneNumber}
name={contact.name}
profileName={contact.profileName}
shouldShowPubkey={true}
/>
</div>
{statusComponent}
</div>
);
}
public renderContacts() {
const { contacts } = this.props;
if (!contacts || !contacts.length) {
return null;
}
return (
<div className="module-message-detail__contact-container">
{contacts.map(contact => this.renderContact(contact))}
</div>
);
}
public render() {
const { i18n } = window;
const { errors, message, receivedAt, sentAt } = this.props;
return (
<div className="message-detail-wrapper">
<div className="module-message-detail">
<div className="module-message-detail__message-container">
<Message {...message} />
{errors.map((error, index) => (
<div key={index} className="module-message-detail__contact__error">
{error.message}
</div>
<table className="module-message-detail__info">
<tbody>
{(errors || []).map((error, index) => (
<tr key={index}>
<td className="module-message-detail__label">{i18n('error')}</td>
<td>
{' '}
<span className="error-message">{error.message}</span>{' '}
</td>
</tr>
))}
<tr>
<td className="module-message-detail__label">{i18n('sent')}</td>
))}
</div>
{statusComponent}
</div>
);
};
export const MessageDetail = () => {
const { i18n } = window;
const messageDetailProps = useSelector(getMessageDetailsViewProps);
if (!messageDetailProps) {
return null;
}
const { errors, message, receivedAt, sentAt } = messageDetailProps;
return (
<div className="message-detail-wrapper">
<div className="module-message-detail">
<div className="module-message-detail__message-container">
<Message {...message} />
</div>
<table className="module-message-detail__info">
<tbody>
{(errors || []).map((error, index) => (
<tr key={index}>
<td className="module-message-detail__label">{i18n('error')}</td>
<td>
{moment(sentAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">({sentAt})</span>
{' '}
<span className="error-message">{error.message}</span>{' '}
</td>
</tr>
{receivedAt ? (
<tr>
<td className="module-message-detail__label">{i18n('received')}</td>
<td>
{moment(receivedAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">({receivedAt})</span>
</td>
</tr>
) : null}
))}
<tr>
<td className="module-message-detail__label">{i18n('sent')}</td>
<td>
{moment(sentAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">({sentAt})</span>
</td>
</tr>
{receivedAt ? (
<tr>
<td className="module-message-detail__label">
{message.direction === 'incoming' ? i18n('from') : i18n('to')}
<td className="module-message-detail__label">{i18n('received')}</td>
<td>
{moment(receivedAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">({receivedAt})</span>
</td>
</tr>
</tbody>
</table>
{this.renderContacts()}
{this.renderDeleteButton()}
</div>
) : null}
<tr>
<td className="module-message-detail__label">
{message.direction === 'incoming' ? i18n('from') : i18n('to')}
</td>
</tr>
</tbody>
</table>
<ContactsItem contacts={messageDetailProps.contacts} />
<DeleteButtonItem message={messageDetailProps.message} />
</div>
);
}
}
</div>
);
};

View file

@ -23,6 +23,7 @@ import { SessionMainPanel } from '../SessionMainPanel';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { TimerOptionsArray, TimerOptionsState } from '../../state/ducks/timerOptions';
import { initialConversationScreen } from '../../state/ducks/conversationScreen';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -117,6 +118,7 @@ export class SessionInboxView extends React.Component<any, State> {
timerOptions: {
timerOptions,
},
conversationScreen: initialConversationScreen,
};
this.store = createStore(initialState);

View file

@ -7,18 +7,15 @@ import { SessionCompositionBox, StagedAttachmentType } from './SessionCompositio
import { Constants } from '../../../session';
import _ from 'lodash';
import { AttachmentUtil, GoogleChrome } from '../../../util';
import {
ConversationHeaderNonReduxProps,
ConversationHeaderWithDetails,
} from '../../conversation/ConversationHeader';
import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader';
import { SessionRightPanelWithDetails } from './SessionRightPanel';
import { SessionTheme } from '../../../state/ducks/SessionTheme';
import { DefaultTheme } from 'styled-components';
import { SessionMessagesList } from './SessionMessagesList';
import { SessionMessageListProps, SessionMessagesList } from './SessionMessagesList';
import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
import { AttachmentType, AttachmentTypeWithPath, save } from '../../../types/Attachment';
import { ToastUtils, UserUtils } from '../../../session/utils';
import { ToastUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone';
import {
@ -34,22 +31,15 @@ import { getConversationController } from '../../../session/conversations';
import { getMessageById, getPubkeysInPublicConversation } from '../../../data/data';
import autoBind from 'auto-bind';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { deleteOpenGroupMessages } from '../../../interactions/conversationInteractions';
import { updateMentionsMembers } from '../../../state/ducks/mentionsInput';
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { SessionButtonColor } from '../SessionButton';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { resetSelectedMessageIds } from '../../../state/ducks/conversationScreen';
interface State {
unreadCount: number;
selectedMessages: Array<string>;
showOverlay: boolean;
showRecordingView: boolean;
showOptionsPane: boolean;
// if set, the `More Info` of a message screen is shown on top of the conversation.
messageDetailShowProps?: any; // FIXME set the type for this
stagedAttachments: Array<StagedAttachmentType>;
isDraggingFile: boolean;
@ -73,6 +63,9 @@ interface Props {
selectedConversation?: ReduxConversationType;
theme: DefaultTheme;
messagesProps: Array<SortedMessageModelProps>;
selectedMessages: Array<string>;
showMessageDetails: boolean;
isRightPanelShowing: boolean;
}
export class SessionConversation extends React.Component<Props, State> {
@ -88,10 +81,8 @@ export class SessionConversation extends React.Component<Props, State> {
const unreadCount = this.props.selectedConversation?.unreadCount || 0;
this.state = {
unreadCount,
selectedMessages: [],
showOverlay: false,
showRecordingView: false,
showOptionsPane: false,
stagedAttachments: [],
isDraggingFile: false,
};
@ -155,14 +146,12 @@ export class SessionConversation extends React.Component<Props, State> {
}
if (newConversationKey !== oldConversationKey) {
void this.loadInitialMessages();
window.inboxStore?.dispatch(resetSelectedMessageIds());
this.setState({
showOptionsPane: false,
selectedMessages: [],
showOverlay: false,
showRecordingView: false,
stagedAttachments: [],
isDraggingFile: false,
messageDetailShowProps: undefined,
quotedMessageProps: undefined,
quotedMessageTimestamp: undefined,
});
@ -188,24 +177,28 @@ export class SessionConversation extends React.Component<Props, State> {
public render() {
const {
showRecordingView,
showOptionsPane,
quotedMessageProps,
lightBoxOptions,
selectedMessages,
isDraggingFile,
stagedAttachments,
messageDetailShowProps,
} = this.state;
const selectionMode = !!selectedMessages.length;
const { selectedConversation, selectedConversationKey, messagesProps } = this.props;
const {
selectedConversation,
selectedConversationKey,
messagesProps,
showMessageDetails,
selectedMessages,
isRightPanelShowing,
} = this.props;
if (!selectedConversation || !messagesProps) {
// return an empty message view
return <MessageView />;
}
const selectionMode = selectedMessages.length > 0;
const conversationModel = getConversationController().get(selectedConversationKey);
// TODO VINCE: OPTIMISE FOR NEW SENDING???
const sendMessageFn = (
body: any,
attachments: any,
@ -224,11 +217,12 @@ export class SessionConversation extends React.Component<Props, State> {
.current as any).scrollTop = this.messageContainerRef.current?.scrollHeight;
}
};
const showMessageDetails = !!messageDetailShowProps;
return (
<SessionTheme theme={this.props.theme}>
<div className="conversation-header">{this.renderHeader()}</div>
<div className="conversation-header">
<ConversationHeaderWithDetails />
</div>
<div
// if you change the classname, also update it on onKeyDown
className={classNames('conversation-content', selectionMode && 'selection-mode')}
@ -237,7 +231,7 @@ export class SessionConversation extends React.Component<Props, State> {
role="navigation"
>
<div className={classNames('conversation-info-panel', showMessageDetails && 'show')}>
{showMessageDetails && <MessageDetail {...messageDetailShowProps} />}
<MessageDetail />
</div>
{lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
@ -272,21 +266,15 @@ export class SessionConversation extends React.Component<Props, State> {
theme={this.props.theme}
/>
</div>
<div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}>
<SessionRightPanelWithDetails
{...this.getRightPanelProps()}
isShowing={showOptionsPane}
/>
<div
className={classNames('conversation-item__options-pane', isRightPanelShowing && 'show')}
>
<SessionRightPanelWithDetails {...this.getRightPanelProps()} />
</div>
</SessionTheme>
);
}
public renderHeader() {
const headerProps = this.getHeaderProps();
return <ConversationHeaderWithDetails {...headerProps} />;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ GETTER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -311,236 +299,35 @@ export class SessionConversation extends React.Component<Props, State> {
);
}
public getHeaderProps(): ConversationHeaderNonReduxProps {
console.warn('generating new header props');
public getMessagesListProps(): SessionMessageListProps {
const { messagesProps } = this.props;
const headerProps = {
showBackButton: Boolean(this.state.messageDetailShowProps),
selectionMode: !!this.state.selectedMessages.length,
onDeleteSelectedMessages: this.deleteSelectedMessages,
onCloseOverlay: this.resetSelection,
onAvatarClick: this.toggleRightPanel,
onGoBack: () => {
this.setState({
messageDetailShowProps: undefined,
});
},
};
return headerProps;
}
public getMessagesListProps() {
const { selectedConversation, selectedConversationKey, ourNumber, messagesProps } = this.props;
const { quotedMessageTimestamp, selectedMessages } = this.state;
if (!selectedConversation) {
throw new Error();
}
return {
selectedMessages,
ourPrimary: ourNumber,
conversationKey: selectedConversationKey,
messagesProps,
resetSelection: this.resetSelection,
quotedMessageTimestamp,
conversation: selectedConversation,
selectMessage: this.selectMessage,
deleteMessage: this.deleteMessage,
messageContainerRef: this.messageContainerRef,
replyToMessage: this.replyToMessage,
showMessageDetails: this.showMessageDetails,
onClickAttachment: this.onClickAttachment,
onDownloadAttachment: this.saveAttachment,
messageContainerRef: this.messageContainerRef,
onDeleteSelectedMessages: this.deleteSelectedMessages,
};
}
// tslint:disable-next-line: max-func-body-length
public getRightPanelProps() {
const { selectedConversationKey } = this.props;
const conversation = getConversationController().getOrThrow(selectedConversationKey);
const ourPrimary = window.storage.get('primaryDevicePubKey');
const members = conversation.get('members') || [];
const isAdmin = conversation.isMediumGroup()
? true
: conversation.isPublic()
? conversation.isAdmin(ourPrimary)
: false;
return {
id: conversation.id,
name: conversation.getName(),
memberCount: members.length,
phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(),
avatarPath: conversation.getAvatarPath(),
isKickedFromGroup: Boolean(conversation.get('isKickedFromGroup')),
left: conversation.get('left'),
isGroup: !conversation.isPrivate(),
isPublic: conversation.isPublic(),
isAdmin,
isBlocked: conversation.isBlocked(),
onGoBack: () => {
this.toggleRightPanel();
},
onShowLightBox: (lightBoxOptions?: LightBoxOptions) => {
this.setState({ lightBoxOptions });
},
};
}
public toggleRightPanel() {
const { showOptionsPane } = this.state;
this.setState({ showOptionsPane: !showOptionsPane });
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async deleteMessagesById(messageIds: Array<string>, askUserForConfirmation: boolean) {
// Get message objects
const { selectedConversationKey, selectedConversation, messagesProps } = this.props;
const conversationModel = getConversationController().getOrThrow(selectedConversationKey);
if (!selectedConversation) {
window?.log?.info('No valid selected conversation.');
return;
}
const selectedMessages = messagesProps.filter(message =>
messageIds.find(selectedMessage => selectedMessage === message.propsForMessage.id)
);
const multiple = selectedMessages.length > 1;
// In future, we may be able to unsend private messages also
// isServerDeletable also defined in ConversationHeader.tsx for
// future reference
const isServerDeletable = selectedConversation.isPublic;
const warningMessage = (() => {
if (selectedConversation.isPublic) {
return multiple
? window.i18n('deleteMultiplePublicWarning')
: window.i18n('deletePublicWarning');
}
return multiple ? window.i18n('deleteMultipleWarning') : window.i18n('deleteWarning');
})();
const doDelete = async () => {
let toDeleteLocallyIds: Array<string>;
if (selectedConversation.isPublic) {
// Get our Moderator status
const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourDevicePubkey) {
return;
}
const isAdmin = conversationModel.isAdmin(ourDevicePubkey);
const isAllOurs = selectedMessages.every(
message => ourDevicePubkey === message.propsForMessage.authorPhoneNumber
);
if (!isAllOurs && !isAdmin) {
ToastUtils.pushMessageDeleteForbidden();
this.setState({ selectedMessages: [] });
return;
}
toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversationModel);
if (toDeleteLocallyIds.length === 0) {
// Message failed to delete from server, show error?
return;
}
} else {
toDeleteLocallyIds = selectedMessages.map(pro => pro.propsForMessage.id);
}
await Promise.all(
toDeleteLocallyIds.map(async msgId => {
await conversationModel.removeMessage(msgId);
})
);
// Update view and trigger update
this.setState({ selectedMessages: [] }, ToastUtils.pushDeleted);
};
let title = '';
// Note: keep that i18n logic separated so the scripts in tools/ find the usage of those
if (isServerDeletable) {
if (multiple) {
title = window.i18n('deleteMessagesForEveryone');
} else {
title = window.i18n('deleteMessageForEveryone');
}
} else {
if (multiple) {
title = window.i18n('deleteMessages');
} else {
title = window.i18n('deleteMessage');
}
}
const okText = window.i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
if (askUserForConfirmation) {
const onClickClose = () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
};
window.inboxStore?.dispatch(
updateConfirmModal({
title,
message: warningMessage,
okText,
okTheme: SessionButtonColor.Danger,
onClickOk: doDelete,
onClickClose,
})
);
} else {
void doDelete();
}
}
public async deleteSelectedMessages() {
await this.deleteMessagesById(this.state.selectedMessages, true);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ MESSAGE SELECTION ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public selectMessage(messageId: string) {
const selectedMessages = this.state.selectedMessages.includes(messageId)
? // Add to array if not selected. Else remove.
this.state.selectedMessages.filter(id => id !== messageId)
: [...this.state.selectedMessages, messageId];
this.setState({ selectedMessages });
}
public deleteMessage(messageId: string) {
this.setState({ selectedMessages: [messageId] }, this.deleteSelectedMessages);
}
public resetSelection() {
this.setState({ selectedMessages: [] });
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private onLoadVoiceNoteView() {
this.setState({
showRecordingView: true,
selectedMessages: [],
});
window.inboxStore?.dispatch(resetSelectedMessageIds());
}
private onExitVoiceNoteView() {
@ -582,18 +369,6 @@ export class SessionConversation extends React.Component<Props, State> {
}
}
private showMessageDetails(messageProps: any) {
messageProps.onDeleteMessage = async (id: string) => {
await this.deleteMessagesById([id], false);
this.setState({ messageDetailShowProps: undefined });
};
this.setState({
messageDetailShowProps: messageProps,
showOptionsPane: false,
});
}
private onClickAttachment(attachment: AttachmentTypeWithPath, propsForMessage: PropsForMessage) {
let index = -1;
const media = (propsForMessage.attachments || []).map(attachmentForMedia => {
@ -626,7 +401,7 @@ export class SessionConversation extends React.Component<Props, State> {
if (!messageContainer) {
return;
}
const selectionMode = !!this.state.selectedMessages.length;
const selectionMode = !!this.props.selectedMessages.length;
const recordingMode = this.state.showRecordingView;
const pageHeight = messageContainer.clientHeight;
const arrowScrollPx = 50;
@ -642,7 +417,7 @@ export class SessionConversation extends React.Component<Props, State> {
switch (event.key) {
case 'Escape':
if (selectionMode) {
this.resetSelection();
window.inboxStore?.dispatch(resetSelectedMessageIds());
}
break;
// Scrolling

View file

@ -21,10 +21,17 @@ import { TypingBubble } from '../../conversation/TypingBubble';
import { getConversationController } from '../../../session/conversations';
import { MessageModel } from '../../../models/message';
import { MessageRegularProps, QuoteClickOptions } from '../../../models/messageType';
import { getMessageById, getMessagesBySentAt } from '../../../data/data';
import { getMessagesBySentAt } from '../../../data/data';
import autoBind from 'auto-bind';
import { ConversationTypeEnum } from '../../../models/conversation';
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
import { StateType } from '../../../state/reducer';
import { getSelectedMessageIds } from '../../../state/selectors/conversationScreen';
import { connect } from 'react-redux';
import {
getSelectedConversation,
getSelectedConversationKey,
} from '../../../state/selectors/conversations';
interface State {
showScrollButton: boolean;
@ -32,31 +39,27 @@ interface State {
nextMessageToPlay: number | undefined;
}
interface Props {
type Props = SessionMessageListProps & {
conversationKey?: string;
selectedMessages: Array<string>;
conversationKey: string;
conversation?: ReduxConversationType;
};
export type SessionMessageListProps = {
messagesProps: Array<SortedMessageModelProps>;
conversation: ReduxConversationType;
ourPrimary: string;
messageContainerRef: React.RefObject<any>;
selectMessage: (messageId: string) => void;
deleteMessage: (messageId: string) => void;
replyToMessage: (messageId: number) => Promise<void>;
showMessageDetails: (messageProps: any) => void;
onClickAttachment: (attachment: any, message: any) => void;
onDownloadAttachment: ({
attachment,
messageTimestamp,
}: {
onDownloadAttachment: (toDownload: {
attachment: any;
messageTimestamp: number;
messageSender: string;
}) => void;
onDeleteSelectedMessages: () => Promise<void>;
}
};
export class SessionMessagesList extends React.Component<Props, State> {
private readonly messageContainerRef: React.RefObject<any>;
class SessionMessagesListInner extends React.Component<Props, State> {
private scrollOffsetBottomPx: number = Number.MAX_VALUE;
private ignoreScrollEvents: boolean;
private timeoutResetQuotedScroll: NodeJS.Timeout | null = null;
@ -70,7 +73,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
};
autoBind(this);
this.messageContainerRef = this.props.messageContainerRef;
this.ignoreScrollEvents = true;
}
@ -114,7 +116,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
if (this.getScrollOffsetBottomPx() === 0) {
this.scrollToBottom();
} else {
const messageContainer = this.messageContainerRef?.current;
const messageContainer = this.props.messageContainerRef?.current;
if (messageContainer) {
const scrollHeight = messageContainer.scrollHeight;
@ -132,6 +134,10 @@ export class SessionMessagesList extends React.Component<Props, State> {
const { conversationKey, conversation } = this.props;
const { showScrollButton } = this.state;
if (!conversationKey || !conversation) {
return null;
}
let displayedName = null;
if (conversation.type === ConversationTypeEnum.PRIVATE) {
displayedName = getConversationController().getContactProfileNameOrShortenedPubKey(
@ -143,7 +149,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
<div
className="messages-container"
onScroll={this.handleScroll}
ref={this.messageContainerRef}
ref={this.props.messageContainerRef}
>
<TypingBubble
phoneNumber={conversationKey}
@ -166,6 +172,9 @@ export class SessionMessagesList extends React.Component<Props, State> {
private displayUnreadBannerIndex(messages: Array<SortedMessageModelProps>) {
const { conversation } = this.props;
if (!conversation) {
return -1;
}
if (conversation.unreadCount === 0) {
return -1;
}
@ -309,20 +318,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
) {
const messageId = messageProps.propsForMessage.id;
const selected =
!!messageProps?.propsForMessage.id && this.props.selectedMessages.includes(messageId);
const onShowDetail = async () => {
const found = await getMessageById(messageId);
if (found) {
const messageDetailsProps = await found.getPropsForMessageDetail();
this.props.showMessageDetails(messageDetailsProps);
} else {
window.log.warn(`Message ${messageId} not found in db`);
}
};
const onClickAttachment = (attachment: AttachmentType) => {
this.props.onClickAttachment(attachment, messageProps.propsForMessage);
};
@ -347,16 +342,12 @@ export class SessionMessagesList extends React.Component<Props, State> {
const regularProps: MessageRegularProps = {
...messageProps.propsForMessage,
selected,
firstMessageOfSeries,
multiSelectMode,
isQuotedMessageToAnimate: messageId === this.state.animateQuotedMessageId,
nextMessageToPlay: this.state.nextMessageToPlay,
playableMessageIndex,
onSelectMessage: this.props.selectMessage,
onDeleteMessage: this.props.deleteMessage,
onReply: this.props.replyToMessage,
onShowDetail,
onClickAttachment,
onDownload,
playNextMessage: this.playNextMessage,
@ -372,7 +363,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
private updateReadMessages() {
const { messagesProps, conversationKey } = this.props;
if (!messagesProps || messagesProps.length === 0) {
if (!messagesProps || messagesProps.length === 0 || !conversationKey) {
return;
}
@ -387,7 +378,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
if (this.getScrollOffsetBottomPx() === 0) {
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt);
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
}
}
@ -426,10 +417,10 @@ export class SessionMessagesList extends React.Component<Props, State> {
// ~~~~~~~~~~~~ SCROLLING METHODS ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private async handleScroll() {
const messageContainer = this.messageContainerRef?.current;
const messageContainer = this.props.messageContainerRef?.current;
const { conversationKey } = this.props;
if (!messageContainer) {
if (!messageContainer || !conversationKey) {
return;
}
contextMenu.hideAll();
@ -483,6 +474,9 @@ export class SessionMessagesList extends React.Component<Props, State> {
private scrollToUnread() {
const { messagesProps, conversation } = this.props;
if (!conversation) {
return;
}
if (conversation.unreadCount > 0) {
let message;
if (messagesProps.length > conversation.unreadCount) {
@ -542,7 +536,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
);
}
const messageContainer = this.messageContainerRef.current;
const messageContainer = this.props.messageContainerRef.current;
if (!messageContainer) {
return;
}
@ -556,19 +550,19 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
private scrollToBottom() {
const messageContainer = this.messageContainerRef.current;
const messageContainer = this.props.messageContainerRef.current;
if (!messageContainer) {
return;
}
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
const { messagesProps, conversationKey } = this.props;
if (!messagesProps || messagesProps.length === 0) {
if (!messagesProps || messagesProps.length === 0 || !conversationKey) {
return;
}
const conversation = getConversationController().getOrThrow(conversationKey);
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt);
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
}
private async scrollToQuoteMessage(options: QuoteClickOptions) {
@ -622,7 +616,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
// basically the offset in px from the bottom of the view (most recent message)
private getScrollOffsetBottomPx() {
const messageContainer = this.messageContainerRef?.current;
const messageContainer = this.props.messageContainerRef?.current;
if (!messageContainer) {
return Number.MAX_VALUE;
@ -634,3 +628,15 @@ export class SessionMessagesList extends React.Component<Props, State> {
return scrollHeight - scrollTop - clientHeight;
}
}
const mapStateToProps = (state: StateType) => {
return {
selectedMessages: getSelectedMessageIds(state),
conversationKey: getSelectedConversationKey(state),
conversation: getSelectedConversation(state),
};
};
const smart = connect(mapStateToProps);
export const SessionMessagesList = smart(SessionMessagesListInner);

View file

@ -36,26 +36,16 @@ import { ItemClickEvent } from '../../conversation/media-gallery/types/ItemClick
import { MediaItemType } from '../../LightboxGallery';
// tslint:disable-next-line: no-submodule-imports
import useInterval from 'react-use/lib/useInterval';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getTimerOptions } from '../../../state/selectors/timerOptions';
import { closeRightPanel } from '../../../state/ducks/conversationScreen';
import { isRightPanelShowing } from '../../../state/selectors/conversationScreen';
import { getSelectedConversation } from '../../../state/selectors/conversations';
import { useMembersAvatars } from '../../../hooks/useMembersAvatar';
type Props = {
id: string;
name?: string;
profileName?: string;
phoneNumber: string;
memberCount: number;
avatarPath: string | null;
isPublic: boolean;
isAdmin: boolean;
isKickedFromGroup: boolean;
left: boolean;
isBlocked: boolean;
isShowing: boolean;
isGroup: boolean;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onGoBack: () => void;
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void;
};
@ -187,40 +177,109 @@ async function getMediaGalleryProps(
};
}
const HeaderItem = () => {
const selectedConversation = useSelector(getSelectedConversation);
const theme = useTheme();
const dispatch = useDispatch();
const memberDetails = useMembersAvatars(selectedConversation);
if (!selectedConversation) {
return null;
}
const {
avatarPath,
isPublic,
id,
weAreAdmin,
isKickedFromGroup,
profileName,
phoneNumber,
isBlocked,
left,
name,
} = selectedConversation;
const showInviteContacts = (isPublic || weAreAdmin) && !isKickedFromGroup && !isBlocked && !left;
const userName = name || profileName || phoneNumber;
return (
<div className="group-settings-header">
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Medium}
iconRotation={270}
onClick={() => {
dispatch(closeRightPanel());
}}
theme={theme}
/>
<Avatar
avatarPath={avatarPath || ''}
name={userName}
size={AvatarSize.XL}
memberAvatars={memberDetails}
pubkey={id}
/>
<div className="invite-friends-container">
{showInviteContacts && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={() => {
if (selectedConversation) {
showInviteContactByConvoId(selectedConversation.id);
}
}}
theme={theme}
/>
)}
</div>
</div>
);
};
// tslint:disable: cyclomatic-complexity
// tslint:disable: max-func-body-length
export const SessionRightPanelWithDetails = (props: Props) => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
const [onItemClick, setOnItemClick] = useState<any>(undefined);
const theme = useTheme();
const selectedConversation = useSelector(getSelectedConversation);
const isShowing = useSelector(isRightPanelShowing);
console.warn('props', props);
useEffect(() => {
let isRunning = true;
if (props.isShowing) {
void getMediaGalleryProps(props.id, media, props.onShowLightBox).then(results => {
console.warn('results2', results);
if (isShowing && selectedConversation) {
void getMediaGalleryProps(selectedConversation.id, media, props.onShowLightBox).then(
results => {
console.warn('results2', results);
if (isRunning) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
if (isRunning) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
}
}
});
);
}
return () => {
isRunning = false;
return;
};
}, [props.isShowing, props.id]);
}, [isShowing, selectedConversation?.id]);
useInterval(async () => {
if (props.isShowing) {
const results = await getMediaGalleryProps(props.id, media, props.onShowLightBox);
if (isShowing && selectedConversation) {
const results = await getMediaGalleryProps(
selectedConversation.id,
media,
props.onShowLightBox
);
console.warn('results', results);
if (results.documents.length !== documents.length || results.media.length !== media.length) {
setDocuments(results.documents);
@ -230,56 +289,22 @@ export const SessionRightPanelWithDetails = (props: Props) => {
}
}, 10000);
function renderHeader() {
const { memberAvatars, onGoBack, avatarPath, profileName, phoneNumber } = props;
const showInviteContacts = (isPublic || isAdmin) && !isKickedFromGroup && !isBlocked && !left;
const userName = name || profileName || phoneNumber;
return (
<div className="group-settings-header">
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Medium}
iconRotation={270}
onClick={onGoBack}
theme={theme}
/>
<Avatar
avatarPath={avatarPath || ''}
name={userName}
size={AvatarSize.XL}
memberAvatars={memberAvatars}
pubkey={id}
/>
<div className="invite-friends-container">
{showInviteContacts && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={() => {
showInviteContactByConvoId(props.id);
}}
theme={theme}
/>
)}
</div>
</div>
);
if (!selectedConversation) {
return null;
}
const {
id,
memberCount,
subscriberCount,
name,
isKickedFromGroup,
left,
isPublic,
isAdmin,
weAreAdmin,
isBlocked,
isGroup,
} = props;
const showMemberCount = !!(memberCount && memberCount > 0);
} = selectedConversation;
const showMemberCount = !!(subscriberCount && subscriberCount > 0);
const commonNoShow = isKickedFromGroup || left || isBlocked;
const hasDisappearingMessages = !isPublic && !commonNoShow;
const leaveGroupString = isPublic
@ -301,8 +326,8 @@ export const SessionRightPanelWithDetails = (props: Props) => {
};
});
const showUpdateGroupNameButton = isAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
const showUpdateGroupNameButton = weAreAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = weAreAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
@ -316,13 +341,13 @@ export const SessionRightPanelWithDetails = (props: Props) => {
console.warn('onItemClick', onItemClick);
return (
<div className="group-settings">
{renderHeader()}
<HeaderItem />
<h2>{name}</h2>
{showMemberCount && (
<>
<SpacerLG />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
{window.i18n('members', subscriberCount)}
</div>
<SpacerLG />
</>

View file

@ -28,6 +28,7 @@ import {
} from '../state/ducks/modalDialog';
import {
createOrUpdateItem,
getMessageById,
lastAvatarUploadTimestamp,
removeAllMessagesInConversation,
} from '../data/data';
@ -38,6 +39,7 @@ import { FSv2 } from '../fileserver';
import { fromBase64ToArray, toHex } from '../session/utils/String';
import { SessionButtonColor } from '../components/session/SessionButton';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { resetSelectedMessageIds } from '../state/ducks/conversationScreen';
export const getCompleteUrlForV2ConvoId = async (convoId: string) => {
if (convoId.match(openGroupV2ConversationIdRegex)) {
@ -86,8 +88,8 @@ export async function copyPublicKeyByConvoId(convoId: string) {
* @param messages the list of MessageModel to delete
* @param convo the conversation to delete from (only v2 opengroups are supported)
*/
export async function deleteOpenGroupMessages(
messages: Array<SortedMessageModelProps>,
async function deleteOpenGroupMessages(
messages: Array<MessageModel>,
convo: ConversationModel
): Promise<Array<string>> {
if (!convo.isPublic()) {
@ -100,13 +102,13 @@ export async function deleteOpenGroupMessages(
// so logic here is to delete each messages and get which one where not removed
const validServerIdsToRemove = _.compact(
messages.map(msg => {
return msg.propsForMessage.serverId;
return msg.get('serverId');
})
);
const validMessageModelsToRemove = _.compact(
messages.map(msg => {
const serverId = msg.propsForMessage.serverId;
const serverId = msg.get('serverId');
if (serverId) {
return msg;
}
@ -124,7 +126,7 @@ export async function deleteOpenGroupMessages(
// remove only the messages we managed to remove on the server
if (allMessagesAreDeleted) {
window?.log?.info('Removed all those serverIds messages successfully');
return validMessageModelsToRemove.map(m => m.propsForMessage.id);
return validMessageModelsToRemove.map(m => m.id as string);
} else {
window?.log?.info(
'failed to remove all those serverIds message. not removing them locally neither'
@ -429,3 +431,105 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
);
}
}
export async function deleteMessagesById(
messageIds: Array<string>,
conversationId: string,
askUserForConfirmation: boolean
) {
const conversationModel = getConversationController().getOrThrow(conversationId);
const selectedMessages = _.compact(await Promise.all(messageIds.map(getMessageById)));
const moreThanOne = selectedMessages.length > 1;
// In future, we may be able to unsend private messages also
// isServerDeletable also defined in ConversationHeader.tsx for
// future reference
const isServerDeletable = conversationModel.isPublic();
const doDelete = async () => {
let toDeleteLocallyIds: Array<string>;
if (isServerDeletable) {
// Get our Moderator status
const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourDevicePubkey) {
return;
}
const isAdmin = conversationModel.isAdmin(ourDevicePubkey);
const isAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource());
if (!isAllOurs && !isAdmin) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversationModel);
if (toDeleteLocallyIds.length === 0) {
// Message failed to delete from server, show error?
return;
}
} else {
toDeleteLocallyIds = selectedMessages.map(m => m.id as string);
}
await Promise.all(
toDeleteLocallyIds.map(async msgId => {
await conversationModel.removeMessage(msgId);
})
);
// Update view and trigger update
window.inboxStore?.dispatch(resetSelectedMessageIds());
ToastUtils.pushDeleted();
};
if (askUserForConfirmation) {
let title = '';
// Note: keep that i18n logic separated so the scripts in tools/ find the usage of those
if (isServerDeletable) {
if (moreThanOne) {
title = window.i18n('deleteMessagesForEveryone');
} else {
title = window.i18n('deleteMessageForEveryone');
}
} else {
if (moreThanOne) {
title = window.i18n('deleteMessages');
} else {
title = window.i18n('deleteMessage');
}
}
const okText = window.i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
const onClickClose = () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
};
const warningMessage = (() => {
if (isServerDeletable) {
return moreThanOne
? window.i18n('deleteMultiplePublicWarning')
: window.i18n('deletePublicWarning');
}
return moreThanOne ? window.i18n('deleteMultipleWarning') : window.i18n('deleteWarning');
})();
window.inboxStore?.dispatch(
updateConfirmModal({
title,
message: warningMessage,
okText,
okTheme: SessionButtonColor.Danger,
onClickOk: doDelete,
onClickClose,
})
);
} else {
void doDelete();
}
}

View file

@ -169,7 +169,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public updateLastMessage: () => any;
public throttledBumpTyping: any;
public throttledNotify: any;
public markRead: (newestUnreadDate: number, providedOptions: any) => Promise<void>;
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
public initialPromise: any;
private typingRefreshTimer?: NodeJS.Timeout | null;

View file

@ -1,7 +1,7 @@
import Backbone from 'backbone';
// tslint:disable-next-line: match-default-export-name
import filesize from 'filesize';
import _ from 'lodash';
import _, { noop } from 'lodash';
import { SignalService } from '../../ts/protobuf';
import { getMessageQueue, Utils } from '../../ts/session';
import { getConversationController } from '../../ts/session/conversations';
@ -765,16 +765,23 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
contact => `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}`
);
const toRet: MessagePropsDetails = {
sentAt: this.get('sent_at'),
receivedAt: this.get('received_at'),
sentAt: this.get('sent_at') || 0,
receivedAt: this.get('received_at') || 0,
message: {
...this.getPropsForMessage(),
disableMenu: true,
// To ensure that group avatar doesn't show up
conversationType: ConversationTypeEnum.PRIVATE,
multiSelectMode: false,
firstMessageOfSeries: false,
onClickAttachment: noop,
onReply: noop,
onDownload: noop,
// tslint:disable-next-line: no-async-without-await no-empty
onQuoteClick: async () => {},
},
errors,
contacts: sortedContacts,
contacts: sortedContacts || [],
};
return toRet;

View file

@ -238,7 +238,6 @@ export interface MessageRegularProps {
convoId: string;
isPublic?: boolean;
isOpenGroupV2?: boolean;
selected: boolean;
isKickedFromGroup: boolean;
// whether or not to show check boxes
multiSelectMode: boolean;
@ -248,11 +247,8 @@ export interface MessageRegularProps {
isTrustedForAttachmentDownload: boolean;
onClickAttachment: (attachment: AttachmentType) => void;
onSelectMessage: (messageId: string) => void;
onReply: (messagId: number) => void;
onDownload: (attachment: AttachmentType) => void;
onDeleteMessage: (messageId: string) => void;
onShowDetail: () => void;
onQuoteClick: (options: QuoteClickOptions) => Promise<void>;
playableMessageIndex?: number;

View file

@ -223,7 +223,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
const updates: any = {
name: details.name,
members: details.members,
color: details.color,
type: 'group',
is_medium_group: true,
};

View file

@ -0,0 +1,79 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { MessagePropsDetails } from './conversations';
export type ConversationScreenState = {
messageDetailProps: MessagePropsDetails | undefined;
showRightPanel: boolean;
selectedMessageIds: Array<string>;
};
export const initialConversationScreen: ConversationScreenState = {
messageDetailProps: undefined,
showRightPanel: false,
selectedMessageIds: [],
};
/**
* This slice is the one holding the layout of the Conversation Screen of the app
*/
const conversationScreenSlice = createSlice({
name: 'conversationScreen',
initialState: initialConversationScreen,
reducers: {
showMessageDetailsView(
state: ConversationScreenState,
action: PayloadAction<MessagePropsDetails>
) {
// force the right panel to be hidden when showing message detail view
return { ...state, messageDetailProps: action.payload, showRightPanel: false };
},
closeMessageDetailsView(state: ConversationScreenState) {
return { ...state, messageDetailProps: undefined };
},
openRightPanel(state: ConversationScreenState) {
return { ...state, showRightPanel: true };
},
closeRightPanel(state: ConversationScreenState) {
return { ...state, showRightPanel: false };
},
addMessageIdToSelection(state: ConversationScreenState, action: PayloadAction<string>) {
if (state.selectedMessageIds.some(id => id === action.payload)) {
return state;
}
return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] };
},
removeMessageIdFromSelection(state: ConversationScreenState, action: PayloadAction<string>) {
const index = state.selectedMessageIds.findIndex(id => id === action.payload);
if (index === -1) {
return state;
}
return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) };
},
toggleSelectedMessageId(state: ConversationScreenState, action: PayloadAction<string>) {
const index = state.selectedMessageIds.findIndex(id => id === action.payload);
if (index === -1) {
return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] };
}
return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) };
},
resetSelectedMessageIds(state: ConversationScreenState) {
return { ...state, selectedMessageIds: [] };
},
},
});
// destructures
const { actions, reducer } = conversationScreenSlice;
export const {
showMessageDetailsView,
closeMessageDetailsView,
openRightPanel,
closeRightPanel,
addMessageIdToSelection,
resetSelectedMessageIds,
} = actions;
export const defaultConversationScreenReducer = reducer;

View file

@ -12,6 +12,7 @@ import {
import {
MessageDeliveryStatus,
MessageModelType,
MessageRegularProps,
PropsForDataExtractionNotification,
} from '../../models/messageType';
import { NotificationForConvoOption } from '../../components/conversation/ConversationHeader';
@ -25,7 +26,25 @@ export type MessageModelProps = {
propsForGroupNotification: PropsForGroupUpdate | null;
};
export type MessagePropsDetails = {};
export type ContactPropsMessageDetail = {
status: string | null;
phoneNumber: string;
name?: string | null;
profileName?: string | null;
avatarPath?: string | null;
isOutgoingKeyError: boolean;
errors?: Array<Error>;
};
export type MessagePropsDetails = {
sentAt: number;
receivedAt: number;
message: MessageRegularProps;
errors: Array<Error>;
contacts: Array<ContactPropsMessageDetail>;
};
export type LastMessageStatusType = MessageDeliveryStatus | null;

View file

@ -10,6 +10,10 @@ import {
defaultMentionsInputReducer as mentionsInput,
MentionsInputState,
} from './ducks/mentionsInput';
import {
ConversationScreenState,
defaultConversationScreenReducer as conversationScreen,
} from './ducks/conversationScreen';
import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
@ -27,6 +31,7 @@ export type StateType = {
modals: ModalState;
userConfig: UserConfigState;
timerOptions: TimerOptionsState;
conversationScreen: ConversationScreenState;
};
export const reducers = {
@ -41,6 +46,7 @@ export const reducers = {
modals,
userConfig,
timerOptions,
conversationScreen,
};
// Making this work would require that our reducer signature supported AnyAction, not

View file

@ -0,0 +1,38 @@
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import { ConversationScreenState } from '../ducks/conversationScreen';
import { MessagePropsDetails } from '../ducks/conversations';
export const getConversationScreenState = (state: StateType): ConversationScreenState =>
state.conversationScreen;
export const isMessageDetailView = createSelector(
getConversationScreenState,
(state: ConversationScreenState): boolean => state.messageDetailProps !== undefined
);
export const getMessageDetailsViewProps = createSelector(
getConversationScreenState,
(state: ConversationScreenState): MessagePropsDetails | undefined => state.messageDetailProps
);
export const isRightPanelShowing = createSelector(
getConversationScreenState,
(state: ConversationScreenState): boolean => state.showRightPanel
);
export const isMessageSelectionMode = createSelector(
getConversationScreenState,
(state: ConversationScreenState): boolean => state.selectedMessageIds.length > 0
);
export const getSelectedMessageIds = createSelector(
getConversationScreenState,
(state: ConversationScreenState): Array<string> => state.selectedMessageIds
);
export const isMessageSelected = (messageId: string) =>
createSelector(getConversationScreenState, (state: ConversationScreenState): boolean =>
state.selectedMessageIds.includes(messageId)
);

View file

@ -9,6 +9,11 @@ import {
getSelectedConversationKey,
} from '../selectors/conversations';
import { getOurNumber } from '../selectors/user';
import {
getSelectedMessageIds,
isMessageDetailView,
isRightPanelShowing,
} from '../selectors/conversationScreen';
const mapStateToProps = (state: StateType) => {
return {
@ -17,6 +22,9 @@ const mapStateToProps = (state: StateType) => {
theme: getTheme(state),
messagesProps: getMessagesOfSelectedConversation(state),
ourNumber: getOurNumber(state),
showMessageDetails: isMessageDetailView(state),
isRightPanelShowing: isRightPanelShowing(state),
selectedMessages: getSelectedMessageIds(state),
};
};