From 72ae33139a1e9bb78ae74615c4df99bfa670d6d0 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 31 Dec 2021 18:17:46 +0100 Subject: [PATCH] Composer: Support sending message as channels (#1604) --- src/api/gramjs/apiBuilders/messages.ts | 4 +- src/api/gramjs/methods/bots.ts | 4 +- src/api/gramjs/methods/chats.ts | 2 + src/api/gramjs/methods/index.ts | 3 +- src/api/gramjs/methods/messages.ts | 58 +++++++- src/api/types/chats.ts | 3 + src/bundles/extra.ts | 1 + src/components/middle/composer/Composer.scss | 20 +++ src/components/middle/composer/Composer.tsx | 72 +++++++++- .../middle/composer/SendAsMenu.async.tsx | 15 +++ .../middle/composer/SendAsMenu.scss | 74 +++++++++++ src/components/middle/composer/SendAsMenu.tsx | 125 ++++++++++++++++++ .../middle/composer/SymbolMenu.scss | 2 +- src/global/types.ts | 2 +- src/hooks/useMouseInside.ts | 6 +- src/lib/gramjs/tl/apiTl.js | 2 + src/lib/gramjs/tl/static/api.reduced.tl | 2 + src/modules/actions/api/bots.ts | 16 ++- src/modules/actions/api/messages.ts | 49 +++++++ src/modules/selectors/chats.ts | 10 ++ 20 files changed, 452 insertions(+), 18 deletions(-) create mode 100644 src/components/middle/composer/SendAsMenu.async.tsx create mode 100644 src/components/middle/composer/SendAsMenu.scss create mode 100644 src/components/middle/composer/SendAsMenu.tsx diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 31c14a211..063540a06 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -22,6 +22,7 @@ import { ApiThreadInfo, ApiInvoice, ApiGroupCall, + ApiUser, ApiSponsoredMessage, } from '../../types'; @@ -857,6 +858,7 @@ export function buildLocalMessage( poll?: ApiNewPoll, groupedId?: string, scheduledAt?: number, + sendAs?: ApiChat | ApiUser, serverTimeOffset = 0, ): ApiMessage { const localId = localMessageCounter++; @@ -880,7 +882,7 @@ export function buildLocalMessage( }, date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset, isOutgoing: !isChannel, - senderId: currentUserId, + senderId: sendAs?.id || currentUserId, ...(replyingTo && { replyToMessageId: replyingTo }), ...(groupedId && { groupedId, diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 14a793e65..77350238c 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -97,12 +97,13 @@ export async function fetchInlineBotResults({ } export async function sendInlineBotResult({ - chat, resultId, queryId, replyingTo, + chat, resultId, queryId, replyingTo, sendAs, }: { chat: ApiChat; resultId: string; queryId: string; replyingTo?: number; + sendAs?: ApiUser | ApiChat; }) { const randomId = generateRandomBigInt(); @@ -113,6 +114,7 @@ export async function sendInlineBotResult({ peer: buildInputPeer(chat.id, chat.accessHash), id: resultId, ...(replyingTo && { replyToMsgId: replyingTo }), + ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), }), true); } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index e3eb9f5f5..2b7621855 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -402,6 +402,7 @@ async function getFullChannelInfo( hiddenPrehistory, call, botInfo, + defaultSendAs, } = result.fullChat; const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported @@ -452,6 +453,7 @@ async function getFullChannelInfo( groupCallId: call ? String(call.id) : undefined, linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined, botCommands, + sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined, }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], groupCall: call ? { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index bc37674c3..2dbafbecd 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -22,7 +22,8 @@ export { markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal, fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate, fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, - reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, + reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, + saveDefaultSendAs, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 991a39b2f..ec9c73f46 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -52,10 +52,15 @@ import { import localDb from '../localDb'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { fetchFile } from '../../../util/files'; -import { addMessageToLocalDb, deserializeBytes, resolveMessageApiChatId } from '../helpers'; +import { + addEntitiesWithPhotosToLocalDb, + addMessageToLocalDb, + deserializeBytes, + resolveMessageApiChatId, +} from '../helpers'; import { interpolateArray } from '../../../util/waveform'; import { requestChatUpdate } from './chats'; -import { buildApiPeerId } from '../apiBuilders/peers'; +import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; const FAST_SEND_TIMEOUT = 1000; const INPUT_WAVEFORM_LENGTH = 63; @@ -200,6 +205,7 @@ export function sendMessage( scheduledAt, groupedId, noWebPage, + sendAs, serverTimeOffset, }: { chat: ApiChat; @@ -214,12 +220,14 @@ export function sendMessage( scheduledAt?: number; groupedId?: string; noWebPage?: boolean; + sendAs?: ApiUser | ApiChat; serverTimeOffset?: number; }, onProgress?: ApiOnProgress, ) { const localMessage = buildLocalMessage( - chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt, serverTimeOffset, + chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt, + sendAs, serverTimeOffset, ); onUpdate({ '@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage', @@ -289,6 +297,7 @@ export function sendMessage( ...(replyingTo && { replyToMsgId: replyingTo }), ...(media && { media }), ...(noWebPage && { noWebpage: noWebPage }), + ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), }), true); })(); @@ -310,6 +319,7 @@ function sendGroupedMedia( groupedId, isSilent, scheduledAt, + sendAs, }: { chat: ApiChat; text?: string; @@ -319,6 +329,7 @@ function sendGroupedMedia( groupedId: string; isSilent?: boolean; scheduledAt?: number; + sendAs?: ApiUser | ApiChat; }, randomId: GramJs.long, localMessage: ApiMessage, @@ -391,6 +402,7 @@ function sendGroupedMedia( replyToMsgId: replyingTo, ...(isSilent && { silent: isSilent }), ...(scheduledAt && { scheduleDate: scheduledAt }), + ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), }), true); })(); @@ -1052,6 +1064,7 @@ export async function forwardMessages({ serverTimeOffset, isSilent, scheduledAt, + sendAs, }: { fromChat: ApiChat; toChat: ApiChat; @@ -1059,6 +1072,7 @@ export async function forwardMessages({ serverTimeOffset: number; isSilent?: boolean; scheduledAt?: number; + sendAs?: ApiUser | ApiChat; }) { const messageIds = messages.map(({ id }) => id); const randomIds = messages.map(generateRandomBigInt); @@ -1082,6 +1096,7 @@ export async function forwardMessages({ id: messageIds, ...(isSilent && { sil2ent: isSilent }), ...(scheduledAt && { scheduleDate: scheduledAt }), + ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), }), true); } @@ -1208,6 +1223,43 @@ export async function fetchSeenBy({ chat, messageId }: { chat: ApiChat; messageI return result ? result.map(String) : undefined; } +export async function fetchSendAs({ + chat, +}: { + chat: ApiChat; +}) { + const result = await invokeRequest(new GramJs.channels.GetSendAs({ + peer: buildInputPeer(chat.id, chat.accessHash), + })); + + if (!result) { + return undefined; + } + + addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesWithPhotosToLocalDb(result.chats); + + const users = result.users.map(buildApiUser).filter(Boolean as any); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean as any); + + return { + users, + chats, + ids: result.peers.map(getApiChatIdFromMtpPeer), + }; +} + +export function saveDefaultSendAs({ + sendAs, chat, +}: { + sendAs: ApiChat | ApiUser; chat: ApiChat; +}) { + return invokeRequest(new GramJs.messages.SaveDefaultSendAs({ + peer: buildInputPeer(chat.id, chat.accessHash), + sendAs: buildInputPeer(sendAs.id, sendAs.accessHash), + })); +} + export async function fetchSponsoredMessages({ chat }: { chat: ApiChat }) { const result = await invokeRequest(new GramJs.channels.GetSponsoredMessages({ channel: buildInputPeer(chat.id, chat.accessHash), diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 0cf8d59d4..53dc72c7f 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -55,6 +55,8 @@ export interface ApiChat { fullInfo?: ApiChatFullInfo; // Obtained with UpdateUserTyping or UpdateChatUserTyping updates typingStatus?: ApiTypingStatus; + + sendAsIds?: string[]; } export interface ApiTypingStatus { @@ -83,6 +85,7 @@ export interface ApiChatFullInfo { }; linkedChatId?: string; botCommands?: ApiBotCommand[]; + sendAsId?: string; } export interface ApiChatMember { diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index dbd069e5a..370a80fcb 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -40,6 +40,7 @@ export { default as DropArea } from '../components/middle/composer/DropArea'; export { default as TextFormatter } from '../components/middle/composer/TextFormatter'; export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip'; export { default as InlineBotTooltip } from '../components/middle/composer/InlineBotTooltip'; +export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu'; export { default as RightSearch } from '../components/right/RightSearch'; export { default as StickerSearch } from '../components/right/StickerSearch'; diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index e70fbf64b..73137f96b 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -33,6 +33,22 @@ --border-width: 0; } + @keyframes show-send-as-button { + from { + width: 1rem; + transform: scale(0); + } + + to { + width: 3.5rem; + transform: scale(1); + } + } + + body:not(.animation-level-0) & .send-as-button { + animation: 0.25s ease-in-out forwards show-send-as-button; + } + > .Button { flex-shrink: 0; margin-left: .5rem; @@ -118,6 +134,10 @@ } } + .send-as-button { + z-index: 1; + } + .mobile-symbol-menu-button { width: 2.875rem; height: 2.875rem; diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 44b05a14b..2dd2cdc74 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -96,6 +96,8 @@ import DropArea, { DropAreaState } from './DropArea.async'; import WebPagePreview from './WebPagePreview'; import Portal from '../../ui/Portal'; import CalendarModal from '../../common/CalendarModal.async'; +import SendAsMenu from './SendAsMenu.async'; +import Avatar from '../../common/Avatar'; import './Composer.scss'; @@ -140,6 +142,9 @@ type StateProps = inlineBots?: Record; botCommands?: ApiBotCommand[] | false; chatBotCommands?: ApiBotCommand[]; + sendAsUser?: ApiUser; + sendAsChat?: ApiChat; + sendAsId?: string; } & Pick; @@ -199,6 +204,9 @@ const Composer: FC = ({ isInlineBotLoading, botCommands, chatBotCommands, + sendAsUser, + sendAsChat, + sendAsId, }) => { const { sendMessage, @@ -213,8 +221,8 @@ const Composer: FC = ({ openChat, addRecentEmoji, sendInlineBotResult, + loadSendAs, } = getDispatch(); - const lang = useLang(); // eslint-disable-next-line no-null/no-null @@ -227,6 +235,7 @@ const Composer: FC = ({ scheduledMessageArgs, setScheduledMessageArgs, ] = useState(); const { width: windowWidth } = windowSize.get(); + const sendAsIds = chat?.sendAsIds; const sendMessageAction = useSendMessageAction(chatId, threadId); // Cache for frequently updated state @@ -245,6 +254,12 @@ const Composer: FC = ({ } }, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]); + useEffect(() => { + if (chatId && lastSyncTime && !sendAsIds && isReady) { + loadSendAs({ chatId }); + } + }, [chatId, isReady, lastSyncTime, loadSendAs, sendAsIds]); + useLayoutEffect(() => { if (!appendixRef.current) return; @@ -264,6 +279,7 @@ const Composer: FC = ({ const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag(); const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag(); const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag(); + const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag(); const [isHoverDisabled, disableHover, enableHover] = useFlag(); @@ -566,8 +582,9 @@ const Composer: FC = ({ const handleActivateSymbolMenu = useCallback(() => { closeBotCommandMenu(); + closeSendAsMenu(); openSymbolMenu(); - }, [closeBotCommandMenu, openSymbolMenu]); + }, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]); const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => { sticker = { @@ -701,6 +718,22 @@ const Composer: FC = ({ }, MOBILE_KEYBOARD_HIDE_DELAY_MS); }, [openSymbolMenu, closeBotCommandMenu]); + const handleSendAsMenuOpen = useCallback(() => { + const messageInput = document.getElementById(EDITABLE_INPUT_ID)!; + + if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) { + openSendAsMenu(); + return; + } + + messageInput.blur(); + setTimeout(() => { + closeBotCommandMenu(); + closeSymbolMenu(); + openSendAsMenu(); + }, MOBILE_KEYBOARD_HIDE_DELAY_MS); + }, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu]); + const handleAllScheduledClick = useCallback(() => { openChat({ id: chatId, threadId, type: 'scheduled' }); }, [openChat, chatId, threadId]); @@ -834,6 +867,13 @@ const Composer: FC = ({ message={renderedEditedMessage} /> )} + = ({ )} + {sendAsIds && (sendAsUser || sendAsChat) && ( + + )} {IS_SINGLE_COLUMN_LAYOUT ? (