Moves message details into React pane land

This commit is contained in:
Josh Perez 2022-12-21 15:44:23 -05:00 committed by GitHub
parent 3def746014
commit a80c6d89a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 501 additions and 558 deletions

View file

@ -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;
}

View file

@ -462,7 +462,7 @@ export function CompositionArea({
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={() =>
pushPanelForConversation(conversationId, {
pushPanelForConversation({
type: PanelType.StickerManager,
})
}

View file

@ -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,
});
}

View file

@ -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,

View file

@ -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}

View file

@ -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 },
});

View file

@ -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 },
})

View file

@ -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,
})
}

View file

@ -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'));

View file

@ -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,
};

View file

@ -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<

View file

@ -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,

View file

@ -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>
);

View file

@ -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,

View file

@ -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),
};
}
);

View file

@ -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">

View file

@ -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}
/>
);
}

View file

@ -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 };

View file

@ -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
View file

@ -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;
};