ClosableEmbeddedMessage Menu: Reply in Another Chat (#4504)

This commit is contained in:
Alexander Zinchuk 2024-05-17 15:45:21 +02:00
parent 6eaaa5dce5
commit 6e37c5b163
20 changed files with 451 additions and 219 deletions

View File

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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M20 2.667a2.667 2.667 0 0 0-.306 5.316 8.3 8.3 0 0 1-.562 1.45l-.32.64a1.328 1.328 0 0 0 2.376 1.188l.32-.64a10.9 10.9 0 0 0 1.153-4.887v-.23A2.667 2.667 0 0 0 20 2.666zm6.667 0a2.667 2.667 0 0 0-.307 5.316q-.21.749-.561 1.45l-.32.64a1.328 1.328 0 0 0 2.375 1.188l.32-.64a10.9 10.9 0 0 0 1.154-4.887v-.23a2.667 2.667 0 0 0-2.661-2.837M5.333 14.672h4.634l2.656 2.656h-7.29a1.328 1.328 0 0 1 0-2.656m0 8h12.634l2.656 2.656H5.333a1.328 1.328 0 0 1 0-2.656m21.792 2.575 1.814 1.814a1.328 1.328 0 0 1-1.878 1.878l-24-24a1.328 1.328 0 1 1 1.878-1.878L8.55 6.672H14a1.328 1.328 0 0 1 0 2.656h-2.794l5.344 5.344h10.117a1.328 1.328 0 1 1 0 2.656h-7.461l5.344 5.344h2.117a1.328 1.328 0 0 1 .458 2.575"/></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M3.4 30.7c.2.1.4.1.6.1.4 0 .8-.1 1.1-.5l4.1-4.1c.4-.3.6-.5.7-.6.05 0 .1-.025.15-.05s.1-.05.15-.05h11.4c2.4 0 3.7 0 4.9-.6 1-.5 1.9-1.4 2.4-2.4.6-1.2.6-2.5.6-4.9v-7.2c0-2.4 0-3.7-.6-4.9-.5-1-1.4-1.9-2.4-2.4-1.2-.6-2.5-.6-4.9-.6H10.4c-2.4 0-3.7 0-4.9.6-1 .5-1.9 1.4-2.4 2.4s-.6 2.2-.6 3.8c0 .8.7 1.5 1.5 1.5s1.5-.7 1.5-1.5q0-1.8.3-2.4c.2-.5.6-.9 1.1-1.1.5-.3 1.6-.3 3.5-.3h11.2c1.9 0 3 0 3.5.3.5.2.9.6 1 1.1.3.5.3 1.6.3 3.5v7.2c0 1.9 0 3-.3 3.5s-.6.9-1.1 1.1c-.5.3-1.6.3-3.5.3H11c-.7 0-1.1 0-1.6.1-.4.1-.8.3-1.2.5-.4.3-.7.6-1.2 1.1l-1.5 1.5v-7c0-.8-.7-1.5-1.5-1.5s-1.5.7-1.5 1.5v10.6c0 .6.3 1.2.9 1.4m9.7-11.1c-.5-.5-.5-1.4 0-1.9l2.3-2.4H1.3C.6 15.3 0 14.7 0 14s.6-1.3 1.3-1.3h14.2l-2.4-2.4c-.5-.5-.5-1.4 0-1.9s1.4-.5 1.9 0l4.7 4.7c.2.2.4.5.4.9 0 .3-.1.6-.3.8L15 19.6c-.5.5-1.4.5-1.9 0"/></svg>

After

Width:  |  Height:  |  Size: 864 B

View File

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

View File

@ -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<OwnProps> = ({
}
if (!message) {
return customText || NBSP;
return customText || renderMediaContentType(wrappedMedia) || NBSP;
}
if (isActionMessage(message)) {
@ -155,6 +158,23 @@ const EmbeddedMessage: FC<OwnProps> = ({
);
}
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 (
<span>
{renderText(description)}
</span>
);
}
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<OwnProps> = ({
}
}
const isChatSender = senderChat && senderChat.id === sender?.id;
const isReplyToQuote = isInComposer && Boolean(replyInfo && 'quoteText' in replyInfo && replyInfo?.quoteText);
return (
<>
{!isChatSender && (
{checkShouldRenderSenderTitle() && (
<span className="embedded-sender">
{renderText(isReplyToQuote ? lang('ReplyToQuote', senderTitle) : senderTitle)}
</span>
)}
{icon && <Icon name={icon} className="embedded-chat-icon" />}
{icon && senderChatTitle && renderText(senderChatTitle)}
{icon && senderChatTitle && (
<span className="embedded-sender-chat">
{renderText(senderChatTitle)}
</span>
)}
</>
);
}

View File

@ -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<OwnProps & StateProps> = ({
@ -30,8 +38,10 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
currentUserId,
isManyMessages,
isStory,
isReplying,
}) => {
const {
openChatOrTopicWithReplyInDraft,
setForwardChatOrTopic,
exitForwardMode,
forwardToSavedMessages,
@ -84,9 +94,15 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((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));

View File

@ -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<OwnProps & StateProps> = ({
isContextMenuDisabled,
isReplyToDiscussion,
onClear,
isInChangingRecipientMode,
isChangingChats,
senderChat,
}) => {
const {
resetDraftReplyInfo,
updateDraftReplyInfo,
setEditingId,
focusMessage,
changeForwardRecipient,
changeRecipient,
setForwardNoAuthors,
setForwardNoCaptions,
exitForwardMode,
@ -95,15 +105,17 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
undefined,
!shouldAnimate,
);
useEffect(() => {
if (canAnimate && replyInfo?.isShowingDelayNeeded) {
updateDraftReplyInfo({ isShowingDelayNeeded: false });
}
});
const clearEmbedded = useLastCallback(() => {
if (replyInfo && !shouldForceShowEditing) {
@ -129,24 +146,36 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
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<HTMLButtonElement, 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<OwnProps & StateProps> = ({
);
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<OwnProps & StateProps> = ({
getPeerColorClass(renderingSender),
);
const isShowingReply = replyInfo && !shouldForceShowEditing;
const leftIcon = useMemo(() => {
if (isShowingReply) {
return 'reply';
@ -212,9 +242,9 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
}
return (
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
<div className={className} ref={ref} onContextMenu={handleContextMenu}>
<div className={innerClassName}>
<div className="embedded-left-icon">
<div className="embedded-left-icon" onClick={handleContextMenu}>
{renderingLeftIcon && <Icon name={renderingLeftIcon} />}
{Boolean(replyInfo?.quoteText) && (
<Icon name="quote" className="quote-reply" />
@ -231,6 +261,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
title={(editingId && !isShowingReply) ? lang('EditMessage')
: noAuthors ? lang('HiddenSendersNameDescription') : undefined}
onClick={handleMessageClick}
senderChat={senderChat}
/>
<Button
className="embedded-cancel"
@ -242,7 +273,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
>
<i className="icon icon-close" />
</Button>
{isForwarding && !isContextMenuDisabled && (
{(isShowingReply || isForwarding) && !isContextMenuDisabled && (
<Menu
isOpen={isContextMenuOpen}
transformOriginX={transformOriginX}
@ -254,55 +285,83 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
<MenuItem
icon={!noAuthors ? 'message-succeeded' : undefined}
customIcon={noAuthors ? <i className="icon icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoAuthors({
noAuthors: false,
})}
>
{lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')}
</MenuItem>
<MenuItem
icon={noAuthors ? 'message-succeeded' : undefined}
customIcon={!noAuthors ? <i className="icon icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoAuthors({
noAuthors: true,
})}
>
{lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')}
</MenuItem>
{forwardsHaveCaptions && (
{isForwarding && (
<>
<MenuSeparator />
<MenuItem
icon={!noCaptions ? 'message-succeeded' : undefined}
customIcon={noCaptions ? <i className="icon icon-placeholder" /> : undefined}
icon={!noAuthors ? 'message-succeeded' : undefined}
customIcon={noAuthors ? <i className="icon icon-placeholder" /> : 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')}
</MenuItem>
<MenuItem
icon={noCaptions ? 'message-succeeded' : undefined}
customIcon={!noCaptions ? <i className="icon icon-placeholder" /> : undefined}
icon={noAuthors ? 'message-succeeded' : undefined}
customIcon={!noAuthors ? <i className="icon icon-placeholder" /> : 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')}
</MenuItem>
{forwardsHaveCaptions && (
<>
<MenuSeparator />
<MenuItem
icon={!noCaptions ? 'message-succeeded' : undefined}
customIcon={noCaptions ? <i className="icon icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoCaptions({
noCaptions: false,
})}
>
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')}
</MenuItem>
<MenuItem
icon={noCaptions ? 'message-succeeded' : undefined}
customIcon={!noCaptions ? <i className="icon icon-placeholder" /> : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setForwardNoCaptions({
noCaptions: true,
})}
>
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')}
</MenuItem>
</>
)}
<MenuSeparator />
<MenuItem icon="replace" onClick={handleForwardToAnotherChatClick}>
{lang('ForwardAnotherChat')}
</MenuItem>
</>
)}
{isShowingReply && (
<>
<MenuItem
icon="show-message"
onClick={handleShowMessageClick}
>
{lang('Message.Context.Goto')}
</MenuItem>
{isReplyWithQuote && (
<MenuItem
icon="remove-quote"
onClick={handleRemoveQuoteClick}
>
{lang('RemoveQuote')}
</MenuItem>
)}
<MenuItem icon="replace" onClick={handleChangeReplyRecipientClick}>
{lang('ReplyToAnotherChat')}
</MenuItem>
<MenuItem icon="delete" onClick={handleDoNotReplyClick}>
{lang('DoNotReply')}
</MenuItem>
</>
)}
<MenuSeparator />
<MenuItem icon="replace" onClick={handleChangeRecipientClick}>
{lang('ChangeRecipient')}
</MenuItem>
</Menu>
)}
</div>
@ -319,7 +378,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
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<OwnProps>(
isCurrentUserPremium: selectIsCurrentUserPremium(global),
isContextMenuDisabled,
isReplyToDiscussion,
isInChangingRecipientMode: isModalShown,
isChangingChats,
senderChat,
};
},
)(ComposerEmbeddedMessage));

View File

@ -52,13 +52,11 @@ const MenuItem: FC<MenuItemProps> = (props) => {
const lang = useLang();
const { isTouchScreen } = useAppLayout();
const handleClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (disabled || !onClick) {
e.stopPropagation();
e.preventDefault();
return;
}
onClick(e, clickArg);
});
@ -67,13 +65,12 @@ const MenuItem: FC<MenuItemProps> = (props) => {
return;
}
e.stopPropagation();
if (disabled || !onClick) {
e.stopPropagation();
e.preventDefault();
return;
}
onClick(e, clickArg);
});
const handleMouseDown = useLastCallback((e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>) => {

View File

@ -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<T extends GlobalState>(
global: T,
user: ApiUser,
chatId: string,
): Promise<boolean> {
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<T extends GlobalState>(
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<void> => {
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 });

View File

@ -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: {

View File

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

View File

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

View File

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

View File

@ -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<T extends GlobalState>(
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<T extends GlobalState>
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId];
}
export function selectReplyCanBeSentToChat<T extends GlobalState>(
global: T,
toChatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
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<T extends GlobalState>(
global: T,
toChatId: string,
@ -1374,33 +1391,30 @@ export function selectForwardsCanBeSentToChat<T extends GlobalState>(
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<T extends GlobalState>(

View File

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

View File

@ -24,6 +24,7 @@ const useShowTransition = (
if (closeTimeoutRef.current) {
window.clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = undefined;
}
} else {

View File

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

Binary file not shown.

Binary file not shown.

View File

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