Composer: Redesign Embedded, support forwarding options (#1935)

This commit is contained in:
Alexander Zinchuk 2022-08-05 19:22:58 +02:00
parent f43a2bd57f
commit 7735be21ba
14 changed files with 333 additions and 34 deletions

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ type OwnProps = {
customText?: string;
noUserColors?: boolean;
isProtected?: boolean;
hasContextMenu?: boolean;
onClick: NoneToVoidFunction;
};
@ -46,6 +47,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
customText,
isProtected,
noUserColors,
hasContextMenu,
observeIntersection,
onClick,
}) => {
@ -84,6 +86,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
</p>
<div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div>
</div>
{hasContextMenu && <i className="embedded-more icon-more" />}
</div>
);
};

View File

@ -202,8 +202,8 @@ const MediaViewer: FC<StateProps> = ({
const handleForward = useCallback(() => {
openForwardMenu({
fromChatId: chatId,
messageIds: [mediaId],
fromChatId: chatId!,
messageIds: [mediaId!],
});
}, [openForwardMenu, chatId, mediaId]);

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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<HTMLDivElement>(null);
const lang = useLang();
const isForwarding = Boolean(forwardedMessagesCount);
const isShown = Boolean(
((replyingToId || editingId) && message)
|| (sender && forwardedMessagesCount),
@ -87,13 +108,55 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
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<HTMLButtonElement, 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<OwnProps & StateProps> = ({
}
return (
<div className={className}>
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
<div>
<Button round faded color="translucent" ariaLabel="Cancel replying" onClick={clearEmbedded}>
<i className="icon-close" />
</Button>
<div className="embedded-left-icon">
<i className={leftIcon} />
</div>
<EmbeddedMessage
className="inside-input"
message={message}
sender={sender}
sender={!noAuthors ? sender : undefined}
customText={customText}
title={editingId ? 'Edit Message' : undefined}
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
onClick={handleMessageClick}
hasContextMenu={isForwarding}
/>
<Button
className="embedded-cancel"
round
faded
color="translucent"
ariaLabel={lang('Cancel')}
onClick={handleClearClick}
>
<i className="icon-close" />
</Button>
{isForwarding && (
<Menu
isOpen={isContextMenuOpen}
transformOriginX={transformOriginX}
transformOriginY={transformOriginY}
positionX={positionX}
positionY={positionY}
style={menuStyle}
className="forward-context-menu"
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
<MenuItem
icon={!noAuthors ? 'message-succeeded' : undefined}
customIcon={noAuthors ? <i className="icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoAuthors(false)}
>
{lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')}
</MenuItem>
<MenuItem
icon={noAuthors ? 'message-succeeded' : undefined}
customIcon={!noAuthors ? <i className="icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoAuthors(true)}
>
{lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')}
</MenuItem>
{forwardsHaveCaptions && (
<>
<MenuSeparator />
<MenuItem
icon={!noCaptions ? 'message-succeeded' : undefined}
customIcon={noCaptions ? <i className="icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoCaptions(false)}
>
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')}
</MenuItem>
<MenuItem
icon={noCaptions ? 'message-succeeded' : undefined}
customIcon={!noCaptions ? <i className="icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoCaptions(true)}
>
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')}
</MenuItem>
</>
)}
<MenuSeparator />
<MenuItem icon="replace" onClick={handleChangeRecipientClick}>
{lang('ChangeRecipient')}
</MenuItem>
</Menu>
)}
</div>
</div>
);
@ -127,7 +256,9 @@ export default memo(withGlobal<OwnProps>(
}
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<OwnProps>(
: 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<OwnProps>(
}
}
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<OwnProps>(
sender,
shouldAnimate,
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
noAuthors,
noCaptions,
forwardsHaveCaptions,
};
},
)(ComposerEmbeddedMessage));

View File

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

View File

@ -105,10 +105,20 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
return (
<div className={buildClassName('WebPagePreview', transitionClassNames)}>
<div>
<Button round faded color="translucent" ariaLabel="Clear Webpage Preview" onClick={handleClearWebpagePreview}>
<div className="WebPagePreview-left-icon">
<i className="icon-link" />
</div>
<WebPage message={messageStub} inPreview theme={theme} />
<Button
className="WebPagePreview-clear"
round
faded
color="translucent"
ariaLabel="Clear Webpage Preview"
onClick={handleClearWebpagePreview}
>
<i className="icon-close" />
</Button>
<WebPage message={messageStub} inPreview theme={theme} />
</div>
</div>
);

View File

@ -0,0 +1,6 @@
.root {
margin: 0.25rem 1rem;
height: 1px;
border-radius: 1px;
background-color: var(--color-interactive-inactive);
}

View File

@ -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<OwnProps> = ({ className }) => {
return (
<div className={buildClassName(styles.root, className)} />
);
};
export default MenuSeparator;

View File

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

View File

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

View File

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