From 7735be21ba72ee9d9720a7b99d6833df8f1e6b00 Mon Sep 17 00:00:00 2001
From: Alexander Zinchuk
Date: Fri, 5 Aug 2022 19:22:58 +0200
Subject: [PATCH] Composer: Redesign Embedded, support forwarding options
(#1935)
---
src/api/gramjs/apiBuilders/messages.ts | 11 +-
src/api/gramjs/methods/messages.ts | 10 +-
src/components/common/EmbeddedMessage.scss | 12 +-
src/components/common/EmbeddedMessage.tsx | 3 +
src/components/mediaViewer/MediaViewer.tsx | 4 +-
.../composer/ComposerEmbeddedMessage.scss | 33 +++-
.../composer/ComposerEmbeddedMessage.tsx | 165 ++++++++++++++++--
.../middle/composer/WebPagePreview.scss | 22 ++-
.../middle/composer/WebPagePreview.tsx | 14 +-
src/components/ui/MenuSeparator.module.scss | 6 +
src/components/ui/MenuSeparator.tsx | 19 ++
src/global/actions/api/messages.ts | 4 +-
src/global/actions/ui/messages.ts | 39 ++++-
src/global/types.ts | 25 ++-
14 files changed, 333 insertions(+), 34 deletions(-)
create mode 100644 src/components/ui/MenuSeparator.module.scss
create mode 100644 src/components/ui/MenuSeparator.tsx
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 && (
+
+ )}
);
@@ -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' |