Moves message details into React pane land
This commit is contained in:
parent
3def746014
commit
a80c6d89a8
20 changed files with 501 additions and 558 deletions
|
@ -1460,9 +1460,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
// Send Escape to active conversation so it can close panels
|
||||
if (conversation && key === 'Escape') {
|
||||
window.reduxActions.conversations.popPanelForConversation(
|
||||
conversation.id
|
||||
);
|
||||
window.reduxActions.conversations.popPanelForConversation();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
|
@ -1536,12 +1534,9 @@ export async function startApp(): Promise<void> {
|
|||
shiftKey &&
|
||||
(key === 'm' || key === 'M')
|
||||
) {
|
||||
window.reduxActions.conversations.pushPanelForConversation(
|
||||
conversation.id,
|
||||
{
|
||||
type: PanelType.AllMedia,
|
||||
}
|
||||
);
|
||||
window.reduxActions.conversations.pushPanelForConversation({
|
||||
type: PanelType.AllMedia,
|
||||
});
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
|
@ -1634,14 +1629,12 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
window.reduxActions.conversations.pushPanelForConversation(
|
||||
conversation.id,
|
||||
{
|
||||
type: PanelType.MessageDetails,
|
||||
args: { messageId: selectedMessage },
|
||||
}
|
||||
);
|
||||
|
||||
window.reduxActions.conversations.pushPanelForConversation({
|
||||
type: PanelType.MessageDetails,
|
||||
args: {
|
||||
messageId: selectedMessage,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -462,7 +462,7 @@ export function CompositionArea({
|
|||
recentStickers={recentStickers}
|
||||
clearInstalledStickerPack={clearInstalledStickerPack}
|
||||
onClickAddPack={() =>
|
||||
pushPanelForConversation(conversationId, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.StickerManager,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -154,12 +154,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
private renderBackButton(): ReactNode {
|
||||
const { i18n, id, popPanelForConversation, showBackButton } = this.props;
|
||||
const { i18n, popPanelForConversation, showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => popPanelForConversation(id)}
|
||||
onClick={popPanelForConversation}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__back-icon',
|
||||
showBackButton ? 'module-ConversationHeader__back-icon--show' : null
|
||||
|
@ -474,7 +474,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation(id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
})
|
||||
}
|
||||
|
@ -487,16 +487,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{isGroup && !hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation(id, { type: PanelType.GroupV1Members })
|
||||
pushPanelForConversation({ type: PanelType.GroupV1Members })
|
||||
}
|
||||
>
|
||||
{i18n('showMembers')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation(id, { type: PanelType.AllMedia })
|
||||
}
|
||||
onClick={() => pushPanelForConversation({ type: PanelType.AllMedia })}
|
||||
>
|
||||
{i18n('viewRecentMedia')}
|
||||
</MenuItem>
|
||||
|
@ -565,13 +563,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
private renderHeader(): ReactNode {
|
||||
const {
|
||||
conversationTitle,
|
||||
id,
|
||||
groupVersion,
|
||||
pushPanelForConversation,
|
||||
type,
|
||||
} = this.props;
|
||||
const { conversationTitle, groupVersion, pushPanelForConversation, type } =
|
||||
this.props;
|
||||
|
||||
if (conversationTitle !== undefined) {
|
||||
return (
|
||||
|
@ -589,14 +582,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
switch (type) {
|
||||
case 'direct':
|
||||
onClick = () => {
|
||||
pushPanelForConversation(id, { type: PanelType.ConversationDetails });
|
||||
pushPanelForConversation({ type: PanelType.ConversationDetails });
|
||||
};
|
||||
break;
|
||||
case 'group': {
|
||||
const hasGV2AdminEnabled = groupVersion === 2;
|
||||
onClick = hasGV2AdminEnabled
|
||||
? () => {
|
||||
pushPanelForConversation(id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ export type AudioAttachmentProps = {
|
|||
id: string;
|
||||
conversationId: string;
|
||||
played: boolean;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
timestamp: number;
|
||||
|
@ -750,27 +751,25 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const {
|
||||
conversationId,
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
status,
|
||||
i18n,
|
||||
pushPanelForConversation,
|
||||
status,
|
||||
text,
|
||||
textAttachment,
|
||||
timestamp,
|
||||
id,
|
||||
} = this.props;
|
||||
|
||||
const isStickerLike = isSticker || this.canRenderStickerLikeEmoji();
|
||||
|
||||
return (
|
||||
<MessageMetadata
|
||||
conversationId={conversationId}
|
||||
deletedForEveryone={deletedForEveryone}
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
|
@ -827,23 +826,24 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
public renderAttachment(): JSX.Element | null {
|
||||
const {
|
||||
attachments,
|
||||
conversationId,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
i18n,
|
||||
id,
|
||||
conversationId,
|
||||
isSticker,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
pushPanelForConversation,
|
||||
quote,
|
||||
readStatus,
|
||||
reducedMotion,
|
||||
renderAudioAttachment,
|
||||
renderingContext,
|
||||
showLightbox,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
showLightbox,
|
||||
status,
|
||||
text,
|
||||
textAttachment,
|
||||
|
@ -966,6 +966,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
id,
|
||||
conversationId,
|
||||
played,
|
||||
pushPanelForConversation,
|
||||
status,
|
||||
textPending: textAttachment?.pending,
|
||||
timestamp,
|
||||
|
@ -1562,7 +1563,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
public renderEmbeddedContact(): JSX.Element | null {
|
||||
const {
|
||||
contact,
|
||||
conversationId,
|
||||
conversationType,
|
||||
direction,
|
||||
i18n,
|
||||
|
@ -1598,7 +1598,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
: undefined;
|
||||
|
||||
pushPanelForConversation(conversationId, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ContactDetails,
|
||||
args: {
|
||||
contact,
|
||||
|
@ -2243,7 +2243,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
attachments,
|
||||
contact,
|
||||
conversationId,
|
||||
showLightboxForViewOnceMedia,
|
||||
direction,
|
||||
giftBadge,
|
||||
|
@ -2381,7 +2380,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
uuid: contact.uuid,
|
||||
}
|
||||
: undefined;
|
||||
pushPanelForConversation(conversationId, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ContactDetails,
|
||||
args: {
|
||||
contact,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { animated, useSpring } from '@react-spring/web';
|
|||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
|
||||
|
@ -16,7 +17,6 @@ import type { ComputePeaksResult } from '../GlobalAudioContext';
|
|||
import { MessageMetadata } from './MessageMetadata';
|
||||
import * as log from '../../logging/log';
|
||||
import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
active: ActiveAudioPlayerStateType | undefined;
|
||||
|
@ -592,7 +592,6 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
|
||||
{!withContentBelow && !collapseMetadata && (
|
||||
<MessageMetadata
|
||||
conversationId={conversationId}
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
|
|
|
@ -8,14 +8,13 @@ import Measure from 'react-measure';
|
|||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
import { ExpireTimer } from './ExpireTimer';
|
||||
import { MessageTimestamp } from './MessageTimestamp';
|
||||
import { Spinner } from '../Spinner';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
type PropsType = {
|
||||
conversationId: string;
|
||||
deletedForEveryone?: boolean;
|
||||
direction: DirectionType;
|
||||
expirationLength?: number;
|
||||
|
@ -35,7 +34,6 @@ type PropsType = {
|
|||
};
|
||||
|
||||
export function MessageMetadata({
|
||||
conversationId,
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
expirationLength,
|
||||
|
@ -80,7 +78,7 @@ export function MessageMetadata({
|
|||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
pushPanelForConversation(conversationId, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.MessageDetails,
|
||||
args: { messageId: id },
|
||||
});
|
||||
|
|
|
@ -23,12 +23,12 @@ import type {
|
|||
PropsData as MessagePropsData,
|
||||
PropsHousekeeping,
|
||||
} from './Message';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
||||
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||
|
||||
export type PropsData = {
|
||||
canDownload: boolean;
|
||||
|
@ -46,8 +46,8 @@ export type PropsActions = {
|
|||
messageId: string;
|
||||
}) => void;
|
||||
deleteMessageForEveryone: (id: string) => void;
|
||||
toggleForwardMessageModal: (id: string) => void;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
toggleForwardMessageModal: (id: string) => void;
|
||||
reactToMessage: (
|
||||
id: string,
|
||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||
|
@ -75,42 +75,42 @@ type Trigger = {
|
|||
*/
|
||||
export function TimelineMessage(props: Props): JSX.Element {
|
||||
const {
|
||||
i18n,
|
||||
id,
|
||||
author,
|
||||
attachments,
|
||||
author,
|
||||
canDeleteForEveryone,
|
||||
canDownload,
|
||||
canReact,
|
||||
canReply,
|
||||
canRetry,
|
||||
canDeleteForEveryone,
|
||||
canRetryDeleteForEveryone,
|
||||
contact,
|
||||
payment,
|
||||
conversationId,
|
||||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
deletedForEveryone,
|
||||
conversationId,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
giftBadge,
|
||||
i18n,
|
||||
id,
|
||||
isSelected,
|
||||
isSticker,
|
||||
isTapToView,
|
||||
kickOffAttachmentDownload,
|
||||
payment,
|
||||
pushPanelForConversation,
|
||||
reactToMessage,
|
||||
setQuoteByMessageId,
|
||||
renderReactionPicker,
|
||||
renderEmojiPicker,
|
||||
retryMessageSend,
|
||||
renderReactionPicker,
|
||||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
selectedReaction,
|
||||
toggleForwardMessageModal,
|
||||
setQuoteByMessageId,
|
||||
text,
|
||||
timestamp,
|
||||
kickOffAttachmentDownload,
|
||||
saveAttachment,
|
||||
toggleForwardMessageModal,
|
||||
} = props;
|
||||
|
||||
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
||||
|
@ -410,7 +410,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
|
||||
}
|
||||
onMoreInfo={() =>
|
||||
pushPanelForConversation(conversationId, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.MessageDetails,
|
||||
args: { messageId: id },
|
||||
})
|
||||
|
|
|
@ -441,7 +441,7 @@ export function ConversationDetails({
|
|||
}
|
||||
label={i18n('showChatColorEditor')}
|
||||
onClick={() => {
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ChatColorEditor,
|
||||
});
|
||||
}}
|
||||
|
@ -464,7 +464,7 @@ export function ConversationDetails({
|
|||
}
|
||||
label={i18n('ConversationDetails--notifications')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.NotificationSettings,
|
||||
})
|
||||
}
|
||||
|
@ -520,7 +520,7 @@ export function ConversationDetails({
|
|||
}
|
||||
label={i18n('ConversationDetails--group-link')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupLinkManagement,
|
||||
})
|
||||
}
|
||||
|
@ -536,7 +536,7 @@ export function ConversationDetails({
|
|||
}
|
||||
label={i18n('ConversationDetails--requests-and-invites')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupInvites,
|
||||
})
|
||||
}
|
||||
|
@ -552,7 +552,7 @@ export function ConversationDetails({
|
|||
}
|
||||
label={i18n('permissions')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupPermissions,
|
||||
})
|
||||
}
|
||||
|
@ -566,7 +566,7 @@ export function ConversationDetails({
|
|||
i18n={i18n}
|
||||
loadRecentMediaItems={loadRecentMediaItems}
|
||||
showAllMedia={() =>
|
||||
pushPanelForConversation(conversation.id, {
|
||||
pushPanelForConversation({
|
||||
type: PanelType.AllMedia,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
groupBy,
|
||||
difference,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
|
@ -14,7 +13,6 @@ import {
|
|||
omit,
|
||||
partition,
|
||||
pick,
|
||||
reject,
|
||||
union,
|
||||
without,
|
||||
} from 'lodash';
|
||||
|
@ -43,7 +41,6 @@ import { drop } from '../util/drop';
|
|||
import { dropNull } from '../util/dropNull';
|
||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||
import type { ConversationModel } from './conversations';
|
||||
import type { Contact as SmartMessageDetailContact } from '../state/smart/MessageDetail';
|
||||
import { getCallingNotificationText } from '../util/callingNotification';
|
||||
import type {
|
||||
ProcessedDataMessage,
|
||||
|
@ -51,7 +48,6 @@ import type {
|
|||
ProcessedUnidentifiedDeliveryStatus,
|
||||
CallbackResultType,
|
||||
} from '../textsecure/Types.d';
|
||||
import type { Props as PropsForMessageDetails } from '../components/conversation/MessageDetail';
|
||||
import { SendMessageProtoError } from '../textsecure/Errors';
|
||||
import * as expirationTimer from '../util/expirationTimer';
|
||||
import { getUserLanguages } from '../util/userLanguages';
|
||||
|
@ -76,7 +72,6 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
|
|||
import {
|
||||
SendActionType,
|
||||
SendStatus,
|
||||
isMessageJustForMe,
|
||||
isSent,
|
||||
sendStateReducer,
|
||||
someSendStatus,
|
||||
|
@ -100,7 +95,6 @@ import {
|
|||
getAttachmentsForMessage,
|
||||
getMessagePropStatus,
|
||||
getPropsForCallHistory,
|
||||
getPropsForMessage,
|
||||
hasErrors,
|
||||
isCallHistory,
|
||||
isChatSessionRefreshed,
|
||||
|
@ -129,8 +123,6 @@ import {
|
|||
getCallSelector,
|
||||
getActiveCall,
|
||||
} from '../state/selectors/calling';
|
||||
import { getAccountSelector } from '../state/selectors/accounts';
|
||||
import { getContactNameColorSelector } from '../state/selectors/conversations';
|
||||
import {
|
||||
MessageReceipts,
|
||||
MessageReceiptType,
|
||||
|
@ -263,11 +255,6 @@ async function shouldReplyNotifyUser(
|
|||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
export type MinimalPropsForMessageDetails = Pick<
|
||||
PropsForMessageDetails,
|
||||
'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
|
||||
>;
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Message: TypedMessage } = window.Signal.Types;
|
||||
|
@ -474,137 +461,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
});
|
||||
}
|
||||
|
||||
getPropsForMessageDetail(
|
||||
ourConversationId: string
|
||||
): MinimalPropsForMessageDetails {
|
||||
const newIdentity = window.i18n('newIdentity');
|
||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||
|
||||
const sendStateByConversationId =
|
||||
this.get('sendStateByConversationId') || {};
|
||||
|
||||
const unidentifiedDeliveries = this.get('unidentifiedDeliveries') || [];
|
||||
const unidentifiedDeliveriesSet = new Set(
|
||||
map(
|
||||
unidentifiedDeliveries,
|
||||
identifier =>
|
||||
window.ConversationController.getConversationId(identifier) as string
|
||||
)
|
||||
);
|
||||
|
||||
let conversationIds: Array<string>;
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
if (isIncoming(this.attributes)) {
|
||||
conversationIds = [getContactId(this.attributes)!];
|
||||
} else if (!isEmpty(sendStateByConversationId)) {
|
||||
if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) {
|
||||
conversationIds = [ourConversationId];
|
||||
} else {
|
||||
conversationIds = Object.keys(sendStateByConversationId).filter(
|
||||
id => id !== ourConversationId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Older messages don't have the recipients included on the message, so we fall back
|
||||
// to the conversation's current recipients
|
||||
conversationIds = (this.getConversation()?.getRecipients() || []).map(
|
||||
(id: string) => window.ConversationController.getConversationId(id)!
|
||||
);
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
// This will make the error message for outgoing key errors a bit nicer
|
||||
const allErrors = (this.get('errors') || []).map(error => {
|
||||
if (error.name === OUTGOING_KEY_ERROR) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.message = newIdentity;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
|
||||
// If an error has a specific number it's associated with, we'll show it next to
|
||||
// that contact. Otherwise, it will be a standalone entry.
|
||||
const errors = reject(allErrors, error =>
|
||||
Boolean(error.identifier || error.number)
|
||||
);
|
||||
const errorsGroupedById = groupBy(allErrors, error => {
|
||||
const identifier = error.identifier || error.number;
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.ConversationController.getConversationId(identifier);
|
||||
});
|
||||
|
||||
const contacts: ReadonlyArray<SmartMessageDetailContact> =
|
||||
conversationIds.map(id => {
|
||||
const errorsForContact = getOwn(errorsGroupedById, id);
|
||||
const isOutgoingKeyError = Boolean(
|
||||
errorsForContact?.some(error => error.name === OUTGOING_KEY_ERROR)
|
||||
);
|
||||
const isUnidentifiedDelivery =
|
||||
window.storage.get('unidentifiedDeliveryIndicators', false) &&
|
||||
this.isUnidentifiedDelivery(id, unidentifiedDeliveriesSet);
|
||||
|
||||
const sendState = getOwn(sendStateByConversationId, id);
|
||||
|
||||
let status = sendState?.status;
|
||||
|
||||
// If a message was only sent to yourself (Note to Self or a lonely group), it
|
||||
// is shown read.
|
||||
if (id === ourConversationId && status && isSent(status)) {
|
||||
status = SendStatus.Read;
|
||||
}
|
||||
|
||||
const statusTimestamp = sendState?.updatedAt;
|
||||
|
||||
return {
|
||||
...findAndFormatContact(id),
|
||||
status,
|
||||
statusTimestamp:
|
||||
statusTimestamp === this.get('sent_at')
|
||||
? undefined
|
||||
: statusTimestamp,
|
||||
errors: errorsForContact,
|
||||
isOutgoingKeyError,
|
||||
isUnidentifiedDelivery,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
sentAt: this.get('sent_at'),
|
||||
receivedAt: this.getReceivedAt(),
|
||||
message: getPropsForMessage(this.attributes, {
|
||||
conversationSelector: findAndFormatContact,
|
||||
ourConversationId,
|
||||
ourNumber: window.textsecure.storage.user.getNumber(),
|
||||
ourACI: window.textsecure.storage.user
|
||||
.getCheckedUuid(UUIDKind.ACI)
|
||||
.toString(),
|
||||
ourPNI: window.textsecure.storage.user
|
||||
.getCheckedUuid(UUIDKind.PNI)
|
||||
.toString(),
|
||||
regionCode: window.storage.get('regionCode', 'ZZ'),
|
||||
accountSelector: (identifier?: string) => {
|
||||
const state = window.reduxStore.getState();
|
||||
const accountSelector = getAccountSelector(state);
|
||||
return accountSelector(identifier);
|
||||
},
|
||||
contactNameColorSelector: (
|
||||
conversationId: string,
|
||||
contactId: string | undefined
|
||||
) => {
|
||||
const state = window.reduxStore.getState();
|
||||
const contactNameColorSelector = getContactNameColorSelector(state);
|
||||
return contactNameColorSelector(conversationId, contactId);
|
||||
},
|
||||
}),
|
||||
errors,
|
||||
contacts,
|
||||
};
|
||||
}
|
||||
|
||||
// Dependencies of prop-generation functions
|
||||
getConversation(): ConversationModel | undefined {
|
||||
return window.ConversationController.get(this.get('conversationId'));
|
||||
|
|
|
@ -26,7 +26,6 @@ import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
|||
// State
|
||||
import { createApp } from './state/roots/createApp';
|
||||
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||
import { createMessageDetail } from './state/roots/createMessageDetail';
|
||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||
|
||||
|
@ -395,7 +394,6 @@ export const setup = (options: {
|
|||
const Roots = {
|
||||
createApp,
|
||||
createGroupV2JoinModal,
|
||||
createMessageDetail,
|
||||
createSafetyNumberViewer,
|
||||
createShortcutGuideModal,
|
||||
};
|
||||
|
|
|
@ -6,9 +6,11 @@ import type { ThunkAction } from 'redux-thunk';
|
|||
import * as Errors from '../../types/errors';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { UUIDStringType } from '../../types/UUID';
|
||||
import { getUuidsForE164s } from '../../util/getUuidsForE164s';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
import type { NoopActionType } from './noop';
|
||||
|
||||
|
@ -36,6 +38,10 @@ export const actions = {
|
|||
checkForAccount,
|
||||
};
|
||||
|
||||
export const useAccountsActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function checkForAccount(
|
||||
phoneNumber: string
|
||||
): ThunkAction<
|
||||
|
|
|
@ -125,11 +125,12 @@ import {
|
|||
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
|
||||
} from '../../groups';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import type { PanelRenderType } from '../../types/Panels';
|
||||
import type { PanelRenderType, PanelRequestType } from '../../types/Panels';
|
||||
import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
|
||||
import { isOlderThan } from '../../util/timestamp';
|
||||
import { DAY } from '../../util/durations';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import { startConversation } from '../../util/startConversation';
|
||||
|
||||
// State
|
||||
|
@ -407,6 +408,7 @@ export type ConversationsStateType = {
|
|||
selectedMessageCounter: number;
|
||||
selectedMessageSource: SelectedMessageSource | undefined;
|
||||
selectedConversationPanels: Array<PanelRenderType>;
|
||||
selectedMessageForDetails?: MessageAttributesType;
|
||||
showArchived: boolean;
|
||||
composer?: ComposerStateType;
|
||||
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
||||
|
@ -817,11 +819,11 @@ type ReplaceAvatarsActionType = {
|
|||
export type ConversationActionType =
|
||||
| CancelVerificationDataByConversationActionType
|
||||
| ClearCancelledVerificationActionType
|
||||
| ClearVerificationDataByConversationActionType
|
||||
| ClearGroupCreationErrorActionType
|
||||
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
||||
| ClearSelectedMessageActionType
|
||||
| ClearUnreadMetricsActionType
|
||||
| ClearVerificationDataByConversationActionType
|
||||
| CloseContactSpoofingReviewActionType
|
||||
| CloseMaximumGroupSizeModalActionType
|
||||
| CloseRecommendedGroupSizeModalActionType
|
||||
|
@ -843,6 +845,7 @@ export type ConversationActionType =
|
|||
| MessageChangedActionType
|
||||
| MessageDeletedActionType
|
||||
| MessageExpandedActionType
|
||||
| MessageExpiredActionType
|
||||
| MessageSelectedActionType
|
||||
| MessagesAddedActionType
|
||||
| MessagesResetActionType
|
||||
|
@ -871,8 +874,8 @@ export type ConversationActionType =
|
|||
| ShowSendAnywayDialogActionType
|
||||
| StartComposingActionType
|
||||
| StartSettingGroupMetadataActionType
|
||||
| ToggleConversationInChooseMembersActionType
|
||||
| ToggleComposeEditingAvatarActionType;
|
||||
| ToggleComposeEditingAvatarActionType
|
||||
| ToggleConversationInChooseMembersActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
|
@ -1515,7 +1518,7 @@ function deleteMessage({
|
|||
} else {
|
||||
conversation.decrementMessageCount();
|
||||
}
|
||||
popPanelForConversation(conversationId)(dispatch, getState, undefined);
|
||||
popPanelForConversation()(dispatch, getState, undefined);
|
||||
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
|
@ -2536,44 +2539,50 @@ function setIsFetchingUUID(
|
|||
}
|
||||
|
||||
export type PushPanelForConversationActionType = (
|
||||
conversationId: string,
|
||||
panel: PanelRenderType
|
||||
panel: PanelRequestType
|
||||
) => unknown;
|
||||
|
||||
function pushPanelForConversation(
|
||||
conversationId: string,
|
||||
panel: PanelRenderType
|
||||
): PushPanelActionType {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
`addPanelToConversation: No conversation found for conversation ${conversationId}`
|
||||
);
|
||||
}
|
||||
panel: PanelRequestType
|
||||
): ThunkAction<void, RootStateType, unknown, PushPanelActionType> {
|
||||
return async dispatch => {
|
||||
if (panel.type === PanelType.MessageDetails) {
|
||||
const { messageId } = panel.args;
|
||||
|
||||
conversation.trigger('pushPanel', panel);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
'pushPanelForConversation: could not find message for MessageDetails'
|
||||
);
|
||||
}
|
||||
dispatch({
|
||||
type: PUSH_PANEL,
|
||||
payload: {
|
||||
type: PanelType.MessageDetails,
|
||||
args: {
|
||||
message: message.attributes,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: PUSH_PANEL,
|
||||
payload: panel,
|
||||
dispatch({
|
||||
type: PUSH_PANEL,
|
||||
payload: panel,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type PopPanelForConversationActionType = (
|
||||
conversationId: string
|
||||
) => unknown;
|
||||
export type PopPanelForConversationActionType = () => unknown;
|
||||
|
||||
function popPanelForConversation(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, PopPanelActionType> {
|
||||
function popPanelForConversation(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
PopPanelActionType
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
`addPanelToConversation: No conversation found for conversation ${conversationId}`
|
||||
);
|
||||
}
|
||||
|
||||
const { conversations } = getState();
|
||||
const { selectedConversationPanels } = conversations;
|
||||
|
||||
|
@ -2581,14 +2590,6 @@ function popPanelForConversation(
|
|||
return;
|
||||
}
|
||||
|
||||
const panel = [...selectedConversationPanels].pop();
|
||||
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.trigger('popPanel', panel);
|
||||
|
||||
dispatch({
|
||||
type: POP_PANEL,
|
||||
payload: null,
|
||||
|
@ -3718,6 +3719,30 @@ function visitListsInVerificationData(
|
|||
return result;
|
||||
}
|
||||
|
||||
function maybeUpdateSelectedMessageForDetails(
|
||||
{
|
||||
messageId,
|
||||
selectedMessageForDetails,
|
||||
}: {
|
||||
messageId: string;
|
||||
selectedMessageForDetails: MessageAttributesType | undefined;
|
||||
},
|
||||
state: ConversationsStateType
|
||||
): ConversationsStateType {
|
||||
if (!state.selectedMessageForDetails) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state.selectedMessageForDetails.id !== messageId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedMessageForDetails,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<ConversationsStateType> = getEmptyState(),
|
||||
action: Readonly<ConversationActionType | StoryDistributionListsActionType>
|
||||
|
@ -4242,18 +4267,26 @@ export function reducer(
|
|||
verificationDataByConversation,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'MESSAGE_CHANGED') {
|
||||
const { id, conversationId, data } = action.payload;
|
||||
const existingConversation = state.messagesByConversation[conversationId];
|
||||
|
||||
// We don't keep track of messages unless their conversation is loaded...
|
||||
if (!existingConversation) {
|
||||
return state;
|
||||
return maybeUpdateSelectedMessageForDetails(
|
||||
{ messageId: id, selectedMessageForDetails: data },
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// ...and we've already loaded that message once
|
||||
const existingMessage = getOwn(state.messagesLookup, id);
|
||||
if (!existingMessage) {
|
||||
return state;
|
||||
return maybeUpdateSelectedMessageForDetails(
|
||||
{ messageId: id, selectedMessageForDetails: data },
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
const conversationAttrs = state.conversationLookup[conversationId];
|
||||
|
@ -4265,7 +4298,13 @@ export function reducer(
|
|||
const toIncrement = data.reactions?.length ? 1 : 0;
|
||||
|
||||
return {
|
||||
...state,
|
||||
...maybeUpdateSelectedMessageForDetails(
|
||||
{
|
||||
messageId: id,
|
||||
selectedMessageForDetails: data,
|
||||
},
|
||||
state
|
||||
),
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: {
|
||||
|
@ -4283,6 +4322,14 @@ export function reducer(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === MESSAGE_EXPIRED) {
|
||||
return maybeUpdateSelectedMessageForDetails(
|
||||
{ messageId: action.payload.id, selectedMessageForDetails: undefined },
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
if (action.type === 'MESSAGE_EXPANDED') {
|
||||
const { id, displayLimit } = action.payload;
|
||||
|
||||
|
@ -4459,7 +4506,10 @@ export function reducer(
|
|||
|
||||
const existingConversation = messagesByConversation[conversationId];
|
||||
if (!existingConversation) {
|
||||
return state;
|
||||
return maybeUpdateSelectedMessageForDetails(
|
||||
{ messageId: id, selectedMessageForDetails: undefined },
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// Assuming that we always have contiguous groups of messages in memory, the removal
|
||||
|
@ -4503,7 +4553,10 @@ export function reducer(
|
|||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...maybeUpdateSelectedMessageForDetails(
|
||||
{ messageId: id, selectedMessageForDetails: undefined },
|
||||
state
|
||||
),
|
||||
messagesLookup: omit(messagesLookup, id),
|
||||
messagesByConversation: {
|
||||
[conversationId]: {
|
||||
|
@ -4792,6 +4845,17 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (action.type === PUSH_PANEL) {
|
||||
if (action.payload.type === PanelType.MessageDetails) {
|
||||
return {
|
||||
...state,
|
||||
selectedConversationPanels: [
|
||||
...state.selectedConversationPanels,
|
||||
action.payload,
|
||||
],
|
||||
selectedMessageForDetails: action.payload.args.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedConversationPanels: [
|
||||
|
@ -4804,7 +4868,19 @@ export function reducer(
|
|||
if (action.type === POP_PANEL) {
|
||||
const { selectedConversationPanels } = state;
|
||||
const nextPanels = [...selectedConversationPanels];
|
||||
nextPanels.pop();
|
||||
const panel = nextPanels.pop();
|
||||
|
||||
if (!panel) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.MessageDetails) {
|
||||
return {
|
||||
...state,
|
||||
selectedConversationPanels: nextPanels,
|
||||
selectedMessageForDetails: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { SmartMessageDetail } from '../smart/MessageDetail';
|
||||
|
||||
export const createMessageDetail = (
|
||||
store: Store,
|
||||
props: Parameters<typeof SmartMessageDetail>[0]
|
||||
): ReactElement => (
|
||||
<Provider store={store}>
|
||||
<SmartMessageDetail {...props} />
|
||||
</Provider>
|
||||
);
|
|
@ -65,8 +65,7 @@ import { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
|||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { reduce } from '../../util/iterables';
|
||||
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
|
||||
import type { ReactPanelRenderType, PanelRenderType } from '../../types/Panels';
|
||||
import { isPanelHandledByReact } from '../../types/Panels';
|
||||
import type { PanelRenderType } from '../../types/Panels';
|
||||
|
||||
let placeholderContact: ConversationType;
|
||||
export const getPlaceholderContact = (): ConversationType => {
|
||||
|
@ -1135,7 +1134,7 @@ export const getHideStoryConversationIds = createSelector(
|
|||
)
|
||||
);
|
||||
|
||||
const getTopPanel = createSelector(
|
||||
export const getTopPanel = createSelector(
|
||||
getConversations,
|
||||
(conversations): PanelRenderType | undefined =>
|
||||
conversations.selectedConversationPanels[
|
||||
|
@ -1143,22 +1142,6 @@ const getTopPanel = createSelector(
|
|||
]
|
||||
);
|
||||
|
||||
export const getTopPanelRenderableByReact = createSelector(
|
||||
getConversations,
|
||||
(conversations): ReactPanelRenderType | undefined => {
|
||||
const topPanel =
|
||||
conversations.selectedConversationPanels[
|
||||
conversations.selectedConversationPanels.length - 1
|
||||
];
|
||||
|
||||
if (!isPanelHandledByReact(topPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return topPanel;
|
||||
}
|
||||
);
|
||||
|
||||
export const getConversationTitle = createSelector(
|
||||
getIntl,
|
||||
getTopPanel,
|
||||
|
|
|
@ -1,19 +1,35 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { identity, isEqual, isNumber, isObject, map, omit, pick } from 'lodash';
|
||||
import {
|
||||
groupBy,
|
||||
identity,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isNumber,
|
||||
isObject,
|
||||
map,
|
||||
omit,
|
||||
pick,
|
||||
} from 'lodash';
|
||||
import { createSelector, createSelectorCreator } from 'reselect';
|
||||
import filesize from 'filesize';
|
||||
import getDirection from 'direction';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import LinkifyIt from 'linkify-it';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import type {
|
||||
LastMessageStatus,
|
||||
MessageAttributesType,
|
||||
MessageReactionType,
|
||||
ShallowChallengeError,
|
||||
} from '../../model-types.d';
|
||||
|
||||
import type {
|
||||
Contact as SmartMessageDetailContact,
|
||||
OwnProps as SmartMessageDetailPropsType,
|
||||
} from '../smart/MessageDetail';
|
||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||
import type { PropsData } from '../../components/conversation/Message';
|
||||
import type { PropsData as TimelineMessagePropsData } from '../../components/conversation/TimelineMessage';
|
||||
|
@ -52,6 +68,8 @@ import { ReadStatus } from '../../messages/MessageReadStatus';
|
|||
import type { CallingNotificationType } from '../../util/callingNotification';
|
||||
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getRecipients } from '../../util/getRecipients';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import { isMoreRecentThan } from '../../util/timestamp';
|
||||
import * as iterables from '../../util/iterables';
|
||||
|
@ -65,10 +83,11 @@ import {
|
|||
isMissingRequiredProfileSharing,
|
||||
} from './conversations';
|
||||
import {
|
||||
getIntl,
|
||||
getRegionCode,
|
||||
getUserACI,
|
||||
getUserConversationId,
|
||||
getUserNumber,
|
||||
getUserACI,
|
||||
getUserPNI,
|
||||
} from './user';
|
||||
|
||||
|
@ -1937,3 +1956,172 @@ export function getLastChallengeError(
|
|||
|
||||
return challengeErrors.pop();
|
||||
}
|
||||
|
||||
const getSelectedMessageForDetails = (
|
||||
state: StateType
|
||||
): MessageAttributesType | undefined =>
|
||||
state.conversations.selectedMessageForDetails;
|
||||
|
||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||
|
||||
export const getMessageDetails = createSelector(
|
||||
getAccountSelector,
|
||||
getContactNameColorSelector,
|
||||
getConversationSelector,
|
||||
getIntl,
|
||||
getRegionCode,
|
||||
getSelectedMessageForDetails,
|
||||
getUserACI,
|
||||
getUserPNI,
|
||||
getUserConversationId,
|
||||
getUserNumber,
|
||||
(
|
||||
accountSelector,
|
||||
contactNameColorSelector,
|
||||
conversationSelector,
|
||||
i18n,
|
||||
regionCode,
|
||||
message,
|
||||
ourACI,
|
||||
ourPNI,
|
||||
ourConversationId,
|
||||
ourNumber
|
||||
): SmartMessageDetailPropsType | undefined => {
|
||||
if (!message || !ourConversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
errors: messageErrors = [],
|
||||
sendStateByConversationId = {},
|
||||
unidentifiedDeliveries = [],
|
||||
unidentifiedDeliveryReceived,
|
||||
} = message;
|
||||
|
||||
const unidentifiedDeliveriesSet = new Set(
|
||||
map(
|
||||
unidentifiedDeliveries,
|
||||
identifier =>
|
||||
window.ConversationController.getConversationId(identifier) as string
|
||||
)
|
||||
);
|
||||
|
||||
let conversationIds: Array<string>;
|
||||
if (isIncoming(message)) {
|
||||
conversationIds = [
|
||||
getContactId(message, {
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourACI,
|
||||
}),
|
||||
].filter(isNotNil);
|
||||
} else if (!isEmpty(sendStateByConversationId)) {
|
||||
if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) {
|
||||
conversationIds = [ourConversationId];
|
||||
} else {
|
||||
conversationIds = Object.keys(sendStateByConversationId).filter(
|
||||
id => id !== ourConversationId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const messageConversation = window.ConversationController.get(
|
||||
message.conversationId
|
||||
);
|
||||
const conversationRecipients = messageConversation
|
||||
? getRecipients(messageConversation.attributes) || []
|
||||
: [];
|
||||
// Older messages don't have the recipients included on the message, so we fall back
|
||||
// to the conversation's current recipients
|
||||
conversationIds = conversationRecipients
|
||||
.map((id: string) =>
|
||||
window.ConversationController.getConversationId(id)
|
||||
)
|
||||
.filter(isNotNil);
|
||||
}
|
||||
|
||||
// This will make the error message for outgoing key errors a bit nicer
|
||||
const allErrors = messageErrors.map(error => {
|
||||
if (error.name === OUTGOING_KEY_ERROR) {
|
||||
return {
|
||||
...error,
|
||||
message: i18n('newIdentity'),
|
||||
};
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
|
||||
// If an error has a specific number it's associated with, we'll show it next to
|
||||
// that contact. Otherwise, it will be a standalone entry.
|
||||
const errors = allErrors.filter(error =>
|
||||
Boolean(error.identifier || error.number)
|
||||
);
|
||||
const errorsGroupedById = groupBy(allErrors, error => {
|
||||
const identifier = error.identifier || error.number;
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.ConversationController.getConversationId(identifier);
|
||||
});
|
||||
|
||||
const hasUnidentifiedDeliveryIndicators = window.storage.get(
|
||||
'unidentifiedDeliveryIndicators',
|
||||
false
|
||||
);
|
||||
|
||||
const contacts: ReadonlyArray<SmartMessageDetailContact> =
|
||||
conversationIds.map(id => {
|
||||
const errorsForContact = getOwn(errorsGroupedById, id);
|
||||
const isOutgoingKeyError = Boolean(
|
||||
errorsForContact?.some(error => error.name === OUTGOING_KEY_ERROR)
|
||||
);
|
||||
|
||||
let isUnidentifiedDelivery = false;
|
||||
if (hasUnidentifiedDeliveryIndicators) {
|
||||
isUnidentifiedDelivery = isIncoming(message)
|
||||
? Boolean(unidentifiedDeliveryReceived)
|
||||
: unidentifiedDeliveriesSet.has(id);
|
||||
}
|
||||
|
||||
const sendState = getOwn(sendStateByConversationId, id);
|
||||
|
||||
let status = sendState?.status;
|
||||
|
||||
// If a message was only sent to yourself (Note to Self or a lonely group), it
|
||||
// is shown read.
|
||||
if (id === ourConversationId && status && isSent(status)) {
|
||||
status = SendStatus.Read;
|
||||
}
|
||||
|
||||
const statusTimestamp = sendState?.updatedAt;
|
||||
|
||||
return {
|
||||
...conversationSelector(id),
|
||||
errors: errorsForContact,
|
||||
isOutgoingKeyError,
|
||||
isUnidentifiedDelivery,
|
||||
status,
|
||||
statusTimestamp:
|
||||
statusTimestamp === message.timestamp ? undefined : statusTimestamp,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
contacts,
|
||||
errors,
|
||||
message: getPropsForMessage(message, {
|
||||
accountSelector,
|
||||
contactNameColorSelector,
|
||||
conversationSelector,
|
||||
ourACI,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourPNI,
|
||||
regionCode,
|
||||
}),
|
||||
receivedAt: Number(message.received_at_ms || message.received_at),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { PanelRenderType } from '../../types/Panels';
|
||||
import type { StateType } from '../reducer';
|
||||
import type { ReactPanelRenderType } from '../../types/Panels';
|
||||
import * as log from '../../logging/log';
|
||||
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
||||
import { ConversationView } from '../../components/conversation/ConversationView';
|
||||
|
@ -18,11 +18,12 @@ import { SmartConversationNotificationsSettings } from './ConversationNotificati
|
|||
import { SmartGV1Members } from './GV1Members';
|
||||
import { SmartGroupLinkManagement } from './GroupLinkManagement';
|
||||
import { SmartGroupV2Permissions } from './GroupV2Permissions';
|
||||
import { SmartMessageDetail } from './MessageDetail';
|
||||
import { SmartPendingInvites } from './PendingInvites';
|
||||
import { SmartStickerManager } from './StickerManager';
|
||||
import { SmartTimeline } from './Timeline';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getTopPanelRenderableByReact } from '../selectors/conversations';
|
||||
import { getTopPanel } from '../selectors/conversations';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
|
||||
|
@ -33,10 +34,10 @@ export type PropsType = {
|
|||
export function SmartConversationView({
|
||||
conversationId,
|
||||
}: PropsType): JSX.Element {
|
||||
const { startConversation } = useConversationsActions();
|
||||
const topPanel = useSelector<StateType, ReactPanelRenderType | undefined>(
|
||||
getTopPanelRenderableByReact
|
||||
const topPanel = useSelector<StateType, PanelRenderType | undefined>(
|
||||
getTopPanel
|
||||
);
|
||||
const { startConversation } = useConversationsActions();
|
||||
|
||||
const { processAttachments } = useComposerActions();
|
||||
const i18n = useSelector(getIntl);
|
||||
|
@ -136,6 +137,14 @@ export function SmartConversationView({
|
|||
);
|
||||
}
|
||||
|
||||
if (topPanel.type === PanelType.MessageDetails) {
|
||||
return (
|
||||
<div className="panel message-detail-wrapper">
|
||||
<SmartMessageDetail />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (topPanel.type === PanelType.NotificationSettings) {
|
||||
return (
|
||||
<div className="panel">
|
||||
|
|
|
@ -1,63 +1,101 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail';
|
||||
import { MessageDetail } from '../../components/conversation/MessageDetail';
|
||||
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { getContactNameColorSelector } from '../selectors/conversations';
|
||||
import type { MinimalPropsForMessageDetails } from '../../models/messages';
|
||||
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
||||
import { getMessageDetails } from '../selectors/message';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { useAccountsActions } from '../ducks/accounts';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useLightboxActions } from '../ducks/lightbox';
|
||||
import { useStoriesActions } from '../ducks/stories';
|
||||
|
||||
export { Contact } from '../../components/conversation/MessageDetail';
|
||||
export type PropsWithExtraFunctions = MinimalPropsForMessageDetails &
|
||||
Pick<
|
||||
MessageDetailProps,
|
||||
| 'contactNameColor'
|
||||
| 'getPreferredBadge'
|
||||
| 'i18n'
|
||||
| 'interactionMode'
|
||||
| 'renderAudioAttachment'
|
||||
| 'theme'
|
||||
>;
|
||||
export type OwnProps = Pick<
|
||||
MessageDetailProps,
|
||||
'contacts' | 'errors' | 'message' | 'receivedAt'
|
||||
>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: MinimalPropsForMessageDetails
|
||||
): PropsWithExtraFunctions => {
|
||||
const { contacts, errors, message, receivedAt, sentAt } = props;
|
||||
export function SmartMessageDetail(): JSX.Element | null {
|
||||
const getContactNameColor = useSelector(getContactNameColorSelector);
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const i18n = useSelector(getIntl);
|
||||
const interactionMode = useSelector(getInteractionMode);
|
||||
const messageDetails = useSelector(getMessageDetails);
|
||||
const theme = useSelector(getTheme);
|
||||
const { checkForAccount } = useAccountsActions();
|
||||
const {
|
||||
clearSelectedMessage,
|
||||
doubleCheckMissingQuoteReference,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openGiftBadge,
|
||||
popPanelForConversation,
|
||||
pushPanelForConversation,
|
||||
saveAttachment,
|
||||
showConversation,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
startConversation,
|
||||
} = useConversationsActions();
|
||||
const { showContactModal, toggleSafetyNumberModal } = useGlobalModalActions();
|
||||
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
|
||||
const { viewStory } = useStoriesActions();
|
||||
|
||||
useEffect(() => {
|
||||
if (!messageDetails) {
|
||||
popPanelForConversation();
|
||||
}
|
||||
}, [messageDetails, popPanelForConversation]);
|
||||
|
||||
if (!messageDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { contacts, errors, message, receivedAt } = messageDetails;
|
||||
|
||||
const contactNameColor =
|
||||
message.conversationType === 'group'
|
||||
? getContactNameColorSelector(state)(
|
||||
message.conversationId,
|
||||
message.author.id
|
||||
)
|
||||
? getContactNameColor(message.conversationId, message.author.id)
|
||||
: undefined;
|
||||
|
||||
const getPreferredBadge = getPreferredBadgeSelector(state);
|
||||
|
||||
return {
|
||||
contacts,
|
||||
contactNameColor,
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
getPreferredBadge,
|
||||
i18n: getIntl(state),
|
||||
interactionMode: getInteractionMode(state),
|
||||
theme: getTheme(state),
|
||||
|
||||
renderAudioAttachment,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
export const SmartMessageDetail = smart(MessageDetail);
|
||||
return (
|
||||
<MessageDetail
|
||||
checkForAccount={checkForAccount}
|
||||
clearSelectedMessage={clearSelectedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
contacts={contacts}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
errors={errors}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
message={message}
|
||||
openGiftBadge={openGiftBadge}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
receivedAt={receivedAt}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
saveAttachment={saveAttachment}
|
||||
sentAt={message.timestamp}
|
||||
showContactModal={showContactModal}
|
||||
showConversation={showConversation}
|
||||
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
|
||||
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
|
||||
showLightbox={showLightbox}
|
||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||
startConversation={startConversation}
|
||||
theme={theme}
|
||||
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||
viewStory={viewStory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { EmbeddedContactType } from './EmbeddedContact';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
|
||||
export enum PanelType {
|
||||
|
@ -18,7 +19,7 @@ export enum PanelType {
|
|||
StickerManager = 'StickerManager',
|
||||
}
|
||||
|
||||
export type ReactPanelRenderType =
|
||||
export type PanelRequestType =
|
||||
| { type: PanelType.AllMedia }
|
||||
| { type: PanelType.ChatColorEditor }
|
||||
| {
|
||||
|
@ -36,33 +37,28 @@ export type ReactPanelRenderType =
|
|||
| { type: PanelType.GroupLinkManagement }
|
||||
| { type: PanelType.GroupPermissions }
|
||||
| { type: PanelType.GroupV1Members }
|
||||
| { type: PanelType.MessageDetails; args: { messageId: string } }
|
||||
| { type: PanelType.NotificationSettings }
|
||||
| { type: PanelType.StickerManager };
|
||||
|
||||
export type BackbonePanelRenderType = {
|
||||
type: PanelType.MessageDetails;
|
||||
args: { messageId: string };
|
||||
};
|
||||
|
||||
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
|
||||
|
||||
export function isPanelHandledByReact(
|
||||
panel: PanelRenderType
|
||||
): panel is ReactPanelRenderType {
|
||||
if (!panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
panel.type === PanelType.AllMedia ||
|
||||
panel.type === PanelType.ChatColorEditor ||
|
||||
panel.type === PanelType.ContactDetails ||
|
||||
panel.type === PanelType.ConversationDetails ||
|
||||
panel.type === PanelType.GroupInvites ||
|
||||
panel.type === PanelType.GroupLinkManagement ||
|
||||
panel.type === PanelType.GroupPermissions ||
|
||||
panel.type === PanelType.GroupV1Members ||
|
||||
panel.type === PanelType.NotificationSettings ||
|
||||
panel.type === PanelType.StickerManager
|
||||
);
|
||||
}
|
||||
export type PanelRenderType =
|
||||
| { type: PanelType.AllMedia }
|
||||
| { type: PanelType.ChatColorEditor }
|
||||
| {
|
||||
type: PanelType.ContactDetails;
|
||||
args: {
|
||||
contact: EmbeddedContactType;
|
||||
signalAccount?: {
|
||||
phoneNumber: string;
|
||||
uuid: UUIDStringType;
|
||||
};
|
||||
};
|
||||
}
|
||||
| { type: PanelType.ConversationDetails }
|
||||
| { type: PanelType.GroupInvites }
|
||||
| { type: PanelType.GroupLinkManagement }
|
||||
| { type: PanelType.GroupPermissions }
|
||||
| { type: PanelType.GroupV1Members }
|
||||
| { type: PanelType.MessageDetails; args: { message: MessageAttributesType } }
|
||||
| { type: PanelType.NotificationSettings }
|
||||
| { type: PanelType.StickerManager };
|
||||
|
|
|
@ -17,23 +17,11 @@ import {
|
|||
removeLinkPreview,
|
||||
suspendLinkPreviews,
|
||||
} from '../services/LinkPreview';
|
||||
import { SECOND } from '../util/durations';
|
||||
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
|
||||
import { PanelType, isPanelHandledByReact } from '../types/Panels';
|
||||
import { UUIDKind } from '../types/UUID';
|
||||
|
||||
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
|
||||
|
||||
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||
// Sub-views
|
||||
private contactModalView?: Backbone.View;
|
||||
private conversationView?: Backbone.View;
|
||||
private lightboxView?: ReactWrapperView;
|
||||
private stickerPreviewModalView?: Backbone.View;
|
||||
|
||||
// Panel support
|
||||
private panels: Array<BackbonePanelType> = [];
|
||||
private previousFocus?: HTMLElement;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(...args: Array<any>) {
|
||||
|
@ -48,9 +36,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
this.unload(`model trigger - ${reason}`)
|
||||
);
|
||||
|
||||
this.listenTo(this.model, 'pushPanel', this.pushPanel);
|
||||
this.listenTo(this.model, 'popPanel', this.popPanel);
|
||||
|
||||
this.render();
|
||||
|
||||
this.setupConversationView();
|
||||
|
@ -139,22 +124,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
this.conversationView?.remove();
|
||||
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
}
|
||||
if (this.stickerPreviewModalView) {
|
||||
this.stickerPreviewModalView.remove();
|
||||
}
|
||||
if (this.lightboxView) {
|
||||
this.lightboxView.remove();
|
||||
}
|
||||
if (this.panels && this.panels.length) {
|
||||
for (let i = 0, max = this.panels.length; i < max; i += 1) {
|
||||
const panel = this.panels[i];
|
||||
panel.view.remove();
|
||||
}
|
||||
}
|
||||
|
||||
removeLinkPreview();
|
||||
suspendLinkPreviews();
|
||||
|
||||
|
@ -226,143 +195,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
void this.model.updateVerified();
|
||||
}
|
||||
|
||||
getMessageDetail({
|
||||
messageId,
|
||||
}: {
|
||||
messageId: string;
|
||||
}): Backbone.View | undefined {
|
||||
const message = window.MessageController.getById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`getMessageDetail: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
if (!message.isNormalBubble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getProps = () => ({
|
||||
...message.getPropsForMessageDetail(
|
||||
window.ConversationController.getOurConversationIdOrThrow()
|
||||
),
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
this.stopListening(message, 'change', update);
|
||||
window.reduxActions.conversations.popPanelForConversation(this.model.id);
|
||||
};
|
||||
|
||||
const view = new ReactWrapperView({
|
||||
className: 'panel message-detail-wrapper',
|
||||
JSX: window.Signal.State.Roots.createMessageDetail(
|
||||
window.reduxStore,
|
||||
getProps()
|
||||
),
|
||||
onClose,
|
||||
});
|
||||
|
||||
const update = () =>
|
||||
view.update(
|
||||
window.Signal.State.Roots.createMessageDetail(
|
||||
window.reduxStore,
|
||||
getProps()
|
||||
)
|
||||
);
|
||||
this.listenTo(message, 'change', update);
|
||||
this.listenTo(message, 'expired', onClose);
|
||||
// We could listen to all involved contacts, but we'll call that overkill
|
||||
|
||||
view.render();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
pushPanel(panel: PanelRenderType): void {
|
||||
if (isPanelHandledByReact(panel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.panels = this.panels || [];
|
||||
|
||||
if (this.panels.length === 0) {
|
||||
this.previousFocus = document.activeElement as HTMLElement;
|
||||
}
|
||||
|
||||
const { type } = panel as BackbonePanelRenderType;
|
||||
|
||||
let view: Backbone.View | undefined;
|
||||
if (panel.type === PanelType.MessageDetails) {
|
||||
view = this.getMessageDetail(panel.args);
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.panels.push({
|
||||
panelType: type,
|
||||
view,
|
||||
});
|
||||
|
||||
view.$el.insertAfter(this.$('.panel').last());
|
||||
view.$el.one('animationend', () => {
|
||||
if (view) {
|
||||
view.$el.addClass('panel--static');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
popPanel(poppedPanel: PanelRenderType): void {
|
||||
if (!this.panels || !this.panels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.panels.length === 0 &&
|
||||
this.previousFocus &&
|
||||
this.previousFocus.focus
|
||||
) {
|
||||
this.previousFocus.focus();
|
||||
this.previousFocus = undefined;
|
||||
}
|
||||
|
||||
const panel = this.panels[this.panels.length - 1];
|
||||
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPanelHandledByReact(poppedPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.panels.pop();
|
||||
|
||||
if (panel.panelType !== poppedPanel.type) {
|
||||
log.warn('popPanel: last panel was not of same type');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.panels.length > 0) {
|
||||
this.panels[this.panels.length - 1].view.$el.fadeIn(250);
|
||||
}
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const removePanel = () => {
|
||||
if (!timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
|
||||
panel.view.remove();
|
||||
};
|
||||
panel.view.$el.addClass('panel--remove').one('transitionend', removePanel);
|
||||
|
||||
// Backup, in case things go wrong with the transitionend event
|
||||
timeout = setTimeout(removePanel, SECOND);
|
||||
}
|
||||
}
|
||||
|
||||
window.Whisper.ConversationView = ConversationView;
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -38,7 +38,6 @@ import type { ReduxActions } from './state/types';
|
|||
import type { createStore } from './state/createStore';
|
||||
import type { createApp } from './state/roots/createApp';
|
||||
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||
import type { createMessageDetail } from './state/roots/createMessageDetail';
|
||||
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||
import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||
import type * as appDuck from './state/ducks/app';
|
||||
|
@ -161,7 +160,6 @@ export type SignalCoreType = {
|
|||
Roots: {
|
||||
createApp: typeof createApp;
|
||||
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
||||
createMessageDetail: typeof createMessageDetail;
|
||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue