diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 06ad3e672..a3d9237ff 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -409,6 +409,7 @@ export interface ApiInputMessageReplyInfo { replyToTopId?: number; replyToPeerId?: string; quoteText?: ApiFormattedText; + isShowingDelayNeeded?: boolean; } export interface ApiInputStoryReplyInfo { @@ -532,6 +533,9 @@ export type MediaContent = { isExpiredRoundVideo?: boolean; ttlSeconds?: number; }; +export type MediaContainer = { + content: MediaContent; +}; export interface ApiMessage { id: number; diff --git a/src/assets/font-icons/remove-quote.svg b/src/assets/font-icons/remove-quote.svg new file mode 100644 index 000000000..09c58ec29 --- /dev/null +++ b/src/assets/font-icons/remove-quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/show-message.svg b/src/assets/font-icons/show-message.svg new file mode 100644 index 000000000..57b1cb371 --- /dev/null +++ b/src/assets/font-icons/show-message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/common/embedded/EmbeddedMessage.scss b/src/components/common/embedded/EmbeddedMessage.scss index 97b6c1ba5..e81f8cd12 100644 --- a/src/components/common/embedded/EmbeddedMessage.scss +++ b/src/components/common/embedded/EmbeddedMessage.scss @@ -110,12 +110,12 @@ .message-title { display: flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; flex: 1; column-gap: 0.25rem; } - .message-title, .embedded-sender { + .message-title, .embedded-sender, .embedded-sender-chat { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index afbcbe2ef..9a6855026 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -3,13 +3,15 @@ import React, { useMemo, useRef } from '../../../lib/teact/teact'; import type { ApiChat, - ApiMessage, ApiPeer, ApiReplyInfo, + ApiMessage, ApiPeer, ApiReplyInfo, MediaContainer, } from '../../../api/types'; import type { ChatTranslatedMessages } from '../../../global/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { IconName } from '../../../types/icons'; +import { CONTENT_NOT_SUPPORTED } from '../../../config'; import { + getMediaContentTypeDescription, getMessageIsSpoiler, getMessageMediaHash, getMessageRoundVideo, @@ -18,6 +20,7 @@ import { isChatChannel, isChatGroup, isMessageTranslatable, + isUserId, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed'; @@ -58,7 +61,7 @@ type OwnProps = { isOpen?: boolean; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; - onClick: NoneToVoidFunction; + onClick: ((e: React.MouseEvent) => void); }; const NBSP = '\u00A0'; @@ -128,7 +131,7 @@ const EmbeddedMessage: FC = ({ } if (!message) { - return customText || NBSP; + return customText || renderMediaContentType(wrappedMedia) || NBSP; } if (isActionMessage(message)) { @@ -155,6 +158,23 @@ const EmbeddedMessage: FC = ({ ); } + function renderMediaContentType(media?: MediaContainer) { + if (!media || media.content.text) return NBSP; + const description = getMediaContentTypeDescription(lang, media.content); + if (!description || description === CONTENT_NOT_SUPPORTED) return NBSP; + return ( + + {renderText(description)} + + ); + } + + function checkShouldRenderSenderTitle() { + if (!senderChat) return true; + if (isUserId(senderChat?.id)) return true; + if (senderChat.id === sender?.id) return false; + return true; + } function renderSender() { if (title) { return renderText(title); @@ -175,18 +195,21 @@ const EmbeddedMessage: FC = ({ } } - const isChatSender = senderChat && senderChat.id === sender?.id; const isReplyToQuote = isInComposer && Boolean(replyInfo && 'quoteText' in replyInfo && replyInfo?.quoteText); return ( <> - {!isChatSender && ( + {checkShouldRenderSenderTitle() && ( {renderText(isReplyToQuote ? lang('ReplyToQuote', senderTitle) : senderTitle)} )} {icon && } - {icon && senderChatTitle && renderText(senderChatTitle)} + {icon && senderChatTitle && ( + + {renderText(senderChatTitle)} + + )} ); } diff --git a/src/components/main/ForwardRecipientPicker.tsx b/src/components/main/ForwardRecipientPicker.tsx index 8ba2200dc..9de575d0a 100644 --- a/src/components/main/ForwardRecipientPicker.tsx +++ b/src/components/main/ForwardRecipientPicker.tsx @@ -5,9 +5,16 @@ import React, { import { getActions, getGlobal, withGlobal } from '../../global'; import type { ThreadId } from '../../types'; +import { MAIN_THREAD_ID } from '../../api/types'; import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers'; -import { selectChat, selectTabState, selectUser } from '../../global/selectors'; +import { + selectChat, + selectCurrentChat, + selectDraft, + selectTabState, + selectUser, +} from '../../global/selectors'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; @@ -23,6 +30,7 @@ interface StateProps { currentUserId?: string; isManyMessages?: boolean; isStory?: boolean; + isReplying?: boolean; } const ForwardRecipientPicker: FC = ({ @@ -30,8 +38,10 @@ const ForwardRecipientPicker: FC = ({ currentUserId, isManyMessages, isStory, + isReplying, }) => { const { + openChatOrTopicWithReplyInDraft, setForwardChatOrTopic, exitForwardMode, forwardToSavedMessages, @@ -84,9 +94,15 @@ const ForwardRecipientPicker: FC = ({ forwardToSavedMessages(); showNotification({ message }); } else { - setForwardChatOrTopic({ chatId: recipientId, topicId: Number(threadId) }); + const chatId = recipientId; + const topicId = threadId ? Number(threadId) : undefined; + if (isReplying) { + openChatOrTopicWithReplyInDraft({ chatId, topicId }); + } else { + setForwardChatOrTopic({ chatId, topicId }); + } } - }, [currentUserId, isManyMessages, isStory, lang]); + }, [currentUserId, isManyMessages, isStory, lang, isReplying]); const handleClose = useCallback(() => { exitForwardMode(); @@ -110,9 +126,12 @@ const ForwardRecipientPicker: FC = ({ export default memo(withGlobal((global): StateProps => { const { messageIds, storyId } = selectTabState(global).forwardMessages; + const currentChatId = selectCurrentChat(global)?.id; + const isReplying = currentChatId && selectDraft(global, currentChatId, MAIN_THREAD_ID)?.replyInfo; return { currentUserId: global.currentUserId, isManyMessages: (messageIds?.length || 0) > 1, isStory: Boolean(storyId), + isReplying: Boolean(isReplying), }; })(ForwardRecipientPicker)); diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index c361fc76b..3c4652570 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -4,11 +4,14 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiInputMessageReplyInfo, ApiMessage, ApiPeer } from '../../../api/types'; +import type { + ApiChat, ApiInputMessageReplyInfo, ApiMessage, ApiPeer, +} from '../../../api/types'; import { stripCustomEmoji } from '../../../global/helpers'; import { selectCanAnimateInterface, + selectChat, selectChatMessage, selectCurrentMessageList, selectDraft, @@ -56,6 +59,9 @@ type StateProps = { isCurrentUserPremium?: boolean; isContextMenuDisabled?: boolean; isReplyToDiscussion?: boolean; + isInChangingRecipientMode?: boolean; + isChangingChats?: boolean; + senderChat?: ApiChat; }; type OwnProps = { @@ -80,12 +86,16 @@ const ComposerEmbeddedMessage: FC = ({ isContextMenuDisabled, isReplyToDiscussion, onClear, + isInChangingRecipientMode, + isChangingChats, + senderChat, }) => { const { resetDraftReplyInfo, + updateDraftReplyInfo, setEditingId, focusMessage, - changeForwardRecipient, + changeRecipient, setForwardNoAuthors, setForwardNoCaptions, exitForwardMode, @@ -95,15 +105,17 @@ const ComposerEmbeddedMessage: FC = ({ const lang = useLang(); const isReplyToTopicStart = message?.content.action?.type === 'topicCreate'; + const isShowingReply = replyInfo && !shouldForceShowEditing; + const isReplyWithQuote = Boolean(replyInfo?.quoteText); const isForwarding = Boolean(forwardedMessagesCount); const isShown = Boolean( - ((replyInfo || editingId) && message) + ((replyInfo || editingId) && message && !isInChangingRecipientMode) || (sender && forwardedMessagesCount), ); const canAnimate = useAsyncRendering( [isShown, isForwarding], - isShown && isForwarding ? FORWARD_RENDERING_DELAY : undefined, + isShown && isChangingChats ? FORWARD_RENDERING_DELAY : undefined, ); const { @@ -115,6 +127,11 @@ const ComposerEmbeddedMessage: FC = ({ undefined, !shouldAnimate, ); + useEffect(() => { + if (canAnimate && replyInfo?.isShowingDelayNeeded) { + updateDraftReplyInfo({ isShowingDelayNeeded: false }); + } + }); const clearEmbedded = useLastCallback(() => { if (replyInfo && !shouldForceShowEditing) { @@ -129,24 +146,36 @@ const ComposerEmbeddedMessage: FC = ({ useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]); - const handleMessageClick = useLastCallback((): void => { - if (isForwarding) return; + const { + isContextMenuOpen, contextMenuPosition, handleContextMenu, + handleContextMenuClose, handleContextMenuHide, + } = useContextMenuHandlers(ref); + + const focusMessageFromDraft = () => { focusMessage({ chatId: message!.chatId, messageId: message!.id, noForumTopicPanel: true }); + }; + const handleMessageClick = useLastCallback((e: React.MouseEvent): void => { + handleContextMenu(e); }); const handleClearClick = useLastCallback((e: React.MouseEvent): void => { e.stopPropagation(); clearEmbedded(); + handleContextMenuHide(); }); - - const handleChangeRecipientClick = useLastCallback(() => { - changeForwardRecipient(); - }); - - const { - isContextMenuOpen, contextMenuPosition, handleContextMenu, - handleContextMenuClose, handleContextMenuHide, - } = useContextMenuHandlers(ref); + const buildAutoCloseMenuItemHandler = (action: NoneToVoidFunction) => { + return () => { + handleContextMenuClose(); + action(); + }; + }; + const handleForwardToAnotherChatClick = useLastCallback(buildAutoCloseMenuItemHandler(changeRecipient)); + const handleShowMessageClick = useLastCallback(buildAutoCloseMenuItemHandler(focusMessageFromDraft)); + const handleRemoveQuoteClick = useLastCallback(buildAutoCloseMenuItemHandler( + () => updateDraftReplyInfo({ quoteText: undefined }), + )); + const handleChangeReplyRecipientClick = useLastCallback(buildAutoCloseMenuItemHandler(changeRecipient)); + const handleDoNotReplyClick = useLastCallback(buildAutoCloseMenuItemHandler(clearEmbedded)); const getTriggerElement = useLastCallback(() => ref.current); const getRootElement = useLastCallback(() => ref.current!); @@ -162,8 +191,11 @@ const ComposerEmbeddedMessage: FC = ({ ); useEffect(() => { - if (!shouldRender) handleContextMenuClose(); - }, [handleContextMenuClose, shouldRender]); + if (!shouldRender) { + handleContextMenuClose(); + handleContextMenuHide(); + } + }, [handleContextMenuClose, handleContextMenuHide, shouldRender]); const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames); const renderingSender = useCurrentOrPrev(sender, true); @@ -172,8 +204,6 @@ const ComposerEmbeddedMessage: FC = ({ getPeerColorClass(renderingSender), ); - const isShowingReply = replyInfo && !shouldForceShowEditing; - const leftIcon = useMemo(() => { if (isShowingReply) { return 'reply'; @@ -212,9 +242,9 @@ const ComposerEmbeddedMessage: FC = ({ } return ( -
+
-
+
{renderingLeftIcon && } {Boolean(replyInfo?.quoteText) && ( @@ -231,6 +261,7 @@ const ComposerEmbeddedMessage: FC = ({ title={(editingId && !isShowingReply) ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined} onClick={handleMessageClick} + senderChat={senderChat} /> - {isForwarding && !isContextMenuDisabled && ( + {(isShowingReply || isForwarding) && !isContextMenuDisabled && ( = ({ onClose={handleContextMenuClose} onCloseAnimationEnd={handleContextMenuHide} > - : undefined} - // eslint-disable-next-line react/jsx-no-bind - onClick={() => setForwardNoAuthors({ - noAuthors: false, - })} - > - {lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')} - - : undefined} - // eslint-disable-next-line react/jsx-no-bind - onClick={() => setForwardNoAuthors({ - noAuthors: true, - })} - > - {lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')} - - {forwardsHaveCaptions && ( + {isForwarding && ( <> - : undefined} + icon={!noAuthors ? 'message-succeeded' : undefined} + customIcon={noAuthors ? : undefined} // eslint-disable-next-line react/jsx-no-bind - onClick={() => setForwardNoCaptions({ - noCaptions: false, + onClick={() => setForwardNoAuthors({ + noAuthors: false, })} > - {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')} + {lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')} : undefined} + icon={noAuthors ? 'message-succeeded' : undefined} + customIcon={!noAuthors ? : undefined} // eslint-disable-next-line react/jsx-no-bind - onClick={() => setForwardNoCaptions({ - noCaptions: true, + onClick={() => setForwardNoAuthors({ + noAuthors: true, })} > - {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')} + {lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')} + + {forwardsHaveCaptions && ( + <> + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoCaptions({ + noCaptions: false, + })} + > + {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')} + + : undefined} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => setForwardNoCaptions({ + noCaptions: true, + })} + > + {lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')} + + + )} + + + {lang('ForwardAnotherChat')} + + + )} + {isShowingReply && ( + <> + + {lang('Message.Context.Goto')} + + {isReplyWithQuote && ( + + {lang('RemoveQuote')} + + )} + + {lang('ReplyToAnotherChat')} + + + {lang('DoNotReply')} )} - - - {lang('ChangeRecipient')} - )}
@@ -319,7 +378,7 @@ export default memo(withGlobal( const { forwardMessages: { - fromChatId, toChatId, messageIds: forwardMessageIds, noAuthors, noCaptions, + fromChatId, toChatId, messageIds: forwardMessageIds, noAuthors, noCaptions, isModalShown, }, } = selectTabState(global); @@ -332,7 +391,10 @@ export default memo(withGlobal( const draft = selectDraft(global, chatId, threadId); const replyInfo = draft?.replyInfo; + const replyToPeerId = replyInfo?.replyToPeerId; + const senderChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined; + const isChangingChats = isForwarding || replyInfo?.isShowingDelayNeeded; let message: ApiMessage | undefined; if (replyInfo && !shouldForceShowEditing) { message = selectChatMessage(global, replyInfo.replyToPeerId || chatId, replyInfo.replyToMsgId); @@ -389,6 +451,9 @@ export default memo(withGlobal( isCurrentUserPremium: selectIsCurrentUserPremium(global), isContextMenuDisabled, isReplyToDiscussion, + isInChangingRecipientMode: isModalShown, + isChangingChats, + senderChat, }; }, )(ComposerEmbeddedMessage)); diff --git a/src/components/ui/MenuItem.tsx b/src/components/ui/MenuItem.tsx index ba238dd12..b2882f13a 100644 --- a/src/components/ui/MenuItem.tsx +++ b/src/components/ui/MenuItem.tsx @@ -52,13 +52,11 @@ const MenuItem: FC = (props) => { const lang = useLang(); const { isTouchScreen } = useAppLayout(); const handleClick = useLastCallback((e: React.MouseEvent) => { + e.stopPropagation(); if (disabled || !onClick) { - e.stopPropagation(); e.preventDefault(); - return; } - onClick(e, clickArg); }); @@ -67,13 +65,12 @@ const MenuItem: FC = (props) => { return; } + e.stopPropagation(); if (disabled || !onClick) { - e.stopPropagation(); e.preventDefault(); return; } - onClick(e, clickArg); }); const handleMouseDown = useLastCallback((e: React.SyntheticEvent) => { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 8beff59c2..031eca38e 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -13,6 +13,7 @@ import type { ApiSticker, ApiStory, ApiStorySkipped, + ApiUser, ApiVideo, } from '../../../api/types'; import type { MessageKey } from '../../../util/messageKey'; @@ -122,6 +123,7 @@ import { selectPeerStory, selectPinnedIds, selectRealLastReadId, + selectReplyCanBeSentToChat, selectScheduledMessage, selectSendAs, selectSponsoredMessage, @@ -1750,29 +1752,104 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { } }); +async function checkIfVoiceMessagesAllowed( + global: T, + user: ApiUser, + chatId: string, +): Promise { + let fullInfo = selectUserFullInfo(global, chatId); + if (!fullInfo) { + const { accessHash } = user; + const result = await callApi('fetchFullUser', { id: chatId, accessHash }); + fullInfo = result?.fullInfo; + } + return Boolean(!fullInfo?.noVoiceMessages); +} + +function moveReplyToNewDraft( + global: T, + threadId: ThreadId, + replyInfo: ApiInputMessageReplyInfo, + toChatId: string, +) { + const currentDraft = selectDraft(global, toChatId, threadId); + + if (!replyInfo.replyToMsgId) return; + + const newDraft: ApiDraft = { + ...currentDraft, + replyInfo, + }; + + saveDraft({ + global, chatId: toChatId, threadId, draft: newDraft, isLocalOnly: true, noLocalTimeUpdate: true, + }); +} +addActionHandler('openChatOrTopicWithReplyInDraft', (global, actions, payload): ActionReturnType => { + const { chatId: toChatId, topicId, tabId = getCurrentTabId() } = payload; + + global = getGlobal(); + + if (!selectReplyCanBeSentToChat(global, toChatId, tabId)) { + actions.showNotification({ message: translate('Chat.SendNotAllowedText'), tabId }); + return; + } + + global = updateTabState(global, { + forwardMessages: { + ...selectTabState(global, tabId).forwardMessages, + isModalShown: false, + }, + }, tabId); + setGlobal(global); + + const currentChat = selectCurrentChat(global, tabId); + if (!currentChat) return; + + const threadId = topicId || MAIN_THREAD_ID; + const currentChatId = currentChat.id; + + const currentReplyInfo = selectDraft(global, currentChatId, threadId)?.replyInfo; + if (!currentReplyInfo) return; + if (!currentReplyInfo.replyToPeerId && toChatId === currentChat.id) return; + + const getPeerId = () => { + if (!currentReplyInfo?.replyToPeerId) return currentChatId; + return currentReplyInfo.replyToPeerId === toChatId ? undefined : currentReplyInfo.replyToPeerId; + }; + const currentThreadId = selectCurrentMessageList(global, tabId)?.threadId; + if (!currentThreadId) { + return; + } + const replyToPeerId = getPeerId(); + const newReply: ApiInputMessageReplyInfo = { + ...currentReplyInfo, + replyToPeerId, + type: 'message', + isShowingDelayNeeded: true, + }; + + moveReplyToNewDraft(global, threadId, newReply, toChatId); + actions.openThread({ chatId: toChatId, threadId, tabId }); + actions.closeMediaViewer({ tabId }); + actions.exitMessageSelectMode({ tabId }); + actions.clearDraft({ chatId: currentChatId, threadId: currentThreadId }); +}); + addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Promise => { const { chatId, topicId, tabId = getCurrentTabId() } = payload; - let user = selectUser(global, chatId); - if (user && selectForwardsContainVoiceMessages(global, tabId)) { - let fullInfo = selectUserFullInfo(global, chatId); - if (!fullInfo) { - const { accessHash } = user; - const result = await callApi('fetchFullUser', { id: chatId, accessHash }); - global = getGlobal(); - user = result?.user; - fullInfo = result?.fullInfo; - } - - if (fullInfo!.noVoiceMessages) { - actions.showDialog({ - data: { - message: translate('VoiceMessagesRestrictedByPrivacy', getUserFullName(user)), - }, - tabId, - }); - return; - } + const user = selectUser(global, chatId); + const isSelectForwardsContainVoiceMessages = selectForwardsContainVoiceMessages(global, tabId); + if (isSelectForwardsContainVoiceMessages && user && !await checkIfVoiceMessagesAllowed(global, user, chatId)) { + actions.showDialog({ + data: { + message: translate('VoiceMessagesRestrictedByPrivacy', getUserFullName(user)), + }, + tabId, + }); + return; } + global = getGlobal(); if (!selectForwardsCanBeSentToChat(global, chatId, tabId)) { actions.showAllowedMessageTypesNotification({ chatId, tabId }); diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index cd9135ab2..1c3119e7f 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -515,7 +515,7 @@ addActionHandler('openForwardMenu', (global, actions, payload): ActionReturnType }, tabId); }); -addActionHandler('changeForwardRecipient', (global, actions, payload): ActionReturnType => { +addActionHandler('changeRecipient', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; return updateTabState(global, { forwardMessages: { diff --git a/src/global/helpers/messageMedia.ts b/src/global/helpers/messageMedia.ts index 5ffccc21f..5719d6120 100644 --- a/src/global/helpers/messageMedia.ts +++ b/src/global/helpers/messageMedia.ts @@ -10,7 +10,7 @@ import type { ApiPhoto, ApiVideo, ApiWebDocument, - MediaContent, + MediaContainer, } from '../../api/types'; import { ApiMediaFormat } from '../../api/types'; @@ -25,10 +25,6 @@ import { import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo'; import { getAttachmentType, matchLinkInMessageText } from './messages'; -type MediaContainer = { - content: MediaContent; -}; - type Target = 'micro' | 'pictogram' diff --git a/src/global/helpers/messageSummary.ts b/src/global/helpers/messageSummary.ts index 9b4c154c5..753605780 100644 --- a/src/global/helpers/messageSummary.ts +++ b/src/global/helpers/messageSummary.ts @@ -1,6 +1,6 @@ import type { TeactNode } from '../../lib/teact/teact'; -import type { ApiMessage } from '../../api/types'; +import type { ApiMessage, MediaContent } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; import { ApiMessageEntityTypes } from '../../api/types'; @@ -8,7 +8,7 @@ import { CONTENT_NOT_SUPPORTED } from '../../config'; import trimText from '../../util/trimText'; import { getGlobal } from '../index'; import { - getExpiredMessageDescription, getMessageText, getMessageTranscription, isExpiredMessage, + getExpiredMessageContentDescription, getMessageText, getMessageTranscription, isExpiredMessageContent, } from './messages'; import { getUserFirstOrLastName } from './users'; @@ -102,11 +102,23 @@ export function getMessageSummaryEmoji(message: ApiMessage) { return undefined; } +export function getMediaContentTypeDescription(lang: LangFn, content: MediaContent) { + return getSummaryDescription(lang, content); +} export function getMessageSummaryDescription( lang: LangFn, message: ApiMessage, truncatedText?: string | TeactNode, isExtended = false, +) { + return getSummaryDescription(lang, message.content, message, truncatedText, isExtended); +} +function getSummaryDescription( + lang: LangFn, + mediaContent: MediaContent, + message?: ApiMessage, + truncatedText?: string | TeactNode, + isExtended = false, ) { const { text, @@ -124,12 +136,12 @@ export function getMessageSummaryDescription( storyData, giveaway, giveawayResults, - } = message.content; + } = mediaContent; let hasUsedTruncatedText = false; let summary: string | TeactNode | undefined; - if (message.groupedId) { + if (message?.groupedId) { hasUsedTruncatedText = true; summary = truncatedText || lang('lng_in_dlg_album'); } @@ -149,7 +161,7 @@ export function getMessageSummaryDescription( } if (audio) { - summary = getMessageAudioCaption(message) || lang('AttachMusic'); + summary = getMessageAudioCaption(mediaContent) || lang('AttachMusic'); } if (voice) { @@ -203,7 +215,7 @@ export function getMessageSummaryDescription( } if (storyData) { - if (storyData.isMention) { + if (message && storyData.isMention) { // eslint-disable-next-line eslint-multitab-tt/no-immediate-global const global = getGlobal(); const firstName = getUserFirstOrLastName(global.users.byId[message.chatId]); @@ -211,12 +223,12 @@ export function getMessageSummaryDescription( ? lang('Chat.Service.StoryMentioned.You', firstName) : lang('Chat.Service.StoryMentioned', firstName); } else { - summary = lang('ForwardedStory'); + summary = message ? lang('ForwardedStory') : lang('Chat.ReplyStory'); } } - if (isExpiredMessage(message)) { - const expiredMessageText = getExpiredMessageDescription(lang, message); + if (isExpiredMessageContent(mediaContent)) { + const expiredMessageText = getExpiredMessageContentDescription(lang, mediaContent); if (expiredMessageText) { summary = expiredMessageText; } @@ -232,11 +244,11 @@ export function generateBrailleSpoiler(length: number) { .join(''); } -function getMessageAudioCaption(message: ApiMessage) { +function getMessageAudioCaption(mediaContent: MediaContent) { const { audio, text, - } = message.content; + } = mediaContent; return (audio && [audio.title, audio.performer].filter(Boolean) .join(' — ')) || (text?.text); diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index 81fc329e2..4cfa252ae 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -1,6 +1,7 @@ import type { ApiAttachment, ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiPeer, ApiStory, ApiUser, } from '../../api/types'; +import type { MediaContent } from '../../api/types/messages'; import type { LangFn } from '../../hooks/useLang'; import { ApiMessageEntityTypes } from '../../api/types'; @@ -320,7 +321,10 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList = } export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage): string | undefined { - const { isExpiredVoice, isExpiredRoundVideo } = message.content; + return getExpiredMessageContentDescription(langFn, message.content); +} +export function getExpiredMessageContentDescription(langFn: LangFn, mediaContent: MediaContent): string | undefined { + const { isExpiredVoice, isExpiredRoundVideo } = mediaContent; if (isExpiredVoice) { return langFn('Message.VoiceMessageExpired'); } else if (isExpiredRoundVideo) { @@ -330,7 +334,11 @@ export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage } export function isExpiredMessage(message: ApiMessage) { - const { isExpiredVoice, isExpiredRoundVideo } = message.content ?? {}; + return isExpiredMessageContent(message.content); +} + +export function isExpiredMessageContent(content: MediaContent) { + const { isExpiredVoice, isExpiredRoundVideo } = content ?? {}; return Boolean(isExpiredVoice || isExpiredRoundVideo); } diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 2050640e5..fdd22e4ee 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -9,6 +9,7 @@ import type { ApiStickerSetInfo, } from '../../api/types'; import type { ThreadId } from '../../types'; +import type { IAllowedAttachmentOptions } from '../helpers'; import type { ChatTranslatedMessages, GlobalState, MessageListType, TabArgs, TabThread, Thread, @@ -47,6 +48,7 @@ import { isChatGroup, isChatSuperGroup, isCommonBoxChat, + isExpiredMessage, isForwardedMessage, isMessageDocumentSticker, isMessageFailed, @@ -1336,7 +1338,7 @@ export function selectForwardsContainVoiceMessages( const chatMessages = selectChatMessages(global, fromChatId!); return messageIds.some((messageId) => { const message = chatMessages[messageId]; - return Boolean(message.content.voice) || message.content.video?.isRound; + return Boolean(message.content.voice) || Boolean(message.content.video?.isRound); }); } @@ -1358,7 +1360,22 @@ export function selectRequestedMessageTranslationLanguage const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId]; return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId]; } +export function selectReplyCanBeSentToChat( + global: T, + toChatId: string, + ...[tabId = getCurrentTabId()]: TabArgs +) { + const currentChat = selectCurrentChat(global, tabId); + if (!currentChat) return false; + const replyInfo = selectDraft(global, currentChat.id, MAIN_THREAD_ID)?.replyInfo; + if (!replyInfo || !replyInfo.replyToMsgId) return false; + const fromChatId = replyInfo?.replyToPeerId ?? currentChat.id; + if (toChatId === fromChatId) return true; + const chatMessages = selectChatMessages(global, fromChatId!); + const message = chatMessages[replyInfo.replyToMsgId]; + return !isExpiredMessage(message); +} export function selectForwardsCanBeSentToChat( global: T, toChatId: string, @@ -1374,33 +1391,30 @@ export function selectForwardsCanBeSentToChat( const chatFullInfo = selectChatFullInfo(global, toChatId); const chatMessages = selectChatMessages(global, fromChatId!); - const { - canSendVoices, canSendRoundVideos, canSendStickers, canSendDocuments, canSendAudios, canSendVideos, - canSendPhotos, canSendGifs, canSendPlainText, - } = getAllowedAttachmentOptions(chat, chatFullInfo); - return !messageIds!.some((messageId) => { - const message = chatMessages[messageId]; - const isVoice = message.content.voice; - const isRoundVideo = message.content.video?.isRound; - const isPhoto = message.content.photo; - const isGif = message.content.video?.isGif; - const isVideo = message.content.video && !isRoundVideo && !isGif; - const isAudio = message.content.audio; - const isDocument = message.content.document; - const isSticker = message.content.sticker; - const isPlainText = message.content.text - && !isVoice && !isRoundVideo && !isSticker && !isDocument && !isAudio && !isVideo && !isPhoto && !isGif; + const options = getAllowedAttachmentOptions(chat, chatFullInfo); + return !messageIds!.some((messageId) => сheckMessageSendingDenied(chatMessages[messageId], options)); +} +function сheckMessageSendingDenied(message: ApiMessage, options: IAllowedAttachmentOptions) { + const isVoice = message.content.voice; + const isRoundVideo = message.content.video?.isRound; + const isPhoto = message.content.photo; + const isGif = message.content.video?.isGif; + const isVideo = message.content.video && !isRoundVideo && !isGif; + const isAudio = message.content.audio; + const isDocument = message.content.document; + const isSticker = message.content.sticker; + const isPlainText = message.content.text + && !isVoice && !isRoundVideo && !isSticker && !isDocument && !isAudio && !isVideo && !isPhoto && !isGif; - return (isVoice && !canSendVoices) - || (isRoundVideo && !canSendRoundVideos) - || (isSticker && !canSendStickers) - || (isDocument && !canSendDocuments) - || (isAudio && !canSendAudios) - || (isVideo && !canSendVideos) - || (isPhoto && !canSendPhotos) - || (isGif && !canSendGifs) - || (isPlainText && !canSendPlainText); - }); + return (isVoice && !options.canSendVoices) + || (isRoundVideo && !options.canSendRoundVideos) + || (isSticker && !options.canSendStickers) + || (isDocument && !options.canSendDocuments) + || (isAudio && !options.canSendAudios) + || (isVideo && !options.canSendVideos) + || (isPhoto && !options.canSendPhotos) + || (isGif && !options.canSendGifs) + || (isPlainText && !options.canSendPlainText); } export function selectCanTranslateMessage( diff --git a/src/global/types.ts b/src/global/types.ts index c3dba2a9a..6f7fb64b9 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2540,6 +2540,10 @@ export interface ActionPayloads { chatId: string; topicId?: number; } & WithTabId; + openChatOrTopicWithReplyInDraft: { + chatId: string; + topicId?: number; + } & WithTabId; forwardMessages: { isSilent?: boolean; scheduledAt?: number; @@ -2551,7 +2555,7 @@ export interface ActionPayloads { noCaptions: boolean; } & WithTabId; exitForwardMode: WithTabId | undefined; - changeForwardRecipient: WithTabId | undefined; + changeRecipient: WithTabId | undefined; forwardToSavedMessages: WithTabId | undefined; forwardStory: { toChatId: string; diff --git a/src/hooks/useShowTransition.ts b/src/hooks/useShowTransition.ts index 811eb9745..4916912d5 100644 --- a/src/hooks/useShowTransition.ts +++ b/src/hooks/useShowTransition.ts @@ -24,6 +24,7 @@ const useShowTransition = ( if (closeTimeoutRef.current) { window.clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = undefined; } } else { diff --git a/src/styles/icons.scss b/src/styles/icons.scss index e809895bf..e72ec83b3 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -187,83 +187,85 @@ $icons-map: ( "readchats": "\f19c", "recent": "\f19d", "reload": "\f19e", - "remove": "\f19f", - "reopen-topic": "\f1a0", - "replace": "\f1a1", - "replies": "\f1a2", - "reply-filled": "\f1a3", - "reply": "\f1a4", - "revenue-split": "\f1a5", - "revote": "\f1a6", - "save-story": "\f1a7", - "saved-messages": "\f1a8", - "schedule": "\f1a9", - "search": "\f1aa", - "select": "\f1ab", - "send-outline": "\f1ac", - "send": "\f1ad", - "settings-filled": "\f1ae", - "settings": "\f1af", - "share-filled": "\f1b0", - "share-screen-outlined": "\f1b1", - "share-screen-stop": "\f1b2", - "share-screen": "\f1b3", - "sidebar": "\f1b4", - "skip-next": "\f1b5", - "skip-previous": "\f1b6", - "smallscreen": "\f1b7", - "smile": "\f1b8", - "sort": "\f1b9", - "speaker-muted-story": "\f1ba", - "speaker-outline": "\f1bb", - "speaker-story": "\f1bc", - "speaker": "\f1bd", - "spoiler-disable": "\f1be", - "spoiler": "\f1bf", - "sport": "\f1c0", - "stats": "\f1c1", - "stealth-future": "\f1c2", - "stealth-past": "\f1c3", - "stickers": "\f1c4", - "stop-raising-hand": "\f1c5", - "stop": "\f1c6", - "story-caption": "\f1c7", - "story-expired": "\f1c8", - "story-priority": "\f1c9", - "story-reply": "\f1ca", - "strikethrough": "\f1cb", - "tag-add": "\f1cc", - "tag-crossed": "\f1cd", - "tag-filter": "\f1ce", - "tag-name": "\f1cf", - "tag": "\f1d0", - "timer": "\f1d1", - "transcribe": "\f1d2", - "truck": "\f1d3", - "unarchive": "\f1d4", - "underlined": "\f1d5", - "unlock-badge": "\f1d6", - "unlock": "\f1d7", - "unmute": "\f1d8", - "unpin": "\f1d9", - "unread": "\f1da", - "up": "\f1db", - "user-filled": "\f1dc", - "user-online": "\f1dd", - "user": "\f1de", - "video-outlined": "\f1df", - "video-stop": "\f1e0", - "video": "\f1e1", - "view-once": "\f1e2", - "voice-chat": "\f1e3", - "volume-1": "\f1e4", - "volume-2": "\f1e5", - "volume-3": "\f1e6", - "web": "\f1e7", - "webapp": "\f1e8", - "word-wrap": "\f1e9", - "zoom-in": "\f1ea", - "zoom-out": "\f1eb", + "remove-quote": "\f19f", + "remove": "\f1a0", + "reopen-topic": "\f1a1", + "replace": "\f1a2", + "replies": "\f1a3", + "reply-filled": "\f1a4", + "reply": "\f1a5", + "revenue-split": "\f1a6", + "revote": "\f1a7", + "save-story": "\f1a8", + "saved-messages": "\f1a9", + "schedule": "\f1aa", + "search": "\f1ab", + "select": "\f1ac", + "send-outline": "\f1ad", + "send": "\f1ae", + "settings-filled": "\f1af", + "settings": "\f1b0", + "share-filled": "\f1b1", + "share-screen-outlined": "\f1b2", + "share-screen-stop": "\f1b3", + "share-screen": "\f1b4", + "show-message": "\f1b5", + "sidebar": "\f1b6", + "skip-next": "\f1b7", + "skip-previous": "\f1b8", + "smallscreen": "\f1b9", + "smile": "\f1ba", + "sort": "\f1bb", + "speaker-muted-story": "\f1bc", + "speaker-outline": "\f1bd", + "speaker-story": "\f1be", + "speaker": "\f1bf", + "spoiler-disable": "\f1c0", + "spoiler": "\f1c1", + "sport": "\f1c2", + "stats": "\f1c3", + "stealth-future": "\f1c4", + "stealth-past": "\f1c5", + "stickers": "\f1c6", + "stop-raising-hand": "\f1c7", + "stop": "\f1c8", + "story-caption": "\f1c9", + "story-expired": "\f1ca", + "story-priority": "\f1cb", + "story-reply": "\f1cc", + "strikethrough": "\f1cd", + "tag-add": "\f1ce", + "tag-crossed": "\f1cf", + "tag-filter": "\f1d0", + "tag-name": "\f1d1", + "tag": "\f1d2", + "timer": "\f1d3", + "transcribe": "\f1d4", + "truck": "\f1d5", + "unarchive": "\f1d6", + "underlined": "\f1d7", + "unlock-badge": "\f1d8", + "unlock": "\f1d9", + "unmute": "\f1da", + "unpin": "\f1db", + "unread": "\f1dc", + "up": "\f1dd", + "user-filled": "\f1de", + "user-online": "\f1df", + "user": "\f1e0", + "video-outlined": "\f1e1", + "video-stop": "\f1e2", + "video": "\f1e3", + "view-once": "\f1e4", + "voice-chat": "\f1e5", + "volume-1": "\f1e6", + "volume-2": "\f1e7", + "volume-3": "\f1e8", + "web": "\f1e9", + "webapp": "\f1ea", + "word-wrap": "\f1eb", + "zoom-in": "\f1ec", + "zoom-out": "\f1ed", ); .icon-active-sessions::before { @@ -740,6 +742,9 @@ $icons-map: ( .icon-reload::before { content: map.get($icons-map, "reload"); } +.icon-remove-quote::before { + content: map.get($icons-map, "remove-quote"); +} .icon-remove::before { content: map.get($icons-map, "remove"); } @@ -803,6 +808,9 @@ $icons-map: ( .icon-share-screen::before { content: map.get($icons-map, "share-screen"); } +.icon-show-message::before { + content: map.get($icons-map, "show-message"); +} .icon-sidebar::before { content: map.get($icons-map, "sidebar"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index f944e4d08..07826423a 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 1a74a357a..af2268d22 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index 8f49ccf13..27999d7c9 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -157,6 +157,7 @@ export type FontIconName = | 'readchats' | 'recent' | 'reload' + | 'remove-quote' | 'remove' | 'reopen-topic' | 'replace' @@ -178,6 +179,7 @@ export type FontIconName = | 'share-screen-outlined' | 'share-screen-stop' | 'share-screen' + | 'show-message' | 'sidebar' | 'skip-next' | 'skip-previous'