Ensure consistency in forwarding logic

This commit is contained in:
trevor-signal 2025-05-27 16:59:50 -04:00 committed by GitHub
parent 38666fe0a4
commit 15263c2d16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 72 additions and 9 deletions

View file

@ -199,6 +199,7 @@ export type OwnProps = Readonly<{
props: SmartCompositionRecordingDraftProps
) => JSX.Element | null;
selectedMessageIds: ReadonlyArray<string> | undefined;
areSelectedMessagesForwardable: boolean | undefined;
toggleSelectMode: (on: boolean) => void;
toggleForwardMessagesModal: (
payload: ForwardMessagesPayload,
@ -367,6 +368,7 @@ export const CompositionArea = memo(function CompositionArea({
renderSmartCompositionRecordingDraft,
// Selected messages
selectedMessageIds,
areSelectedMessagesForwardable,
toggleSelectMode,
toggleForwardMessagesModal,
// DraftGifMessageSendModal
@ -906,6 +908,7 @@ export const CompositionArea = memo(function CompositionArea({
<SelectModeActions
i18n={i18n}
selectedMessageIds={selectedMessageIds}
areSelectedMessagesForwardable={areSelectedMessagesForwardable === true}
onExitSelectMode={() => {
toggleSelectMode(false);
}}

View file

@ -74,6 +74,7 @@ const defaultMessageProps: TimelineMessagesProps = {
}),
canCopy: true,
canEditMessage: true,
canForward: true,
canReact: true,
canReply: true,
canRetry: true,

View file

@ -12,6 +12,7 @@ const MAX_FORWARD_COUNT = 30;
type SelectModeActionsProps = Readonly<{
selectedMessageIds: ReadonlyArray<string>;
areSelectedMessagesForwardable: boolean;
onExitSelectMode: () => void;
onDeleteMessages: () => void;
onForwardMessages: () => void;
@ -21,6 +22,7 @@ type SelectModeActionsProps = Readonly<{
export default function SelectModeActions({
selectedMessageIds,
areSelectedMessagesForwardable,
onExitSelectMode,
onDeleteMessages,
onForwardMessages,
@ -31,7 +33,10 @@ export default function SelectModeActions({
const tooManyMessagesToForward =
selectedMessageIds.length > MAX_FORWARD_COUNT;
const canForward = hasSelectedMessages && !tooManyMessagesToForward;
const canForward =
hasSelectedMessages &&
areSelectedMessagesForwardable &&
!tooManyMessagesToForward;
const canDelete = hasSelectedMessages;
return (

View file

@ -51,6 +51,7 @@ function mockMessageTimelineItem(
canDeleteForEveryone: false,
canDownload: true,
canEditMessage: true,
canForward: true,
canReact: true,
canReply: true,
canRetry: true,

View file

@ -255,6 +255,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
canReply: true,
canDownload: true,
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
canForward: true,
canRetry: overrideProps.canRetry || false,
canRetryDeleteForEveryone: overrideProps.canRetryDeleteForEveryone || false,
checkForAccount: action('checkForAccount'),
@ -266,7 +267,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
conversationId: overrideProps.conversationId ?? '',
conversationType: overrideProps.conversationType || 'direct',
contact: overrideProps.contact,
deletedForEveryone: overrideProps.deletedForEveryone,
// disableMenu: overrideProps.disableMenu,
disableScroll: overrideProps.disableScroll,
direction: overrideProps.direction || 'incoming',
@ -796,11 +796,13 @@ export function Deleted(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
deletedForEveryone: true,
canForward: false,
status: 'sent',
});
const propsSending = createProps({
conversationType: 'direct',
deletedForEveryone: true,
canForward: false,
status: 'sending',
});
@ -817,6 +819,7 @@ DeletedWithExpireTimer.args = {
timestamp: Date.now() - 60 * 1000,
conversationType: 'group',
deletedForEveryone: true,
canForward: false,
expirationLength: 5 * 60 * 1000,
expirationTimestamp: Date.now() + 3 * 60 * 1000,
status: 'sent',
@ -2265,6 +2268,7 @@ const fullContact = {
export const EmbeddedContactFullContact = Template.bind({});
EmbeddedContactFullContact.args = {
contact: fullContact,
canForward: false,
};
export const EmbeddedContactAvatarUndownloaded = Template.bind({});
@ -2279,6 +2283,7 @@ EmbeddedContactAvatarUndownloaded.args = {
isProfile: true,
},
},
canForward: false,
};
export const EmbeddedContactAvatarDownloading = Template.bind({});
EmbeddedContactAvatarDownloading.args = {
@ -2295,6 +2300,7 @@ EmbeddedContactAvatarDownloading.args = {
isProfile: true,
},
},
canForward: false,
};
export const EmbeddedContactAvatarTransientError = Template.bind({});
EmbeddedContactAvatarTransientError.args = {
@ -2316,6 +2322,7 @@ EmbeddedContactAvatarTransientError.args = {
isProfile: true,
},
},
canForward: false,
};
export const EmbeddedContactAvatarPermanentError = Template.bind({});
EmbeddedContactAvatarPermanentError.args = {
@ -2335,6 +2342,7 @@ EmbeddedContactAvatarPermanentError.args = {
isProfile: true,
},
},
canForward: false,
};
export const EmbeddedContactWithSendMessage = Template.bind({});
@ -2345,6 +2353,7 @@ EmbeddedContactWithSendMessage.args = {
serviceId: generateAci(),
},
direction: 'incoming',
canForward: false,
};
export const EmbeddedContactOnlyEmail = Template.bind({});
@ -2352,6 +2361,7 @@ EmbeddedContactOnlyEmail.args = {
contact: {
email: fullContact.email,
},
canForward: false,
};
export const EmbeddedContactGivenName = Template.bind({});
@ -2361,6 +2371,7 @@ EmbeddedContactGivenName.args = {
givenName: 'Jerry',
},
},
canForward: false,
};
export const EmbeddedContactOrganization = Template.bind({});
@ -2368,6 +2379,7 @@ EmbeddedContactOrganization.args = {
contact: {
organization: 'Company 5',
},
canForward: false,
};
export const EmbeddedContactGivenFamilyName = Template.bind({});
@ -2378,6 +2390,7 @@ EmbeddedContactGivenFamilyName.args = {
familyName: 'FamilyName',
},
},
canForward: false,
};
export const EmbeddedContactFamilyName = Template.bind({});
@ -2387,6 +2400,7 @@ EmbeddedContactFamilyName.args = {
familyName: 'FamilyName',
},
},
canForward: false,
};
export const GiftBadgeUnopened = Template.bind({});
@ -2397,6 +2411,7 @@ GiftBadgeUnopened.args = {
level: 3,
state: GiftBadgeStates.Unopened,
},
canForward: false,
};
export const GiftBadgeFailed = Template.bind({});
@ -2404,6 +2419,7 @@ GiftBadgeFailed.args = {
giftBadge: {
state: GiftBadgeStates.Failed,
},
canForward: false,
};
const getPreferredBadge = () => ({
@ -2430,6 +2446,7 @@ GiftBadgeRedeemed30Days.args = {
level: 3,
state: GiftBadgeStates.Redeemed,
},
canForward: false,
};
export const GiftBadgeRedeemed24Hours = Template.bind({});
@ -2441,6 +2458,7 @@ GiftBadgeRedeemed24Hours.args = {
level: 3,
state: GiftBadgeStates.Redeemed,
},
canForward: false,
};
export const GiftBadgeOpened60Minutes = Template.bind({});
@ -2452,6 +2470,7 @@ GiftBadgeOpened60Minutes.args = {
level: 3,
state: GiftBadgeStates.Opened,
},
canForward: false,
};
export const GiftBadgeRedeemed1Minute = Template.bind({});
@ -2463,6 +2482,7 @@ GiftBadgeRedeemed1Minute.args = {
level: 3,
state: GiftBadgeStates.Redeemed,
},
canForward: false,
};
export const GiftBadgeOpenedExpired = Template.bind({});
@ -2474,6 +2494,7 @@ GiftBadgeOpenedExpired.args = {
level: 3,
state: GiftBadgeStates.Opened,
},
canForward: false,
};
export const GiftBadgeMissingBadge = Template.bind({});
@ -2485,12 +2506,14 @@ GiftBadgeMissingBadge.args = {
level: 3,
state: GiftBadgeStates.Redeemed,
},
canForward: false,
};
export const PaymentNotification = Template.bind({});
PaymentNotification.args = {
canReply: false,
canReact: false,
canForward: false,
payment: {
kind: PaymentEventKind.Notification,
note: 'Hello there',

View file

@ -48,6 +48,7 @@ export type PropsData = {
canDownload: boolean;
canCopy: boolean;
canEditMessage: boolean;
canForward: boolean;
canRetry: boolean;
canRetryDeleteForEveryone: boolean;
canReact: boolean;
@ -96,6 +97,7 @@ export function TimelineMessage(props: Props): JSX.Element {
canDownload,
canCopy,
canEditMessage,
canForward,
canReact,
canReply,
canRetry,
@ -103,15 +105,11 @@ export function TimelineMessage(props: Props): JSX.Element {
containerElementRef,
containerWidthBreakpoint,
conversationId,
deletedForEveryone,
direction,
giftBadge,
i18n,
id,
isTargeted,
isTapToView,
kickOffAttachmentDownload,
payment,
copyMessageText,
pushPanelForConversation,
reactToMessage,
@ -255,8 +253,6 @@ export function TimelineMessage(props: Props): JSX.Element {
);
const handleContextMenu = useHandleMessageContextMenu(menuTriggerRef);
const canForward =
!isTapToView && !deletedForEveryone && !giftBadge && !payment;
const shouldShowAdditional =
doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow;

View file

@ -754,6 +754,7 @@ export const getPropsForMessage = (
canEditMessage: canEditMessage(message),
canDeleteForEveryone: canDeleteForEveryone(message, conversation.isMe),
canDownload: canDownload(message, conversationSelector),
canForward: canForward(message),
canReact: canReact(message, ourConversationId, conversationSelector),
canReply: canReply(message, ourConversationId, conversationSelector),
canRetry: hasErrors(message),
@ -2104,6 +2105,15 @@ export function canDownload(
return false;
}
export function canForward(message: ReadonlyMessageAttributesType): boolean {
return (
!isTapToView(message) &&
!message.deletedForEveryone &&
!message.giftBadge &&
!getPayment(message)
);
}
export function getLastChallengeError(
message: Pick<MessageWithUIFieldsType, 'errors'>
): ShallowChallengeError | undefined {

View file

@ -26,6 +26,7 @@ import {
getGroupAdminsSelector,
getHasPanelOpen,
getLastEditableMessageId,
getMessages,
getSelectedMessageIds,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
@ -37,7 +38,7 @@ import {
getShowStickersIntroduction,
getTextFormattingEnabled,
} from '../selectors/items';
import { getPropsForQuote } from '../selectors/message';
import { canForward, getPropsForQuote } from '../selectors/message';
import {
getBlessedStickerPacks,
getInstalledStickerPacks,
@ -96,6 +97,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
const recentEmojis = useSelector(selectRecentEmojis);
const selectedMessageIds = useSelector(getSelectedMessageIds);
const messageLookup = useSelector(getMessages);
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
const lastEditableMessageId = useSelector(getLastEditableMessageId);
const receivedPacks = useSelector(getReceivedStickerPacks);
@ -133,6 +135,16 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
shouldSendHighQualityAttachments,
} = composerState;
const areSelectedMessagesForwardable = useMemo(() => {
return selectedMessageIds?.every(messageId => {
const message = messageLookup[messageId];
if (!message) {
return false;
}
return canForward(message);
});
}, [messageLookup, selectedMessageIds]);
const isActive = useMemo(() => {
return !hasGlobalModalOpen && !hasPanelOpen;
}, [hasGlobalModalOpen, hasPanelOpen]);
@ -365,6 +377,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
sortedGroupMembers={conversation.sortedGroupMembers ?? null}
// Select Mode
selectedMessageIds={selectedMessageIds}
areSelectedMessagesForwardable={areSelectedMessagesForwardable}
toggleSelectMode={toggleSelectMode}
toggleForwardMessagesModal={toggleForwardMessagesModal}
// DraftGifMessageSendModal

View file

@ -23,6 +23,7 @@ import {
sortByMessageOrder,
type ForwardMessageData,
} from '../types/ForwardDraft';
import { canForward } from '../state/selectors/message';
export async function maybeForwardMessages(
messages: Array<ForwardMessageData>,
@ -36,6 +37,16 @@ export async function maybeForwardMessages(
.map(id => window.ConversationController.get(id))
.filter(isNotNil);
const areAllMessagesForwardable = messages.every(msg =>
msg.originalMessage ? canForward(msg.originalMessage) : true
);
if (!areAllMessagesForwardable) {
throw new Error(
'maybeForwardMessage: Attempting to forward unforwardable message(s)'
);
}
const cannotSend = conversations.some(
conversation =>
conversation?.get('announcementsOnly') && !conversation.areWeAdmin()