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
|
// Send Escape to active conversation so it can close panels
|
||||||
if (conversation && key === 'Escape') {
|
if (conversation && key === 'Escape') {
|
||||||
window.reduxActions.conversations.popPanelForConversation(
|
window.reduxActions.conversations.popPanelForConversation();
|
||||||
conversation.id
|
|
||||||
);
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
@ -1536,12 +1534,9 @@ export async function startApp(): Promise<void> {
|
||||||
shiftKey &&
|
shiftKey &&
|
||||||
(key === 'm' || key === 'M')
|
(key === 'm' || key === 'M')
|
||||||
) {
|
) {
|
||||||
window.reduxActions.conversations.pushPanelForConversation(
|
window.reduxActions.conversations.pushPanelForConversation({
|
||||||
conversation.id,
|
type: PanelType.AllMedia,
|
||||||
{
|
});
|
||||||
type: PanelType.AllMedia,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
@ -1634,14 +1629,12 @@ export async function startApp(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.reduxActions.conversations.pushPanelForConversation(
|
window.reduxActions.conversations.pushPanelForConversation({
|
||||||
conversation.id,
|
type: PanelType.MessageDetails,
|
||||||
{
|
args: {
|
||||||
type: PanelType.MessageDetails,
|
messageId: selectedMessage,
|
||||||
args: { messageId: selectedMessage },
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -462,7 +462,7 @@ export function CompositionArea({
|
||||||
recentStickers={recentStickers}
|
recentStickers={recentStickers}
|
||||||
clearInstalledStickerPack={clearInstalledStickerPack}
|
clearInstalledStickerPack={clearInstalledStickerPack}
|
||||||
onClickAddPack={() =>
|
onClickAddPack={() =>
|
||||||
pushPanelForConversation(conversationId, {
|
pushPanelForConversation({
|
||||||
type: PanelType.StickerManager,
|
type: PanelType.StickerManager,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,12 +154,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderBackButton(): ReactNode {
|
private renderBackButton(): ReactNode {
|
||||||
const { i18n, id, popPanelForConversation, showBackButton } = this.props;
|
const { i18n, popPanelForConversation, showBackButton } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => popPanelForConversation(id)}
|
onClick={popPanelForConversation}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-ConversationHeader__back-icon',
|
'module-ConversationHeader__back-icon',
|
||||||
showBackButton ? 'module-ConversationHeader__back-icon--show' : null
|
showBackButton ? 'module-ConversationHeader__back-icon--show' : null
|
||||||
|
@ -474,7 +474,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
{!isGroup || hasGV2AdminEnabled ? (
|
{!isGroup || hasGV2AdminEnabled ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.ConversationDetails,
|
type: PanelType.ConversationDetails,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -487,16 +487,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
{isGroup && !hasGV2AdminEnabled ? (
|
{isGroup && !hasGV2AdminEnabled ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(id, { type: PanelType.GroupV1Members })
|
pushPanelForConversation({ type: PanelType.GroupV1Members })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{i18n('showMembers')}
|
{i18n('showMembers')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() => pushPanelForConversation({ type: PanelType.AllMedia })}
|
||||||
pushPanelForConversation(id, { type: PanelType.AllMedia })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{i18n('viewRecentMedia')}
|
{i18n('viewRecentMedia')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -565,13 +563,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderHeader(): ReactNode {
|
private renderHeader(): ReactNode {
|
||||||
const {
|
const { conversationTitle, groupVersion, pushPanelForConversation, type } =
|
||||||
conversationTitle,
|
this.props;
|
||||||
id,
|
|
||||||
groupVersion,
|
|
||||||
pushPanelForConversation,
|
|
||||||
type,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (conversationTitle !== undefined) {
|
if (conversationTitle !== undefined) {
|
||||||
return (
|
return (
|
||||||
|
@ -589,14 +582,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'direct':
|
case 'direct':
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
pushPanelForConversation(id, { type: PanelType.ConversationDetails });
|
pushPanelForConversation({ type: PanelType.ConversationDetails });
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'group': {
|
case 'group': {
|
||||||
const hasGV2AdminEnabled = groupVersion === 2;
|
const hasGV2AdminEnabled = groupVersion === 2;
|
||||||
onClick = hasGV2AdminEnabled
|
onClick = hasGV2AdminEnabled
|
||||||
? () => {
|
? () => {
|
||||||
pushPanelForConversation(id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.ConversationDetails,
|
type: PanelType.ConversationDetails,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@ export type AudioAttachmentProps = {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
played: boolean;
|
played: boolean;
|
||||||
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
textPending?: boolean;
|
textPending?: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
@ -750,27 +751,25 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
conversationId,
|
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
|
i18n,
|
||||||
|
id,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
status,
|
|
||||||
i18n,
|
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
|
status,
|
||||||
text,
|
text,
|
||||||
textAttachment,
|
textAttachment,
|
||||||
timestamp,
|
timestamp,
|
||||||
id,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isStickerLike = isSticker || this.canRenderStickerLikeEmoji();
|
const isStickerLike = isSticker || this.canRenderStickerLikeEmoji();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageMetadata
|
<MessageMetadata
|
||||||
conversationId={conversationId}
|
|
||||||
deletedForEveryone={deletedForEveryone}
|
deletedForEveryone={deletedForEveryone}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
expirationLength={expirationLength}
|
expirationLength={expirationLength}
|
||||||
|
@ -827,23 +826,24 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
public renderAttachment(): JSX.Element | null {
|
public renderAttachment(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
|
conversationId,
|
||||||
direction,
|
direction,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
conversationId,
|
|
||||||
isSticker,
|
isSticker,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
|
pushPanelForConversation,
|
||||||
quote,
|
quote,
|
||||||
readStatus,
|
readStatus,
|
||||||
reducedMotion,
|
reducedMotion,
|
||||||
renderAudioAttachment,
|
renderAudioAttachment,
|
||||||
renderingContext,
|
renderingContext,
|
||||||
showLightbox,
|
|
||||||
shouldCollapseAbove,
|
shouldCollapseAbove,
|
||||||
shouldCollapseBelow,
|
shouldCollapseBelow,
|
||||||
|
showLightbox,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
textAttachment,
|
textAttachment,
|
||||||
|
@ -966,6 +966,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
id,
|
id,
|
||||||
conversationId,
|
conversationId,
|
||||||
played,
|
played,
|
||||||
|
pushPanelForConversation,
|
||||||
status,
|
status,
|
||||||
textPending: textAttachment?.pending,
|
textPending: textAttachment?.pending,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -1562,7 +1563,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
public renderEmbeddedContact(): JSX.Element | null {
|
public renderEmbeddedContact(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
contact,
|
contact,
|
||||||
conversationId,
|
|
||||||
conversationType,
|
conversationType,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -1598,7 +1598,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
pushPanelForConversation(conversationId, {
|
pushPanelForConversation({
|
||||||
type: PanelType.ContactDetails,
|
type: PanelType.ContactDetails,
|
||||||
args: {
|
args: {
|
||||||
contact,
|
contact,
|
||||||
|
@ -2243,7 +2243,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
contact,
|
contact,
|
||||||
conversationId,
|
|
||||||
showLightboxForViewOnceMedia,
|
showLightboxForViewOnceMedia,
|
||||||
direction,
|
direction,
|
||||||
giftBadge,
|
giftBadge,
|
||||||
|
@ -2381,7 +2380,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
uuid: contact.uuid,
|
uuid: contact.uuid,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
pushPanelForConversation(conversationId, {
|
pushPanelForConversation({
|
||||||
type: PanelType.ContactDetails,
|
type: PanelType.ContactDetails,
|
||||||
args: {
|
args: {
|
||||||
contact,
|
contact,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { animated, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||||
import { isDownloaded } from '../../types/Attachment';
|
import { isDownloaded } from '../../types/Attachment';
|
||||||
import type { DirectionType, MessageStatusType } from './Message';
|
import type { DirectionType, MessageStatusType } from './Message';
|
||||||
|
|
||||||
|
@ -16,7 +17,6 @@ import type { ComputePeaksResult } from '../GlobalAudioContext';
|
||||||
import { MessageMetadata } from './MessageMetadata';
|
import { MessageMetadata } from './MessageMetadata';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer';
|
import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer';
|
||||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
|
||||||
|
|
||||||
export type OwnProps = Readonly<{
|
export type OwnProps = Readonly<{
|
||||||
active: ActiveAudioPlayerStateType | undefined;
|
active: ActiveAudioPlayerStateType | undefined;
|
||||||
|
@ -592,7 +592,6 @@ export function MessageAudio(props: Props): JSX.Element {
|
||||||
|
|
||||||
{!withContentBelow && !collapseMetadata && (
|
{!withContentBelow && !collapseMetadata && (
|
||||||
<MessageMetadata
|
<MessageMetadata
|
||||||
conversationId={conversationId}
|
|
||||||
direction={direction}
|
direction={direction}
|
||||||
expirationLength={expirationLength}
|
expirationLength={expirationLength}
|
||||||
expirationTimestamp={expirationTimestamp}
|
expirationTimestamp={expirationTimestamp}
|
||||||
|
|
|
@ -8,14 +8,13 @@ import Measure from 'react-measure';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { DirectionType, MessageStatusType } from './Message';
|
import type { DirectionType, MessageStatusType } from './Message';
|
||||||
|
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||||
import { ExpireTimer } from './ExpireTimer';
|
import { ExpireTimer } from './ExpireTimer';
|
||||||
import { MessageTimestamp } from './MessageTimestamp';
|
import { MessageTimestamp } from './MessageTimestamp';
|
||||||
import { Spinner } from '../Spinner';
|
|
||||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
|
||||||
import { PanelType } from '../../types/Panels';
|
import { PanelType } from '../../types/Panels';
|
||||||
|
import { Spinner } from '../Spinner';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
conversationId: string;
|
|
||||||
deletedForEveryone?: boolean;
|
deletedForEveryone?: boolean;
|
||||||
direction: DirectionType;
|
direction: DirectionType;
|
||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
|
@ -35,7 +34,6 @@ type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MessageMetadata({
|
export function MessageMetadata({
|
||||||
conversationId,
|
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
|
@ -80,7 +78,7 @@ export function MessageMetadata({
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
pushPanelForConversation(conversationId, {
|
pushPanelForConversation({
|
||||||
type: PanelType.MessageDetails,
|
type: PanelType.MessageDetails,
|
||||||
args: { messageId: id },
|
args: { messageId: id },
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,12 +23,12 @@ import type {
|
||||||
PropsData as MessagePropsData,
|
PropsData as MessagePropsData,
|
||||||
PropsHousekeeping,
|
PropsHousekeeping,
|
||||||
} from './Message';
|
} from './Message';
|
||||||
|
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
||||||
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
||||||
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
||||||
import { PanelType } from '../../types/Panels';
|
import { PanelType } from '../../types/Panels';
|
||||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
|
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
|
@ -46,8 +46,8 @@ export type PropsActions = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
deleteMessageForEveryone: (id: string) => void;
|
deleteMessageForEveryone: (id: string) => void;
|
||||||
toggleForwardMessageModal: (id: string) => void;
|
|
||||||
pushPanelForConversation: PushPanelForConversationActionType;
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
|
toggleForwardMessageModal: (id: string) => void;
|
||||||
reactToMessage: (
|
reactToMessage: (
|
||||||
id: string,
|
id: string,
|
||||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||||
|
@ -75,42 +75,42 @@ type Trigger = {
|
||||||
*/
|
*/
|
||||||
export function TimelineMessage(props: Props): JSX.Element {
|
export function TimelineMessage(props: Props): JSX.Element {
|
||||||
const {
|
const {
|
||||||
i18n,
|
|
||||||
id,
|
|
||||||
author,
|
|
||||||
attachments,
|
attachments,
|
||||||
|
author,
|
||||||
|
canDeleteForEveryone,
|
||||||
canDownload,
|
canDownload,
|
||||||
canReact,
|
canReact,
|
||||||
canReply,
|
canReply,
|
||||||
canRetry,
|
canRetry,
|
||||||
canDeleteForEveryone,
|
|
||||||
canRetryDeleteForEveryone,
|
canRetryDeleteForEveryone,
|
||||||
contact,
|
contact,
|
||||||
payment,
|
|
||||||
conversationId,
|
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
deletedForEveryone,
|
conversationId,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
giftBadge,
|
giftBadge,
|
||||||
|
i18n,
|
||||||
|
id,
|
||||||
isSelected,
|
isSelected,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
|
kickOffAttachmentDownload,
|
||||||
|
payment,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
setQuoteByMessageId,
|
|
||||||
renderReactionPicker,
|
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
retryMessageSend,
|
renderReactionPicker,
|
||||||
retryDeleteForEveryone,
|
retryDeleteForEveryone,
|
||||||
|
retryMessageSend,
|
||||||
|
saveAttachment,
|
||||||
selectedReaction,
|
selectedReaction,
|
||||||
toggleForwardMessageModal,
|
setQuoteByMessageId,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
kickOffAttachmentDownload,
|
toggleForwardMessageModal,
|
||||||
saveAttachment,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
||||||
|
@ -410,7 +410,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
|
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
|
||||||
}
|
}
|
||||||
onMoreInfo={() =>
|
onMoreInfo={() =>
|
||||||
pushPanelForConversation(conversationId, {
|
pushPanelForConversation({
|
||||||
type: PanelType.MessageDetails,
|
type: PanelType.MessageDetails,
|
||||||
args: { messageId: id },
|
args: { messageId: id },
|
||||||
})
|
})
|
||||||
|
|
|
@ -441,7 +441,7 @@ export function ConversationDetails({
|
||||||
}
|
}
|
||||||
label={i18n('showChatColorEditor')}
|
label={i18n('showChatColorEditor')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.ChatColorEditor,
|
type: PanelType.ChatColorEditor,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -464,7 +464,7 @@ export function ConversationDetails({
|
||||||
}
|
}
|
||||||
label={i18n('ConversationDetails--notifications')}
|
label={i18n('ConversationDetails--notifications')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.NotificationSettings,
|
type: PanelType.NotificationSettings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -520,7 +520,7 @@ export function ConversationDetails({
|
||||||
}
|
}
|
||||||
label={i18n('ConversationDetails--group-link')}
|
label={i18n('ConversationDetails--group-link')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.GroupLinkManagement,
|
type: PanelType.GroupLinkManagement,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -536,7 +536,7 @@ export function ConversationDetails({
|
||||||
}
|
}
|
||||||
label={i18n('ConversationDetails--requests-and-invites')}
|
label={i18n('ConversationDetails--requests-and-invites')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.GroupInvites,
|
type: PanelType.GroupInvites,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -552,7 +552,7 @@ export function ConversationDetails({
|
||||||
}
|
}
|
||||||
label={i18n('permissions')}
|
label={i18n('permissions')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.GroupPermissions,
|
type: PanelType.GroupPermissions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -566,7 +566,7 @@ export function ConversationDetails({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
loadRecentMediaItems={loadRecentMediaItems}
|
loadRecentMediaItems={loadRecentMediaItems}
|
||||||
showAllMedia={() =>
|
showAllMedia={() =>
|
||||||
pushPanelForConversation(conversation.id, {
|
pushPanelForConversation({
|
||||||
type: PanelType.AllMedia,
|
type: PanelType.AllMedia,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import {
|
import {
|
||||||
groupBy,
|
|
||||||
difference,
|
difference,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
isEqual,
|
isEqual,
|
||||||
|
@ -14,7 +13,6 @@ import {
|
||||||
omit,
|
omit,
|
||||||
partition,
|
partition,
|
||||||
pick,
|
pick,
|
||||||
reject,
|
|
||||||
union,
|
union,
|
||||||
without,
|
without,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
@ -43,7 +41,6 @@ import { drop } from '../util/drop';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
import type { ConversationModel } from './conversations';
|
import type { ConversationModel } from './conversations';
|
||||||
import type { Contact as SmartMessageDetailContact } from '../state/smart/MessageDetail';
|
|
||||||
import { getCallingNotificationText } from '../util/callingNotification';
|
import { getCallingNotificationText } from '../util/callingNotification';
|
||||||
import type {
|
import type {
|
||||||
ProcessedDataMessage,
|
ProcessedDataMessage,
|
||||||
|
@ -51,7 +48,6 @@ import type {
|
||||||
ProcessedUnidentifiedDeliveryStatus,
|
ProcessedUnidentifiedDeliveryStatus,
|
||||||
CallbackResultType,
|
CallbackResultType,
|
||||||
} from '../textsecure/Types.d';
|
} from '../textsecure/Types.d';
|
||||||
import type { Props as PropsForMessageDetails } from '../components/conversation/MessageDetail';
|
|
||||||
import { SendMessageProtoError } from '../textsecure/Errors';
|
import { SendMessageProtoError } from '../textsecure/Errors';
|
||||||
import * as expirationTimer from '../util/expirationTimer';
|
import * as expirationTimer from '../util/expirationTimer';
|
||||||
import { getUserLanguages } from '../util/userLanguages';
|
import { getUserLanguages } from '../util/userLanguages';
|
||||||
|
@ -76,7 +72,6 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
|
||||||
import {
|
import {
|
||||||
SendActionType,
|
SendActionType,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
isMessageJustForMe,
|
|
||||||
isSent,
|
isSent,
|
||||||
sendStateReducer,
|
sendStateReducer,
|
||||||
someSendStatus,
|
someSendStatus,
|
||||||
|
@ -100,7 +95,6 @@ import {
|
||||||
getAttachmentsForMessage,
|
getAttachmentsForMessage,
|
||||||
getMessagePropStatus,
|
getMessagePropStatus,
|
||||||
getPropsForCallHistory,
|
getPropsForCallHistory,
|
||||||
getPropsForMessage,
|
|
||||||
hasErrors,
|
hasErrors,
|
||||||
isCallHistory,
|
isCallHistory,
|
||||||
isChatSessionRefreshed,
|
isChatSessionRefreshed,
|
||||||
|
@ -129,8 +123,6 @@ import {
|
||||||
getCallSelector,
|
getCallSelector,
|
||||||
getActiveCall,
|
getActiveCall,
|
||||||
} from '../state/selectors/calling';
|
} from '../state/selectors/calling';
|
||||||
import { getAccountSelector } from '../state/selectors/accounts';
|
|
||||||
import { getContactNameColorSelector } from '../state/selectors/conversations';
|
|
||||||
import {
|
import {
|
||||||
MessageReceipts,
|
MessageReceipts,
|
||||||
MessageReceiptType,
|
MessageReceiptType,
|
||||||
|
@ -263,11 +255,6 @@ async function shouldReplyNotifyUser(
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
export type MinimalPropsForMessageDetails = Pick<
|
|
||||||
PropsForMessageDetails,
|
|
||||||
'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
|
|
||||||
>;
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
const { Message: TypedMessage } = window.Signal.Types;
|
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
|
// Dependencies of prop-generation functions
|
||||||
getConversation(): ConversationModel | undefined {
|
getConversation(): ConversationModel | undefined {
|
||||||
return window.ConversationController.get(this.get('conversationId'));
|
return window.ConversationController.get(this.get('conversationId'));
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
||||||
// State
|
// State
|
||||||
import { createApp } from './state/roots/createApp';
|
import { createApp } from './state/roots/createApp';
|
||||||
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||||
import { createMessageDetail } from './state/roots/createMessageDetail';
|
|
||||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||||
|
|
||||||
|
@ -395,7 +394,6 @@ export const setup = (options: {
|
||||||
const Roots = {
|
const Roots = {
|
||||||
createApp,
|
createApp,
|
||||||
createGroupV2JoinModal,
|
createGroupV2JoinModal,
|
||||||
createMessageDetail,
|
|
||||||
createSafetyNumberViewer,
|
createSafetyNumberViewer,
|
||||||
createShortcutGuideModal,
|
createShortcutGuideModal,
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,9 +6,11 @@ import type { ThunkAction } from 'redux-thunk';
|
||||||
import * as Errors from '../../types/errors';
|
import * as Errors from '../../types/errors';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
import { getUuidsForE164s } from '../../util/getUuidsForE164s';
|
import { getUuidsForE164s } from '../../util/getUuidsForE164s';
|
||||||
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
|
|
||||||
|
@ -36,6 +38,10 @@ export const actions = {
|
||||||
checkForAccount,
|
checkForAccount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAccountsActions = (): BoundActionCreatorsMapObject<
|
||||||
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
function checkForAccount(
|
function checkForAccount(
|
||||||
phoneNumber: string
|
phoneNumber: string
|
||||||
): ThunkAction<
|
): ThunkAction<
|
||||||
|
|
|
@ -125,11 +125,12 @@ import {
|
||||||
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
|
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
|
||||||
} from '../../groups';
|
} from '../../groups';
|
||||||
import { getMessageById } from '../../messages/getMessageById';
|
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 type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
|
||||||
import { isOlderThan } from '../../util/timestamp';
|
import { isOlderThan } from '../../util/timestamp';
|
||||||
import { DAY } from '../../util/durations';
|
import { DAY } from '../../util/durations';
|
||||||
import { isNotNil } from '../../util/isNotNil';
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
|
import { PanelType } from '../../types/Panels';
|
||||||
import { startConversation } from '../../util/startConversation';
|
import { startConversation } from '../../util/startConversation';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
@ -407,6 +408,7 @@ export type ConversationsStateType = {
|
||||||
selectedMessageCounter: number;
|
selectedMessageCounter: number;
|
||||||
selectedMessageSource: SelectedMessageSource | undefined;
|
selectedMessageSource: SelectedMessageSource | undefined;
|
||||||
selectedConversationPanels: Array<PanelRenderType>;
|
selectedConversationPanels: Array<PanelRenderType>;
|
||||||
|
selectedMessageForDetails?: MessageAttributesType;
|
||||||
showArchived: boolean;
|
showArchived: boolean;
|
||||||
composer?: ComposerStateType;
|
composer?: ComposerStateType;
|
||||||
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
||||||
|
@ -817,11 +819,11 @@ type ReplaceAvatarsActionType = {
|
||||||
export type ConversationActionType =
|
export type ConversationActionType =
|
||||||
| CancelVerificationDataByConversationActionType
|
| CancelVerificationDataByConversationActionType
|
||||||
| ClearCancelledVerificationActionType
|
| ClearCancelledVerificationActionType
|
||||||
| ClearVerificationDataByConversationActionType
|
|
||||||
| ClearGroupCreationErrorActionType
|
| ClearGroupCreationErrorActionType
|
||||||
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
||||||
| ClearSelectedMessageActionType
|
| ClearSelectedMessageActionType
|
||||||
| ClearUnreadMetricsActionType
|
| ClearUnreadMetricsActionType
|
||||||
|
| ClearVerificationDataByConversationActionType
|
||||||
| CloseContactSpoofingReviewActionType
|
| CloseContactSpoofingReviewActionType
|
||||||
| CloseMaximumGroupSizeModalActionType
|
| CloseMaximumGroupSizeModalActionType
|
||||||
| CloseRecommendedGroupSizeModalActionType
|
| CloseRecommendedGroupSizeModalActionType
|
||||||
|
@ -843,6 +845,7 @@ export type ConversationActionType =
|
||||||
| MessageChangedActionType
|
| MessageChangedActionType
|
||||||
| MessageDeletedActionType
|
| MessageDeletedActionType
|
||||||
| MessageExpandedActionType
|
| MessageExpandedActionType
|
||||||
|
| MessageExpiredActionType
|
||||||
| MessageSelectedActionType
|
| MessageSelectedActionType
|
||||||
| MessagesAddedActionType
|
| MessagesAddedActionType
|
||||||
| MessagesResetActionType
|
| MessagesResetActionType
|
||||||
|
@ -871,8 +874,8 @@ export type ConversationActionType =
|
||||||
| ShowSendAnywayDialogActionType
|
| ShowSendAnywayDialogActionType
|
||||||
| StartComposingActionType
|
| StartComposingActionType
|
||||||
| StartSettingGroupMetadataActionType
|
| StartSettingGroupMetadataActionType
|
||||||
| ToggleConversationInChooseMembersActionType
|
| ToggleComposeEditingAvatarActionType
|
||||||
| ToggleComposeEditingAvatarActionType;
|
| ToggleConversationInChooseMembersActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
|
@ -1515,7 +1518,7 @@ function deleteMessage({
|
||||||
} else {
|
} else {
|
||||||
conversation.decrementMessageCount();
|
conversation.decrementMessageCount();
|
||||||
}
|
}
|
||||||
popPanelForConversation(conversationId)(dispatch, getState, undefined);
|
popPanelForConversation()(dispatch, getState, undefined);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'NOOP',
|
type: 'NOOP',
|
||||||
|
@ -2536,44 +2539,50 @@ function setIsFetchingUUID(
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PushPanelForConversationActionType = (
|
export type PushPanelForConversationActionType = (
|
||||||
conversationId: string,
|
panel: PanelRequestType
|
||||||
panel: PanelRenderType
|
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
|
||||||
function pushPanelForConversation(
|
function pushPanelForConversation(
|
||||||
conversationId: string,
|
panel: PanelRequestType
|
||||||
panel: PanelRenderType
|
): ThunkAction<void, RootStateType, unknown, PushPanelActionType> {
|
||||||
): PushPanelActionType {
|
return async dispatch => {
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
if (panel.type === PanelType.MessageDetails) {
|
||||||
if (!conversation) {
|
const { messageId } = panel.args;
|
||||||
throw new Error(
|
|
||||||
`addPanelToConversation: No conversation found for conversation ${conversationId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
dispatch({
|
||||||
type: PUSH_PANEL,
|
type: PUSH_PANEL,
|
||||||
payload: panel,
|
payload: panel,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopPanelForConversationActionType = (
|
export type PopPanelForConversationActionType = () => unknown;
|
||||||
conversationId: string
|
|
||||||
) => unknown;
|
|
||||||
|
|
||||||
function popPanelForConversation(
|
function popPanelForConversation(): ThunkAction<
|
||||||
conversationId: string
|
void,
|
||||||
): ThunkAction<void, RootStateType, unknown, PopPanelActionType> {
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
PopPanelActionType
|
||||||
|
> {
|
||||||
return (dispatch, getState) => {
|
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 { conversations } = getState();
|
||||||
const { selectedConversationPanels } = conversations;
|
const { selectedConversationPanels } = conversations;
|
||||||
|
|
||||||
|
@ -2581,14 +2590,6 @@ function popPanelForConversation(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panel = [...selectedConversationPanels].pop();
|
|
||||||
|
|
||||||
if (!panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
conversation.trigger('popPanel', panel);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: POP_PANEL,
|
type: POP_PANEL,
|
||||||
payload: null,
|
payload: null,
|
||||||
|
@ -3718,6 +3719,30 @@ function visitListsInVerificationData(
|
||||||
return result;
|
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(
|
export function reducer(
|
||||||
state: Readonly<ConversationsStateType> = getEmptyState(),
|
state: Readonly<ConversationsStateType> = getEmptyState(),
|
||||||
action: Readonly<ConversationActionType | StoryDistributionListsActionType>
|
action: Readonly<ConversationActionType | StoryDistributionListsActionType>
|
||||||
|
@ -4242,18 +4267,26 @@ export function reducer(
|
||||||
verificationDataByConversation,
|
verificationDataByConversation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'MESSAGE_CHANGED') {
|
if (action.type === 'MESSAGE_CHANGED') {
|
||||||
const { id, conversationId, data } = action.payload;
|
const { id, conversationId, data } = action.payload;
|
||||||
const existingConversation = state.messagesByConversation[conversationId];
|
const existingConversation = state.messagesByConversation[conversationId];
|
||||||
|
|
||||||
// We don't keep track of messages unless their conversation is loaded...
|
// We don't keep track of messages unless their conversation is loaded...
|
||||||
if (!existingConversation) {
|
if (!existingConversation) {
|
||||||
return state;
|
return maybeUpdateSelectedMessageForDetails(
|
||||||
|
{ messageId: id, selectedMessageForDetails: data },
|
||||||
|
state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and we've already loaded that message once
|
// ...and we've already loaded that message once
|
||||||
const existingMessage = getOwn(state.messagesLookup, id);
|
const existingMessage = getOwn(state.messagesLookup, id);
|
||||||
if (!existingMessage) {
|
if (!existingMessage) {
|
||||||
return state;
|
return maybeUpdateSelectedMessageForDetails(
|
||||||
|
{ messageId: id, selectedMessageForDetails: data },
|
||||||
|
state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationAttrs = state.conversationLookup[conversationId];
|
const conversationAttrs = state.conversationLookup[conversationId];
|
||||||
|
@ -4265,7 +4298,13 @@ export function reducer(
|
||||||
const toIncrement = data.reactions?.length ? 1 : 0;
|
const toIncrement = data.reactions?.length ? 1 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...maybeUpdateSelectedMessageForDetails(
|
||||||
|
{
|
||||||
|
messageId: id,
|
||||||
|
selectedMessageForDetails: data,
|
||||||
|
},
|
||||||
|
state
|
||||||
|
),
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
...state.messagesByConversation,
|
...state.messagesByConversation,
|
||||||
[conversationId]: {
|
[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') {
|
if (action.type === 'MESSAGE_EXPANDED') {
|
||||||
const { id, displayLimit } = action.payload;
|
const { id, displayLimit } = action.payload;
|
||||||
|
|
||||||
|
@ -4459,7 +4506,10 @@ export function reducer(
|
||||||
|
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
if (!existingConversation) {
|
if (!existingConversation) {
|
||||||
return state;
|
return maybeUpdateSelectedMessageForDetails(
|
||||||
|
{ messageId: id, selectedMessageForDetails: undefined },
|
||||||
|
state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming that we always have contiguous groups of messages in memory, the removal
|
// Assuming that we always have contiguous groups of messages in memory, the removal
|
||||||
|
@ -4503,7 +4553,10 @@ export function reducer(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...maybeUpdateSelectedMessageForDetails(
|
||||||
|
{ messageId: id, selectedMessageForDetails: undefined },
|
||||||
|
state
|
||||||
|
),
|
||||||
messagesLookup: omit(messagesLookup, id),
|
messagesLookup: omit(messagesLookup, id),
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
|
@ -4792,6 +4845,17 @@ export function reducer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === PUSH_PANEL) {
|
if (action.type === PUSH_PANEL) {
|
||||||
|
if (action.payload.type === PanelType.MessageDetails) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedConversationPanels: [
|
||||||
|
...state.selectedConversationPanels,
|
||||||
|
action.payload,
|
||||||
|
],
|
||||||
|
selectedMessageForDetails: action.payload.args.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedConversationPanels: [
|
selectedConversationPanels: [
|
||||||
|
@ -4804,7 +4868,19 @@ export function reducer(
|
||||||
if (action.type === POP_PANEL) {
|
if (action.type === POP_PANEL) {
|
||||||
const { selectedConversationPanels } = state;
|
const { selectedConversationPanels } = state;
|
||||||
const nextPanels = [...selectedConversationPanels];
|
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 {
|
return {
|
||||||
...state,
|
...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 { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
import { reduce } from '../../util/iterables';
|
import { reduce } from '../../util/iterables';
|
||||||
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
|
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
|
||||||
import type { ReactPanelRenderType, PanelRenderType } from '../../types/Panels';
|
import type { PanelRenderType } from '../../types/Panels';
|
||||||
import { isPanelHandledByReact } from '../../types/Panels';
|
|
||||||
|
|
||||||
let placeholderContact: ConversationType;
|
let placeholderContact: ConversationType;
|
||||||
export const getPlaceholderContact = (): ConversationType => {
|
export const getPlaceholderContact = (): ConversationType => {
|
||||||
|
@ -1135,7 +1134,7 @@ export const getHideStoryConversationIds = createSelector(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTopPanel = createSelector(
|
export const getTopPanel = createSelector(
|
||||||
getConversations,
|
getConversations,
|
||||||
(conversations): PanelRenderType | undefined =>
|
(conversations): PanelRenderType | undefined =>
|
||||||
conversations.selectedConversationPanels[
|
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(
|
export const getConversationTitle = createSelector(
|
||||||
getIntl,
|
getIntl,
|
||||||
getTopPanel,
|
getTopPanel,
|
||||||
|
|
|
@ -1,19 +1,35 @@
|
||||||
// Copyright 2021-2022 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { createSelector, createSelectorCreator } from 'reselect';
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
import getDirection from 'direction';
|
import getDirection from 'direction';
|
||||||
import emojiRegex from 'emoji-regex';
|
import emojiRegex from 'emoji-regex';
|
||||||
import LinkifyIt from 'linkify-it';
|
import LinkifyIt from 'linkify-it';
|
||||||
|
|
||||||
|
import type { StateType } from '../reducer';
|
||||||
import type {
|
import type {
|
||||||
LastMessageStatus,
|
LastMessageStatus,
|
||||||
|
MessageAttributesType,
|
||||||
MessageReactionType,
|
MessageReactionType,
|
||||||
ShallowChallengeError,
|
ShallowChallengeError,
|
||||||
} from '../../model-types.d';
|
} from '../../model-types.d';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Contact as SmartMessageDetailContact,
|
||||||
|
OwnProps as SmartMessageDetailPropsType,
|
||||||
|
} from '../smart/MessageDetail';
|
||||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
import type { PropsData } from '../../components/conversation/Message';
|
import type { PropsData } from '../../components/conversation/Message';
|
||||||
import type { PropsData as TimelineMessagePropsData } from '../../components/conversation/TimelineMessage';
|
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 type { CallingNotificationType } from '../../util/callingNotification';
|
||||||
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { getRecipients } from '../../util/getRecipients';
|
||||||
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { isNotNil } from '../../util/isNotNil';
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
import { isMoreRecentThan } from '../../util/timestamp';
|
import { isMoreRecentThan } from '../../util/timestamp';
|
||||||
import * as iterables from '../../util/iterables';
|
import * as iterables from '../../util/iterables';
|
||||||
|
@ -65,10 +83,11 @@ import {
|
||||||
isMissingRequiredProfileSharing,
|
isMissingRequiredProfileSharing,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import {
|
import {
|
||||||
|
getIntl,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
|
getUserACI,
|
||||||
getUserConversationId,
|
getUserConversationId,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
getUserACI,
|
|
||||||
getUserPNI,
|
getUserPNI,
|
||||||
} from './user';
|
} from './user';
|
||||||
|
|
||||||
|
@ -1937,3 +1956,172 @@ export function getLastChallengeError(
|
||||||
|
|
||||||
return challengeErrors.pop();
|
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 React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import type { PanelRenderType } from '../../types/Panels';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { ReactPanelRenderType } from '../../types/Panels';
|
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
||||||
import { ConversationView } from '../../components/conversation/ConversationView';
|
import { ConversationView } from '../../components/conversation/ConversationView';
|
||||||
|
@ -18,11 +18,12 @@ import { SmartConversationNotificationsSettings } from './ConversationNotificati
|
||||||
import { SmartGV1Members } from './GV1Members';
|
import { SmartGV1Members } from './GV1Members';
|
||||||
import { SmartGroupLinkManagement } from './GroupLinkManagement';
|
import { SmartGroupLinkManagement } from './GroupLinkManagement';
|
||||||
import { SmartGroupV2Permissions } from './GroupV2Permissions';
|
import { SmartGroupV2Permissions } from './GroupV2Permissions';
|
||||||
|
import { SmartMessageDetail } from './MessageDetail';
|
||||||
import { SmartPendingInvites } from './PendingInvites';
|
import { SmartPendingInvites } from './PendingInvites';
|
||||||
import { SmartStickerManager } from './StickerManager';
|
import { SmartStickerManager } from './StickerManager';
|
||||||
import { SmartTimeline } from './Timeline';
|
import { SmartTimeline } from './Timeline';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getTopPanelRenderableByReact } from '../selectors/conversations';
|
import { getTopPanel } from '../selectors/conversations';
|
||||||
import { useComposerActions } from '../ducks/composer';
|
import { useComposerActions } from '../ducks/composer';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
|
|
||||||
|
@ -33,10 +34,10 @@ export type PropsType = {
|
||||||
export function SmartConversationView({
|
export function SmartConversationView({
|
||||||
conversationId,
|
conversationId,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const { startConversation } = useConversationsActions();
|
const topPanel = useSelector<StateType, PanelRenderType | undefined>(
|
||||||
const topPanel = useSelector<StateType, ReactPanelRenderType | undefined>(
|
getTopPanel
|
||||||
getTopPanelRenderableByReact
|
|
||||||
);
|
);
|
||||||
|
const { startConversation } = useConversationsActions();
|
||||||
|
|
||||||
const { processAttachments } = useComposerActions();
|
const { processAttachments } = useComposerActions();
|
||||||
const i18n = useSelector(getIntl);
|
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) {
|
if (topPanel.type === PanelType.NotificationSettings) {
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
|
|
|
@ -1,63 +1,101 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail';
|
||||||
import { MessageDetail } 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 { 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 { Contact } from '../../components/conversation/MessageDetail';
|
||||||
export type PropsWithExtraFunctions = MinimalPropsForMessageDetails &
|
export type OwnProps = Pick<
|
||||||
Pick<
|
MessageDetailProps,
|
||||||
MessageDetailProps,
|
'contacts' | 'errors' | 'message' | 'receivedAt'
|
||||||
| 'contactNameColor'
|
>;
|
||||||
| 'getPreferredBadge'
|
|
||||||
| 'i18n'
|
|
||||||
| 'interactionMode'
|
|
||||||
| 'renderAudioAttachment'
|
|
||||||
| 'theme'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const mapStateToProps = (
|
export function SmartMessageDetail(): JSX.Element | null {
|
||||||
state: StateType,
|
const getContactNameColor = useSelector(getContactNameColorSelector);
|
||||||
props: MinimalPropsForMessageDetails
|
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||||
): PropsWithExtraFunctions => {
|
const i18n = useSelector(getIntl);
|
||||||
const { contacts, errors, message, receivedAt, sentAt } = props;
|
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 =
|
const contactNameColor =
|
||||||
message.conversationType === 'group'
|
message.conversationType === 'group'
|
||||||
? getContactNameColorSelector(state)(
|
? getContactNameColor(message.conversationId, message.author.id)
|
||||||
message.conversationId,
|
|
||||||
message.author.id
|
|
||||||
)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const getPreferredBadge = getPreferredBadgeSelector(state);
|
return (
|
||||||
|
<MessageDetail
|
||||||
return {
|
checkForAccount={checkForAccount}
|
||||||
contacts,
|
clearSelectedMessage={clearSelectedMessage}
|
||||||
contactNameColor,
|
contactNameColor={contactNameColor}
|
||||||
errors,
|
contacts={contacts}
|
||||||
message,
|
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||||
receivedAt,
|
errors={errors}
|
||||||
sentAt,
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
i18n={i18n}
|
||||||
getPreferredBadge,
|
interactionMode={interactionMode}
|
||||||
i18n: getIntl(state),
|
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||||
interactionMode: getInteractionMode(state),
|
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||||
theme: getTheme(state),
|
message={message}
|
||||||
|
openGiftBadge={openGiftBadge}
|
||||||
renderAudioAttachment,
|
pushPanelForConversation={pushPanelForConversation}
|
||||||
};
|
receivedAt={receivedAt}
|
||||||
};
|
renderAudioAttachment={renderAudioAttachment}
|
||||||
|
saveAttachment={saveAttachment}
|
||||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
sentAt={message.timestamp}
|
||||||
export const SmartMessageDetail = smart(MessageDetail);
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { EmbeddedContactType } from './EmbeddedContact';
|
import type { EmbeddedContactType } from './EmbeddedContact';
|
||||||
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import type { UUIDStringType } from './UUID';
|
import type { UUIDStringType } from './UUID';
|
||||||
|
|
||||||
export enum PanelType {
|
export enum PanelType {
|
||||||
|
@ -18,7 +19,7 @@ export enum PanelType {
|
||||||
StickerManager = 'StickerManager',
|
StickerManager = 'StickerManager',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactPanelRenderType =
|
export type PanelRequestType =
|
||||||
| { type: PanelType.AllMedia }
|
| { type: PanelType.AllMedia }
|
||||||
| { type: PanelType.ChatColorEditor }
|
| { type: PanelType.ChatColorEditor }
|
||||||
| {
|
| {
|
||||||
|
@ -36,33 +37,28 @@ export type ReactPanelRenderType =
|
||||||
| { type: PanelType.GroupLinkManagement }
|
| { type: PanelType.GroupLinkManagement }
|
||||||
| { type: PanelType.GroupPermissions }
|
| { type: PanelType.GroupPermissions }
|
||||||
| { type: PanelType.GroupV1Members }
|
| { type: PanelType.GroupV1Members }
|
||||||
|
| { type: PanelType.MessageDetails; args: { messageId: string } }
|
||||||
| { type: PanelType.NotificationSettings }
|
| { type: PanelType.NotificationSettings }
|
||||||
| { type: PanelType.StickerManager };
|
| { type: PanelType.StickerManager };
|
||||||
|
|
||||||
export type BackbonePanelRenderType = {
|
export type PanelRenderType =
|
||||||
type: PanelType.MessageDetails;
|
| { type: PanelType.AllMedia }
|
||||||
args: { messageId: string };
|
| { type: PanelType.ChatColorEditor }
|
||||||
};
|
| {
|
||||||
|
type: PanelType.ContactDetails;
|
||||||
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
|
args: {
|
||||||
|
contact: EmbeddedContactType;
|
||||||
export function isPanelHandledByReact(
|
signalAccount?: {
|
||||||
panel: PanelRenderType
|
phoneNumber: string;
|
||||||
): panel is ReactPanelRenderType {
|
uuid: UUIDStringType;
|
||||||
if (!panel) {
|
};
|
||||||
return false;
|
};
|
||||||
}
|
}
|
||||||
|
| { type: PanelType.ConversationDetails }
|
||||||
return (
|
| { type: PanelType.GroupInvites }
|
||||||
panel.type === PanelType.AllMedia ||
|
| { type: PanelType.GroupLinkManagement }
|
||||||
panel.type === PanelType.ChatColorEditor ||
|
| { type: PanelType.GroupPermissions }
|
||||||
panel.type === PanelType.ContactDetails ||
|
| { type: PanelType.GroupV1Members }
|
||||||
panel.type === PanelType.ConversationDetails ||
|
| { type: PanelType.MessageDetails; args: { message: MessageAttributesType } }
|
||||||
panel.type === PanelType.GroupInvites ||
|
| { type: PanelType.NotificationSettings }
|
||||||
panel.type === PanelType.GroupLinkManagement ||
|
| { type: PanelType.StickerManager };
|
||||||
panel.type === PanelType.GroupPermissions ||
|
|
||||||
panel.type === PanelType.GroupV1Members ||
|
|
||||||
panel.type === PanelType.NotificationSettings ||
|
|
||||||
panel.type === PanelType.StickerManager
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,23 +17,11 @@ import {
|
||||||
removeLinkPreview,
|
removeLinkPreview,
|
||||||
suspendLinkPreviews,
|
suspendLinkPreviews,
|
||||||
} from '../services/LinkPreview';
|
} 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';
|
import { UUIDKind } from '../types/UUID';
|
||||||
|
|
||||||
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
|
|
||||||
|
|
||||||
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
// Sub-views
|
// Sub-views
|
||||||
private contactModalView?: Backbone.View;
|
|
||||||
private conversationView?: 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
constructor(...args: Array<any>) {
|
constructor(...args: Array<any>) {
|
||||||
|
@ -48,9 +36,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.unload(`model trigger - ${reason}`)
|
this.unload(`model trigger - ${reason}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.listenTo(this.model, 'pushPanel', this.pushPanel);
|
|
||||||
this.listenTo(this.model, 'popPanel', this.popPanel);
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
this.setupConversationView();
|
this.setupConversationView();
|
||||||
|
@ -139,22 +124,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
|
|
||||||
this.conversationView?.remove();
|
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();
|
removeLinkPreview();
|
||||||
suspendLinkPreviews();
|
suspendLinkPreviews();
|
||||||
|
|
||||||
|
@ -226,143 +195,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
|
|
||||||
void this.model.updateVerified();
|
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;
|
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 { createStore } from './state/createStore';
|
||||||
import type { createApp } from './state/roots/createApp';
|
import type { createApp } from './state/roots/createApp';
|
||||||
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||||
import type { createMessageDetail } from './state/roots/createMessageDetail';
|
|
||||||
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||||
import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||||
import type * as appDuck from './state/ducks/app';
|
import type * as appDuck from './state/ducks/app';
|
||||||
|
@ -161,7 +160,6 @@ export type SignalCoreType = {
|
||||||
Roots: {
|
Roots: {
|
||||||
createApp: typeof createApp;
|
createApp: typeof createApp;
|
||||||
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
||||||
createMessageDetail: typeof createMessageDetail;
|
|
||||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue