From 93aa65cd7279f4c05c3239fa365ae00c3ef884f4 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 12 Dec 2023 12:34:38 +0100 Subject: [PATCH] Forum: Saving the state of the "View as Messages" option (#4091) --- src/api/gramjs/apiBuilders/chats.ts | 3 ++- src/api/gramjs/methods/chats.ts | 15 +++++++++++++ src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/updater.ts | 6 +++++ src/api/types/chats.ts | 1 + src/api/types/updates.ts | 9 +++++++- .../common/ChatForumLastMessage.tsx | 2 +- src/components/common/Composer.tsx | 22 +++++++++++++++++-- .../common/embedded/EmbeddedMessage.tsx | 3 ++- src/components/left/main/Chat.tsx | 18 ++++++++++----- src/components/left/main/Topic.tsx | 8 ++++++- src/components/middle/HeaderActions.tsx | 2 ++ src/components/middle/HeaderMenuContainer.tsx | 7 +++++- .../composer/ComposerEmbeddedMessage.tsx | 1 + src/global/actions/api/chats.ts | 19 +++++++++++++++- src/global/actions/apiUpdaters/chats.ts | 12 ++++++++++ src/global/types.ts | 4 ++++ src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.json | 1 + 19 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 7f1a2a3f5..a779921f4 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -96,7 +96,7 @@ export function buildApiChatFromDialog( const { peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount, notifySettings: { silent, muteUntil }, - readOutboxMaxId, readInboxMaxId, draft, + readOutboxMaxId, readInboxMaxId, draft, viewForumAsMessages, } = dialog; const isMuted = silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil); @@ -114,6 +114,7 @@ export function buildApiChatFromDialog( muteUntil, ...(unreadMark && { hasUnreadMark: true }), ...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }), + ...(viewForumAsMessages && { isForumAsMessages: true }), ...buildApiChatFieldsFromPeerEntity(peerEntity), }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 6aa1aa32f..1b968145f 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -81,6 +81,7 @@ type FullChatData = { userStatusesById: { [userId: string]: ApiUserStatus }; groupCall?: Partial; membersCount?: number; + isForumAsMessages?: true; }; let onUpdate: OnApiUpdate; @@ -483,6 +484,7 @@ async function getFullChannelInfo( participantsHidden, translationsDisabled, storiesPinnedAvailable, + viewForumAsMessages, } = result.fullChat; if (chatPhoto instanceof GramJs.Photo) { @@ -572,6 +574,7 @@ async function getFullChannelInfo( connectionState: 'disconnected', } : undefined, membersCount: participantsCount, + ...(viewForumAsMessages && { isForumAsMessages: true }), }; } @@ -1880,6 +1883,18 @@ export function togglePeerTranslations({ })); } +export function setViewForumAsMessages({ chat, isEnabled }: { chat: ApiChat; isEnabled: boolean }) { + const { id, accessHash } = chat; + const channel = buildInputEntity(id, accessHash); + + return invokeRequest(new GramJs.channels.ToggleViewForumAsMessages({ + channel: channel as GramJs.InputChannel, + enabled: Boolean(isEnabled), + }), { + shouldReturnTrue: true, + }); +} + function handleUserPrivacyRestrictedUpdates(updates: GramJs.TypeUpdates) { if (!(updates instanceof GramJs.Updates) && !(updates instanceof GramJs.UpdatesCombined)) { return undefined; diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 04ae81dc6..2176fd5e0 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -23,7 +23,7 @@ export { getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest, fetchTopics, deleteTopic, togglePinnedTopic, editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite, joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites, - fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, + fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, setViewForumAsMessages, } from './chats'; export { diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 0c2f13cba..bd629c4c1 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -1131,6 +1131,12 @@ export function updater(update: Update) { location: update.location, isUnconfirmed: update.unconfirmed, }); + } else if (update instanceof GramJs.UpdateChannelViewForumAsMessages) { + onUpdate({ + '@type': 'updateViewForumAsMessages', + chatId: buildApiPeerId(update.channelId, 'channel'), + isEnabled: update.enabled ? true : undefined, + }); } else if (DEBUG) { const params = typeof update === 'object' && 'className' in update ? update.className : update; log('UNEXPECTED UPDATE', params); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 1cc0fea09..60d8d85a4 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -44,6 +44,7 @@ export interface ApiChat { fakeType?: ApiFakeType; color?: ApiPeerColor; isForum?: boolean; + isForumAsMessages?: true; topics?: Record; listedTopicIds?: number[]; topicsCount?: number; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 5ba9589c8..ce7354b4e 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -609,6 +609,12 @@ export type ApiUpdateTopics = { chatId: string; }; +export type ApiUpdateViewForumAsMessages = { + '@type': 'updateViewForumAsMessages'; + chatId: string; + isEnabled?: true; +}; + export type ApiUpdateMessageTranslations = { '@type': 'updateMessageTranslations'; chatId: string; @@ -706,7 +712,8 @@ export type ApiUpdate = ( ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | - ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden + ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | + ApiUpdateViewForumAsMessages ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/components/common/ChatForumLastMessage.tsx b/src/components/common/ChatForumLastMessage.tsx index 779e44bc8..7c191c7aa 100644 --- a/src/components/common/ChatForumLastMessage.tsx +++ b/src/components/common/ChatForumLastMessage.tsx @@ -62,7 +62,7 @@ const ChatForumLastMessage: FC = ({ handleClick: handleOpenTopicClick, handleMouseDown: handleOpenTopicMouseDown, } = useFastClick((e: React.MouseEvent) => { - if (lastActiveTopic.unreadCount === 0) return; + if (lastActiveTopic.unreadCount === 0 || chat.isForumAsMessages) return; e.stopPropagation(); e.preventDefault(); diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index a08bed69f..352681b5c 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -22,6 +22,7 @@ import type { ApiReaction, ApiStealthMode, ApiSticker, + ApiTopic, ApiUser, ApiVideo, } from '../../api/types'; @@ -30,6 +31,7 @@ import type { MessageListType, TabState, } from '../../global/types'; import type { IAnchorPosition, InlineBotSettings, ISettings } from '../../types'; +import { MAIN_THREAD_ID } from '../../api/types'; import { BASE_EMOJI_KEYWORD_LANG, @@ -75,6 +77,7 @@ import { selectScheduledIds, selectTabState, selectTheme, + selectTopicFromMessage, selectUser, selectUserFullInfo, } from '../../global/selectors'; @@ -184,6 +187,7 @@ type StateProps = editingMessage?: ApiMessage; chat?: ApiChat; draft?: ApiDraft; + replyToTopic?: ApiTopic; currentMessageList?: MessageList; isChatWithBot?: boolean; isChatWithSelf?: boolean; @@ -282,6 +286,7 @@ const Composer: FC = ({ messageListType, draft, chat, + replyToTopic, isForCurrentMessageList, isCurrentUserPremium, canSendVoiceByPrivacy, @@ -1292,6 +1297,11 @@ const Composer: FC = ({ || isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus; const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen && !isSymbolMenuOpen; + const placeholderForForumAsMessages = chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID + ? (replyToTopic + ? lang('Chat.InputPlaceholderReplyInTopic', replyToTopic.title) + : lang('Message.Placeholder.MessageInGeneral')) + : undefined; useEffect(() => { if (isComposerHasFocus) { @@ -1680,7 +1690,7 @@ const Composer: FC = ({ activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : (!isComposerBlocked - ? (botKeyboardPlaceholder || inputPlaceholder || lang('Message')) + ? (botKeyboardPlaceholder || inputPlaceholder || lang(placeholderForForumAsMessages || 'Message')) : lang('Chat.PlaceholderTextNotAllowed')) } timedPlaceholderDate={timedPlaceholderDate} @@ -1932,13 +1942,20 @@ export default memo(withGlobal( const story = storyId && selectPeerStory(global, chatId, storyId); const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; + const draft = selectDraft(global, chatId, threadId); + const replyToMessage = draft?.replyInfo + ? selectChatMessage(global, chatId, draft.replyInfo.replyToMsgId) + : undefined; + const replyToTopic = chat?.isForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID && replyToMessage + ? selectTopicFromMessage(global, replyToMessage) + : undefined; return { availableReactions: type === 'story' ? global.availableReactions : undefined, topReactions: type === 'story' ? global.topReactions : undefined, isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), - draft: selectDraft(global, chatId, threadId), + draft, chat, isChatWithBot, isChatWithSelf, @@ -1996,6 +2013,7 @@ export default memo(withGlobal( shouldCollectDebugLogs: global.settings.byKey.shouldCollectDebugLogs, sentStoryReaction, stealthMode: global.stories.stealthMode, + replyToTopic, }; }, )(Composer)); diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 803223b56..e5d9bc725 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -173,12 +173,13 @@ const EmbeddedMessage: FC = ({ } const isChatSender = senderChat?.id === sender?.id; + const isReplyToQuote = isInComposer && Boolean(replyInfo && 'quoteText' in replyInfo && replyInfo?.quoteText); return ( <> {!isChatSender && ( - {renderText(isInComposer ? lang('ReplyToQuote', senderTitle) : senderTitle)} + {renderText(isReplyToQuote ? lang('ReplyToQuote', senderTitle) : senderTitle)} )} {icon && } diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 782585a09..51bf9674b 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -147,7 +147,7 @@ const Chat: FC = ({ const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag(); - const { lastMessage, isForum } = chat || {}; + const { lastMessage, isForum, isForumAsMessages } = chat || {}; const { renderSubtitle, ref } = useChatListEntry({ chat, @@ -169,17 +169,23 @@ const Chat: FC = ({ const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); const handleClick = useLastCallback(() => { + const noForumTopicPanel = isMobile && isForumAsMessages; + if (isForum) { if (isForumPanelOpen) { closeForumPanel(undefined, { forceOnHeavyAnimation: true }); - } else { - openForumPanel({ chatId }, { forceOnHeavyAnimation: true }); - } - return; + return; + } else { + if (!noForumTopicPanel) { + openForumPanel({ chatId }, { forceOnHeavyAnimation: true }); + } + + if (!isForumAsMessages) return; + } } - openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true }); + openChat({ id: chatId, noForumTopicPanel, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true }); if (isSelected && canScrollDown) { focusLastMessage(); diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index 01e1a665e..d6d5c98de 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -92,7 +92,12 @@ const Topic: FC = ({ draft, wasTopicOpened, }) => { - const { openThread, deleteTopic, focusLastMessage } = getActions(); + const { + openThread, + deleteTopic, + focusLastMessage, + setViewForumAsMessages, + } = getActions(); const lang = useLang(); @@ -141,6 +146,7 @@ const Topic: FC = ({ const handleOpenTopic = useLastCallback(() => { openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true }); + setViewForumAsMessages({ chatId, isEnabled: false }); if (canScrollDown) { focusLastMessage(); diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index 81f4a8766..f5c24669d 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -130,6 +130,7 @@ const HeaderActions: FC = ({ openChatLanguageModal, setSettingOption, unblockUser, + setViewForumAsMessages, } = getActions(); // eslint-disable-next-line no-null/no-null const menuButtonRef = useRef(null); @@ -209,6 +210,7 @@ const HeaderActions: FC = ({ const handleAsMessagesClick = useLastCallback(() => { openChat({ id: chatId }); + setViewForumAsMessages({ chatId, isEnabled: true }); }); function handleRequestCall() { diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index ccf0b8f4f..74fdd3e83 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -104,6 +104,7 @@ type StateProps = { isMuted?: boolean; isTopic?: boolean; isForum?: boolean; + isForumAsMessages?: true; canAddContact?: boolean; canReportChat?: boolean; canDeleteChat?: boolean; @@ -133,6 +134,7 @@ const HeaderMenuContainer: FC = ({ withForumActions, isTopic, isForum, + isForumAsMessages, isChatInfoShown, canStartBot, canSubscribe, @@ -189,6 +191,7 @@ const HeaderMenuContainer: FC = ({ togglePeerTranslations, blockUser, unblockUser, + setViewForumAsMessages, } = getActions(); const { isMobile } = useAppLayout(); @@ -280,6 +283,7 @@ const HeaderMenuContainer: FC = ({ const handleViewAsTopicsClick = useLastCallback(() => { openChat({ id: undefined }); + setViewForumAsMessages({ chatId, isEnabled: false }); closeMenu(); }); @@ -469,7 +473,7 @@ const HeaderMenuContainer: FC = ({
{pendingJoinRequests}
)} - {withForumActions && !isTopic && ( + {withForumActions && !isTopic && !isForumAsMessages && ( ( isPrivate, isTopic: chat?.isForum && !isMainThread, isForum: chat?.isForum, + isForumAsMessages: chat?.isForumAsMessages, canAddContact, canReportChat, canDeleteChat: getCanDeleteChat(chat), diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index 542a70606..a8e1fdb92 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -327,6 +327,7 @@ export default memo(withGlobal( const draft = selectDraft(global, chatId, threadId); const replyInfo = draft?.replyInfo; + let message: ApiMessage | undefined; if (replyInfo && !shouldForceShowEditing) { message = selectChatMessage(global, replyInfo.replyToPeerId || chatId, replyInfo.replyToMsgId); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index eeaf225c1..b2f6bf3b4 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -2503,6 +2503,20 @@ addActionHandler('togglePeerTranslations', async (global, actions, payload): Pro setGlobal(global); }); +addActionHandler('setViewForumAsMessages', (global, actions, payload): ActionReturnType => { + const { chatId, isEnabled } = payload; + + const chat = selectChat(global, chatId); + if (!chat?.isForum || chat.isForumAsMessages === isEnabled) { + return; + } + + global = updateChat(global, chatId, { isForumAsMessages: isEnabled || undefined }); + setGlobal(global); + + void callApi('setViewForumAsMessages', { chat, isEnabled }); +}); + async function loadChats( listType: 'active' | 'archived', offsetId?: string, @@ -2638,7 +2652,7 @@ export async function loadFullChat( } const { - users, userStatusesById, fullInfo, groupCall, membersCount, + users, userStatusesById, fullInfo, groupCall, membersCount, isForumAsMessages, } = result; global = getGlobal(); @@ -2664,6 +2678,9 @@ export async function loadFullChat( if (membersCount !== undefined) { global = updateChat(global, chat.id, { membersCount }); } + if (chat.isForum) { + global = updateChat(global, chat.id, { isForumAsMessages }); + } global = replaceChatFullInfo(global, chat.id, fullInfo); setGlobal(global); diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index dba0f4141..587840dcb 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -469,6 +469,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { return undefined; } + + case 'updateViewForumAsMessages': { + const { chatId, isEnabled } = update; + + const chat = selectChat(global, chatId); + if (!chat?.isForum) return undefined; + + global = updateChat(global, chatId, { + isForumAsMessages: isEnabled, + }); + setGlobal(global); + } } return undefined; diff --git a/src/global/types.ts b/src/global/types.ts index dc15c0240..0ef53392a 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2819,6 +2819,10 @@ export interface ActionPayloads { isMuted?: boolean; muteUntil?: number; }; + setViewForumAsMessages: { + chatId: string; + isEnabled: boolean; + }; openCreateTopicPanel: { chatId: string; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 69ff8e03a..52d3483d6 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1442,6 +1442,7 @@ channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinne channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; +channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 712117ba1..bd2391d50 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -289,6 +289,7 @@ "channels.deleteTopicHistory", "channels.toggleParticipantsHidden", "channels.clickSponsoredMessage", + "channels.toggleViewForumAsMessages", "photos.uploadContactProfilePhoto", "messages.getMessagesViews", "chatlists.exportChatlistInvite",