mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
cleanup SessionMessageList p1
This commit is contained in:
parent
399041c5b3
commit
63b81b4c8e
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
79
ts/state/ducks/conversationScreen.tsx
Normal file
79
ts/state/ducks/conversationScreen.tsx
Normal 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;
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
38
ts/state/selectors/conversationScreen.ts
Normal file
38
ts/state/selectors/conversationScreen.ts
Normal 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)
|
||||
);
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue