diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index cc9a497d9..ae3abd7d9 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1196,6 +1196,8 @@ export function buildLocalForwardedMessage( message: ApiMessage, serverTimeOffset: number, scheduledAt?: number, + noAuthor?: boolean, + noCaption?: boolean, ): ApiMessage { const localId = getNextLocalMessageId(); const { @@ -1211,11 +1213,16 @@ export function buildLocalForwardedMessage( const asIncomingInChatWithSelf = ( toChat.id === currentUserId && (fromChatId !== toChat.id || message.forwardInfo) && !isAudio ); + const shouldHideText = Object.keys(content).length > 1 && content.text && noCaption; + const updatedContent = { + ...content, + text: !shouldHideText ? content.text : undefined, + }; return { id: localId, chatId: toChat.id, - content, + content: updatedContent, date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset, isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel', senderId: currentUserId, @@ -1223,7 +1230,7 @@ export function buildLocalForwardedMessage( groupedId, isInAlbum, // Forward info doesn't get added when users forwards his own messages, also when forwarding audio - ...(senderId !== currentUserId && !isAudio && { + ...(senderId !== currentUserId && !isAudio && !noAuthor && { forwardInfo: { date: message.date, isChannelPost: false, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 334e82cc4..b2ff184b8 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1126,6 +1126,8 @@ export async function forwardMessages({ scheduledAt, sendAs, withMyScore, + noAuthors, + noCaptions, }: { fromChat: ApiChat; toChat: ApiChat; @@ -1135,12 +1137,16 @@ export async function forwardMessages({ scheduledAt?: number; sendAs?: ApiUser | ApiChat; withMyScore?: boolean; + noAuthors?: boolean; + noCaptions?: boolean; }) { const messageIds = messages.map(({ id }) => id); const randomIds = messages.map(generateRandomBigInt); messages.forEach((message, index) => { - const localMessage = buildLocalForwardedMessage(toChat, message, serverTimeOffset, scheduledAt); + const localMessage = buildLocalForwardedMessage( + toChat, message, serverTimeOffset, scheduledAt, noAuthors, noCaptions, + ); localDb.localMessages[String(randomIds[index])] = localMessage; onUpdate({ @@ -1158,6 +1164,8 @@ export async function forwardMessages({ id: messageIds, withMyScore: withMyScore || undefined, silent: isSilent || undefined, + dropAuthor: noAuthors || undefined, + dropMediaCaptions: noCaptions || undefined, ...(scheduledAt && { scheduleDate: scheduledAt }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), }), true); diff --git a/src/components/common/EmbeddedMessage.scss b/src/components/common/EmbeddedMessage.scss index 9592d1f92..64ec2b169 100644 --- a/src/components/common/EmbeddedMessage.scss +++ b/src/components/common/EmbeddedMessage.scss @@ -125,13 +125,10 @@ &.inside-input { padding-inline-start: 0.5625rem; - margin: 0 0 -0.125rem -0.1875rem; - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: 1fr; width: 100%; --accent-color: var(--color-primary); --hover-color: var(--color-interactive-element-hover); + --active-color: var(--color-reply-active); &::before { bottom: 0.3125rem; @@ -143,11 +140,18 @@ .message-text { margin-inline-start: 0.375rem; + flex-grow: 1; } .message-title { font-weight: 500; color: var(--accent-color); } + + .embedded-more { + font-size: 1.5rem; + opacity: 0.8; + color: var(--color-text-secondary); + } } } diff --git a/src/components/common/EmbeddedMessage.tsx b/src/components/common/EmbeddedMessage.tsx index c27b134eb..3e115d1a9 100644 --- a/src/components/common/EmbeddedMessage.tsx +++ b/src/components/common/EmbeddedMessage.tsx @@ -33,6 +33,7 @@ type OwnProps = { customText?: string; noUserColors?: boolean; isProtected?: boolean; + hasContextMenu?: boolean; onClick: NoneToVoidFunction; }; @@ -46,6 +47,7 @@ const EmbeddedMessage: FC = ({ customText, isProtected, noUserColors, + hasContextMenu, observeIntersection, onClick, }) => { @@ -84,6 +86,7 @@ const EmbeddedMessage: FC = ({

{renderText(senderTitle || title || NBSP)}
+ {hasContextMenu && } ); }; diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index aa1f23b74..0c369c870 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -202,8 +202,8 @@ const MediaViewer: FC = ({ const handleForward = useCallback(() => { openForwardMenu({ - fromChatId: chatId, - messageIds: [mediaId], + fromChatId: chatId!, + messageIds: [mediaId!], }); }, [openForwardMenu, chatId, mediaId]); diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.scss b/src/components/middle/composer/ComposerEmbeddedMessage.scss index 7dc70835e..8867a5da7 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.scss +++ b/src/components/middle/composer/ComposerEmbeddedMessage.scss @@ -22,17 +22,46 @@ padding-top: 0.1875rem; } - & > div > .Button { + & .embedded-left-icon { flex-shrink: 0; background: none !important; width: 3.5rem; height: 2.875rem; margin: 0 -0.0625rem 0 0; padding: 0; - align-self: center; + display: grid; + place-content: center; + font-size: 1.5rem; + color: var(--color-primary); @media (max-width: 600px) { width: 2.875rem; } } + + & .embedded-cancel { + flex-shrink: 0; + background: none !important; + width: 2.25rem; + height: 2.875rem; + margin: 0 -0.0625rem 0 0.75rem; + padding: 0; + align-self: center; + + @media (max-width: 600px) { + width: 1.75rem; + } + } + + .forward-context-menu { + position: absolute; + + .bubble { + width: auto; + } + + .icon-placeholder { + width: 1.25rem; + } + } } diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index ddd2bf29c..65ec66487 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -1,7 +1,9 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact'; +import React, { + memo, useCallback, useEffect, useMemo, useRef, +} from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import { @@ -23,9 +25,15 @@ import { isUserId } from '../../../global/helpers'; import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useShowTransition from '../../../hooks/useShowTransition'; +import useLang from '../../../hooks/useLang'; +import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; +import useContextMenuPosition from '../../../hooks/useContextMenuPosition'; import Button from '../../ui/Button'; import EmbeddedMessage from '../../common/EmbeddedMessage'; +import MenuItem from '../../ui/MenuItem'; +import Menu from '../../ui/Menu'; +import MenuSeparator from '../../ui/MenuSeparator'; import './ComposerEmbeddedMessage.scss'; @@ -36,6 +44,9 @@ type StateProps = { sender?: ApiUser | ApiChat; shouldAnimate?: boolean; forwardedMessagesCount?: number; + noAuthors?: boolean; + noCaptions?: boolean; + forwardsHaveCaptions?: boolean; }; type OwnProps = { @@ -51,15 +62,25 @@ const ComposerEmbeddedMessage: FC = ({ sender, shouldAnimate, forwardedMessagesCount, + noAuthors, + noCaptions, + forwardsHaveCaptions, onClear, }) => { const { setReplyingToId, setEditingId, focusMessage, + changeForwardRecipient, + setForwardNoAuthors, + setForwardNoCaptions, exitForwardMode, } = getActions(); + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const lang = useLang(); + const isForwarding = Boolean(forwardedMessagesCount); const isShown = Boolean( ((replyingToId || editingId) && message) || (sender && forwardedMessagesCount), @@ -87,13 +108,55 @@ const ComposerEmbeddedMessage: FC = ({ useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]); const handleMessageClick = useCallback((): void => { + if (isForwarding) return; focusMessage({ chatId: message!.chatId, messageId: message!.id }); - }, [focusMessage, message]); + }, [focusMessage, isForwarding, message]); + + const handleClearClick = useCallback((e: React.MouseEvent): void => { + e.stopPropagation(); + clearEmbedded(); + }, [clearEmbedded]); + + const handleChangeRecipientClick = useCallback(() => { + changeForwardRecipient(); + }, [changeForwardRecipient]); + + const { + isContextMenuOpen, contextMenuPosition, handleContextMenu, + handleContextMenuClose, handleContextMenuHide, + } = useContextMenuHandlers(ref); + + const getTriggerElement = useCallback(() => ref.current, []); + const getRootElement = useCallback(() => ref.current!, []); + const getMenuElement = useCallback(() => ref.current!.querySelector('.forward-context-menu .bubble'), []); + + const { + positionX, positionY, transformOriginX, transformOriginY, style: menuStyle, + } = useContextMenuPosition( + contextMenuPosition, + getTriggerElement, + getRootElement, + getMenuElement, + ); const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames); + const leftIcon = useMemo(() => { + if (replyingToId) { + return 'icon-reply'; + } + if (editingId) { + return 'icon-edit'; + } + if (isForwarding) { + return 'icon-forward'; + } + + return undefined; + }, [editingId, isForwarding, replyingToId]); + const customText = forwardedMessagesCount && forwardedMessagesCount > 1 - ? `${forwardedMessagesCount} forwarded messages` + ? lang('ForwardedMessageCount', forwardedMessagesCount) : undefined; if (!shouldRender) { @@ -101,19 +164,85 @@ const ComposerEmbeddedMessage: FC = ({ } return ( -
+
- +
+ +
+ + {isForwarding && ( + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoAuthors(false)} + > + {lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')} + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoAuthors(true)} + > + {lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')} + + {forwardsHaveCaptions && ( + <> + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoCaptions(false)} + > + {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')} + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoCaptions(true)} + > + {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')} + + + )} + + + {lang('ChangeRecipient')} + + + )}
); @@ -127,7 +256,9 @@ export default memo(withGlobal( } const { - forwardMessages: { fromChatId, toChatId, messageIds: forwardMessageIds }, + forwardMessages: { + fromChatId, toChatId, messageIds: forwardMessageIds, noAuthors, noCaptions, + }, } = global; const replyingToId = selectReplyingToId(global, chatId, threadId); @@ -136,14 +267,15 @@ export default memo(withGlobal( : selectEditingId(global, chatId, threadId); const shouldAnimate = global.settings.byKey.animationLevel >= 1; const isForwarding = toChatId === chatId; + const forwardedMessages = forwardMessageIds?.map((id) => selectChatMessage(global, fromChatId!, id)!); - let message; + let message: ApiMessage | undefined; if (replyingToId) { message = selectChatMessage(global, chatId, replyingToId); } else if (editingId) { message = selectEditingMessage(global, chatId, threadId, messageListType); } else if (isForwarding && forwardMessageIds!.length === 1) { - message = selectChatMessage(global, fromChatId!, forwardMessageIds![0]); + message = forwardedMessages?.[0]; } let sender: ApiChat | ApiUser | undefined; @@ -169,6 +301,10 @@ export default memo(withGlobal( } } + const forwardsHaveCaptions = forwardedMessages?.some((forward) => ( + forward?.content.text && Object.keys(forward.content).length > 1 + )); + return { replyingToId, editingId, @@ -176,6 +312,9 @@ export default memo(withGlobal( sender, shouldAnimate, forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined, + noAuthors, + noCaptions, + forwardsHaveCaptions, }; }, )(ComposerEmbeddedMessage)); diff --git a/src/components/middle/composer/WebPagePreview.scss b/src/components/middle/composer/WebPagePreview.scss index dd6631fab..220a05d6c 100644 --- a/src/components/middle/composer/WebPagePreview.scss +++ b/src/components/middle/composer/WebPagePreview.scss @@ -29,24 +29,42 @@ margin-top: 0.75rem; } - & > div > .Button { + & &-left-icon { flex-shrink: 0; background: none !important; width: 3.5rem; height: 2.875rem; margin: 0 -0.0625rem 0 0; padding: 0; - align-self: center; + display: grid; + place-content: center; + font-size: 1.5rem; + color: var(--color-primary); @media (max-width: 600px) { width: 2.875rem; } } + & &-clear { + flex-shrink: 0; + background: none !important; + width: 2.25rem; + height: 2.875rem; + margin: 0 -0.0625rem 0 0; + padding: 0; + align-self: center; + + @media (max-width: 600px) { + width: 1.75rem; + } + } + .WebPage { flex-grow: 1; margin: 0.1875rem 0 0.1875rem 0.125rem; max-width: calc(100% - 3.375rem); + overflow: hidden; &::before { top: 0.125rem; diff --git a/src/components/middle/composer/WebPagePreview.tsx b/src/components/middle/composer/WebPagePreview.tsx index 7adb0e304..86e68bd7d 100644 --- a/src/components/middle/composer/WebPagePreview.tsx +++ b/src/components/middle/composer/WebPagePreview.tsx @@ -105,10 +105,20 @@ const WebPagePreview: FC = ({ return (
- -
); diff --git a/src/components/ui/MenuSeparator.module.scss b/src/components/ui/MenuSeparator.module.scss new file mode 100644 index 000000000..b7267eea2 --- /dev/null +++ b/src/components/ui/MenuSeparator.module.scss @@ -0,0 +1,6 @@ +.root { + margin: 0.25rem 1rem; + height: 1px; + border-radius: 1px; + background-color: var(--color-interactive-inactive); +} diff --git a/src/components/ui/MenuSeparator.tsx b/src/components/ui/MenuSeparator.tsx new file mode 100644 index 000000000..2608cef4b --- /dev/null +++ b/src/components/ui/MenuSeparator.tsx @@ -0,0 +1,19 @@ +import React from '../../lib/teact/teact'; + +import type { FC } from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; + +import styles from './MenuSeparator.module.scss'; + +type OwnProps = { + className?: string; +}; + +const MenuSeparator: FC = ({ className }) => { + return ( +
+ ); +}; + +export default MenuSeparator; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 4ef0aa64d..4f2fb66cd 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -602,7 +602,7 @@ addActionHandler('loadPollOptionResults', (global, actions, payload) => { addActionHandler('forwardMessages', (global, action, payload) => { const { - fromChatId, messageIds, toChatId, withMyScore, + fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, } = global.forwardMessages; const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined; const toChat = toChatId ? selectChat(global, toChatId) : undefined; @@ -630,6 +630,8 @@ addActionHandler('forwardMessages', (global, action, payload) => { scheduledAt, sendAs, withMyScore, + noAuthors, + noCaptions, }); } diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 9beb7ebab..9e9170d99 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -413,7 +413,7 @@ addActionHandler('focusMessage', (global, actions, payload) => { addActionHandler('openForwardMenu', (global, actions, payload) => { const { fromChatId, messageIds, groupedId, withMyScore, - } = payload!; + } = payload; let groupedMessageIds; if (groupedId) { groupedMessageIds = selectMessageIdsByGroupId(global, fromChatId, groupedId); @@ -429,6 +429,41 @@ addActionHandler('openForwardMenu', (global, actions, payload) => { }; }); +addActionHandler('changeForwardRecipient', (global) => { + return { + ...global, + forwardMessages: { + ...global.forwardMessages, + toChatId: undefined, + isModalShown: true, + noAuthors: false, + noCaptions: false, + }, + }; +}); + +addActionHandler('setForwardNoAuthors', (global, actions, payload) => { + return { + ...global, + forwardMessages: { + ...global.forwardMessages, + noAuthors: payload, + noCaptions: payload && global.forwardMessages.noCaptions, // `noCaptions` cannot be true when `noAuthors` is false + }, + }; +}); + +addActionHandler('setForwardNoCaptions', (global, actions, payload) => { + return { + ...global, + forwardMessages: { + ...global.forwardMessages, + noCaptions: payload, + noAuthors: payload, // On other clients `noAuthors` updates together with `noCaptions` + }, + }; +}); + addActionHandler('exitForwardMode', (global) => { setGlobal({ ...global, @@ -437,7 +472,7 @@ addActionHandler('exitForwardMode', (global) => { }); addActionHandler('setForwardChatId', (global, actions, payload) => { - const { id } = payload!; + const { id } = payload; setGlobal({ ...global, diff --git a/src/global/types.ts b/src/global/types.ts index 66711eba3..6ce045f12 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -437,6 +437,8 @@ export type GlobalState = { messageIds?: number[]; toChatId?: string; withMyScore?: boolean; + noAuthors?: boolean; + noCaptions?: boolean; }; pollResults: { @@ -824,6 +826,26 @@ export interface ActionPayloads { shouldSharePhoneNumber?: boolean; }; + // Forwards + openForwardMenu: { + fromChatId: string; + messageIds?: number[]; + groupedId?: string; + withMyScore?: boolean; + }; + openForwardMenuForSelectedMessages: never; + setForwardChatId: { + id: string; + }; + forwardMessages: { + isSilent?: boolean; + scheduledAt?: number; + }; + setForwardNoAuthors: boolean; + setForwardNoCaptions: boolean; + exitForwardMode: never; + changeForwardRecipient: never; + // Stickers addRecentSticker: { sticker: ApiSticker; @@ -1097,9 +1119,6 @@ export type NonTypedActionNames = ( 'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' | // poll result 'openPollResults' | 'closePollResults' | 'loadPollOptionResults' | - // forwarding messages - 'openForwardMenu' | 'exitForwardMode' | 'setForwardChatId' | 'forwardMessages' | - 'openForwardMenuForSelectedMessages' | // global search 'setGlobalSearchQuery' | 'searchMessagesGlobal' | 'addRecentlyFoundChatId' | 'clearRecentlyFoundChats' | 'setGlobalSearchContent' | 'setGlobalSearchChatId' | 'setGlobalSearchDate' |