From c88140e256edfb3009827dcb1a7b11224379f707 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 20 Sep 2022 13:35:47 +0400 Subject: [PATCH] Update API scheme to layer 148, start forums. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/tl/api.tl | 22 +++++-- Telegram/SourceFiles/boxes/add_contact_box.h | 1 + .../boxes/peers/edit_peer_info_box.cpp | 65 +++++++++++++++++++ Telegram/SourceFiles/data/data_channel.cpp | 23 +++++++ Telegram/SourceFiles/data/data_channel.h | 21 +++++- Telegram/SourceFiles/data/data_chat.h | 2 +- Telegram/SourceFiles/data/data_forum.cpp | 16 +++++ Telegram/SourceFiles/data/data_forum.h | 21 ++++++ Telegram/SourceFiles/data/data_peer.cpp | 22 ++++++- Telegram/SourceFiles/data/data_peer.h | 1 + Telegram/SourceFiles/data/data_session.cpp | 8 ++- Telegram/SourceFiles/data/data_user.h | 2 +- .../dialogs/dialogs_inner_widget.cpp | 27 ++++++-- .../dialogs/dialogs_inner_widget.h | 4 ++ .../SourceFiles/dialogs/dialogs_widget.cpp | 57 ++++++++++++---- Telegram/SourceFiles/dialogs/dialogs_widget.h | 6 ++ .../export/data/export_data_types.cpp | 4 ++ .../export/data/export_data_types.h | 7 +- .../export/output/export_output_html.cpp | 12 +++- .../export/output/export_output_json.cpp | 4 ++ .../SourceFiles/history/history_service.cpp | 8 +++ .../view/history_view_top_bar_widget.cpp | 15 +++-- .../window/window_session_controller.cpp | 33 ++++++++++ .../window/window_session_controller.h | 6 ++ 25 files changed, 349 insertions(+), 40 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_forum.cpp create mode 100644 Telegram/SourceFiles/data/data_forum.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fb2bf370fb..f6952c4b9a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -467,6 +467,8 @@ PRIVATE data/data_emoji_statuses.h data/data_folder.cpp data/data_folder.h + data/data_forum.cpp + data/data_forum.h data/data_file_click_handler.cpp data/data_file_click_handler.h data/data_file_origin.cpp diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index e14866f112..1f314aae02 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -125,7 +125,7 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#8261ac61 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channel#8261ac61 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; @@ -142,7 +142,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; +message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true topic_start:flags.27?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -192,6 +192,7 @@ messageActionChatJoinedByRequest#ebbca3cb = MessageAction; messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; messageActionGiftPremium#aba0f5c6 currency:string amount:long months:int = MessageAction; +messageActionTopicCreate#4619708d title:string = MessageAction; dialog#a8edd0f5 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -591,7 +592,7 @@ inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; -messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; +messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; @@ -770,7 +771,7 @@ messages.stickerSetInstallResultArchive#35e410a8 sets:Vector stickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered; stickerSetMultiCovered#3407e51b set:StickerSet covers:Vector = StickerSetCovered; -stickerSetFullCovered#1aed5ee5 set:StickerSet packs:Vector documents:Vector = StickerSetCovered; +stickerSetFullCovered#40d13c0e set:StickerSet packs:Vector keywords:Vector documents:Vector = StickerSetCovered; maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords; @@ -1248,7 +1249,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#a6d57763 flags:# reply_to_scheduled:flags.2?true reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; +messageReplyHeader#a6d57763 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1448,6 +1449,12 @@ sendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer messageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia; messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia; +stickerKeyword#fcfeb29c document_id:long keyword:Vector = StickerKeyword; + +forumTopic#4a0005d9 flags:# pinned:flags.2?true id:int date:int title:string top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int = ForumTopic; + +messages.forumTopics#ed93d3e flags:# count:int topics:Vector messages:Vector chats:Vector users:Vector pts:int next_date:flags.0?int = messages.ForumTopics; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1846,6 +1853,9 @@ channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; +channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; +channels.createForumTopic#22cf4868 flags:# no_webpage:flags.3?true channel:InputChannel title:string media:flags.0?InputMedia message:string random_id:long entities:flags.1?Vector send_as:flags.2?InputPeer = Updates; +channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1924,4 +1934,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 146 +// LAYER 148 diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index 34bb6b6d67..8900a8a24d 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -95,6 +95,7 @@ public: Group, Channel, Megagroup, + Forum, }; GroupInfoBox( QWidget*, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 0b2bb79b43..1415247c6d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -266,6 +266,7 @@ private: std::optional title; std::optional description; std::optional hiddenPreHistory; + std::optional forum; std::optional signatures; std::optional noForwards; std::optional joinToWrite; @@ -289,6 +290,7 @@ private: void fillPrivacyTypeButton(); void fillLinkedChatButton(); //void fillInviteLinkButton(); + void fillForumButton(); void fillSignaturesButton(); void fillHistoryVisibilityButton(); void fillManageSection(); @@ -305,6 +307,7 @@ private: [[nodiscard]] bool validateTitle(Saving &to) const; [[nodiscard]] bool validateDescription(Saving &to) const; [[nodiscard]] bool validateHistoryVisibility(Saving &to) const; + [[nodiscard]] bool validateForum(Saving &to) const; [[nodiscard]] bool validateSignatures(Saving &to) const; [[nodiscard]] bool validateForwards(Saving &to) const; [[nodiscard]] bool validateJoinToWrite(Saving &to) const; @@ -316,6 +319,7 @@ private: void saveTitle(); void saveDescription(); void saveHistoryVisibility(); + void saveForum(); void saveSignatures(); void saveForwards(); void saveJoinToWrite(); @@ -339,6 +343,7 @@ private: bool _channelHasLocationOriginalValue = false; std::optional _historyVisibilitySavedValue; std::optional _typeDataSavedValue; + std::optional _forumSavedValue; std::optional _signaturesSavedValue; const not_null _navigation; @@ -800,6 +805,27 @@ void Controller::fillLinkedChatButton() { // buttonCallback); //} +void Controller::fillForumButton() { + Expects(_controls.buttonsLayout != nullptr); + + const auto channel = _peer->asChannel(); + if (!channel) { + return; + } + + AddButtonWithText( + _controls.buttonsLayout, + rpl::single(u"Forum"_q), // #TODO forum + rpl::single(QString()), + [] {}, + { &st::settingsIconGroup, Settings::kIconPurple } + )->toggleOn(rpl::single(channel->isForum()) + )->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + _forumSavedValue = toggled; + }, _controls.buttonsLayout->lifetime()); +} + void Controller::fillSignaturesButton() { Expects(_controls.buttonsLayout != nullptr); @@ -907,6 +933,9 @@ void Controller::fillManageSection() { ? channel->canEditPreHistoryHidden() : chat->canEditPreHistoryHidden(); }(); + const auto canEditForum = isChannel + && channel->isMegagroup() + && channel->canEditInformation(); const auto canEditPermissions = [&] { return isChannel @@ -972,10 +1001,14 @@ void Controller::fillManageSection() { if (canEditPreHistoryHidden) { fillHistoryVisibilityButton(); } + if (canEditForum) { + fillForumButton(); + } if (canEditSignatures) { fillSignaturesButton(); } if (canEditPreHistoryHidden + || canEditForum || canEditSignatures //|| canEditInviteLinks || canViewOrEditLinkedChat @@ -1235,6 +1268,7 @@ std::optional Controller::validate() const { && validateTitle(result) && validateDescription(result) && validateHistoryVisibility(result) + && validateForum(result) && validateSignatures(result) && validateForwards(result) && validateJoinToWrite(result) @@ -1302,6 +1336,14 @@ bool Controller::validateHistoryVisibility(Saving &to) const { return true; } +bool Controller::validateForum(Saving &to) const { + if (!_forumSavedValue.has_value()) { + return true; + } + to.forum = _forumSavedValue; + return true; +} + bool Controller::validateSignatures(Saving &to) const { if (!_signaturesSavedValue.has_value()) { return true; @@ -1347,6 +1389,7 @@ void Controller::save() { pushSaveStage([=] { saveTitle(); }); pushSaveStage([=] { saveDescription(); }); pushSaveStage([=] { saveHistoryVisibility(); }); + pushSaveStage([=] { saveForum(); }); pushSaveStage([=] { saveSignatures(); }); pushSaveStage([=] { saveForwards(); }); pushSaveStage([=] { saveJoinToWrite(); }); @@ -1585,6 +1628,28 @@ void Controller::togglePreHistoryHidden( }).send(); } +void Controller::saveForum() { + const auto channel = _peer->asChannel(); + if (!_savingData.forum + || !channel + || *_savingData.forum == channel->isForum()) { + return continueSave(); + } + _api.request(MTPchannels_ToggleForum( + channel->inputChannel, + MTP_bool(*_savingData.forum) + )).done([=](const MTPUpdates &result) { + channel->session().api().applyUpdates(result); + continueSave(); + }).fail([=](const MTP::Error &error) { + if (error.type() == qstr("CHAT_NOT_MODIFIED")) { + continueSave(); + } else { + cancelSave(); + } + }).send(); +} + void Controller::saveSignatures() { const auto channel = _peer->asChannel(); if (!_savingData.signatures diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index d748cf311e..0cbebc4286 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_session.h" #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_location.h" #include "data/data_histories.h" #include "data/data_group_call.h" @@ -34,6 +35,10 @@ using UpdateFlag = Data::PeerUpdate::Flag; } // namespace +MegagroupInfo::MegagroupInfo() = default; + +MegagroupInfo::~MegagroupInfo() = default; + ChatData *MegagroupInfo::getMigrateFromChat() const { return _migratedFrom; } @@ -55,6 +60,20 @@ Data::ChatBotCommands::Changed MegagroupInfo::setBotCommands( return _botCommands.update(list); } +void MegagroupInfo::setIsForum(bool is) { + if (is == (_forum != nullptr)) { + return; + } else if (is) { + _forum = std::make_unique(); + } else { + _forum = nullptr; + } +} + +Data::Forum *MegagroupInfo::forum() const { + return _forum.get(); +} + ChannelData::ChannelData(not_null owner, PeerId id) : PeerData(owner, id) , inputChannel( @@ -78,6 +97,10 @@ ChannelData::ChannelData(not_null owner, PeerId id) mgInfo = nullptr; } } + if (change.diff & Flag::Forum) { + Assert(mgInfo != nullptr); + mgInfo->setIsForum(change.value & Flag::Forum); + } if (change.diff & Flag::CallNotEmpty) { if (const auto history = this->owner().historyLoaded(this)) { history->updateChatListEntry(); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 6d5d84234d..99f68bf272 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -13,6 +13,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_participant_status.h" #include "data/data_peer_bot_commands.h" +namespace Data { +class Forum; +} // namespace Data + struct ChannelLocation { QString address; Data::LocationPoint point; @@ -55,12 +59,16 @@ enum class ChannelDataFlag { NoForwards = (1 << 20), JoinToWrite = (1 << 21), RequestToJoin = (1 << 22), + Forum = (1 << 23), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; class MegagroupInfo { public: + MegagroupInfo(); + ~MegagroupInfo(); + struct Admin { explicit Admin(ChatAdminRightsInfo rights) : rights(rights) { @@ -92,6 +100,9 @@ public: return _botCommands; } + void setIsForum(bool is); + [[nodiscard]] Data::Forum *forum() const; + std::deque> lastParticipants; base::flat_map, Admin> lastAdmins; base::flat_map, Restricted> lastRestricted; @@ -119,10 +130,11 @@ private: ChatData *_migratedFrom = nullptr; ChannelLocation _location; Data::ChatBotCommands _botCommands; + std::unique_ptr _forum; }; -class ChannelData : public PeerData { +class ChannelData final : public PeerData { public: using Flag = ChannelDataFlag; using Flags = Data::Flags; @@ -243,6 +255,9 @@ public: [[nodiscard]] bool isGigagroup() const { return flags() & Flag::Gigagroup; } + [[nodiscard]] bool isForum() const { + return flags() & Flag::Forum; + } [[nodiscard]] bool hasUsername() const { return flags() & Flag::Username; } @@ -420,6 +435,10 @@ public: void setAllowedReactions(Data::AllowedReactions value); [[nodiscard]] const Data::AllowedReactions &allowedReactions() const; + [[nodiscard]] Data::Forum *forum() const { + return mgInfo ? mgInfo->forum() : nullptr; + } + // Still public data members. uint64 access = 0; diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index 5f8558ddb5..eaccda8c4b 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -27,7 +27,7 @@ enum class ChatDataFlag { inline constexpr bool is_flag_type(ChatDataFlag) { return true; }; using ChatDataFlags = base::flags; -class ChatData : public PeerData { +class ChatData final : public PeerData { public: using Flag = ChatDataFlag; using Flags = Data::Flags; diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp new file mode 100644 index 0000000000..527f63112c --- /dev/null +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -0,0 +1,16 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_forum.h" + +namespace Data { + +Forum::Forum() = default; + +Forum::~Forum() = default; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h new file mode 100644 index 0000000000..437e87f42d --- /dev/null +++ b/Telegram/SourceFiles/data/data_forum.h @@ -0,0 +1,21 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { + +class Forum final { +public: + Forum(); + ~Forum(); + +private: + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index a867cb418d..75c302df36 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -844,15 +844,31 @@ bool PeerData::isFake() const { } bool PeerData::isMegagroup() const { - return isChannel() && asChannel()->isMegagroup(); + if (const auto channel = asChannel()) { + return channel->isMegagroup(); + } + return false; } bool PeerData::isBroadcast() const { - return isChannel() && asChannel()->isBroadcast(); + if (const auto channel = asChannel()) { + return channel->isBroadcast(); + } + return false; +} + +bool PeerData::isForum() const { + if (const auto channel = asChannel()) { + return channel->isForum(); + } + return false; } bool PeerData::isGigagroup() const { - return isChannel() && asChannel()->isGigagroup(); + if (const auto channel = asChannel()) { + return channel->isGigagroup(); + } + return false; } bool PeerData::isRepliesChat() const { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index c17ca33db4..9c880c09b8 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -180,6 +180,7 @@ public: [[nodiscard]] bool isFake() const; [[nodiscard]] bool isMegagroup() const; [[nodiscard]] bool isBroadcast() const; + [[nodiscard]] bool isForum() const; [[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool sharedMediaInfo() const { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 9255638a73..ccea61bf5b 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -777,7 +777,8 @@ not_null Session::processChat(const MTPChat &data) { | (!minimal ? (Flag::Left | Flag::Creator) : Flag()) | Flag::NoForwards | Flag::JoinToWrite - | Flag::RequestToJoin; + | Flag::RequestToJoin + | Flag::Forum; const auto flagsSet = (data.is_broadcast() ? Flag::Broadcast : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) | (data.is_scam() ? Flag::Scam : Flag()) @@ -800,7 +801,10 @@ not_null Session::processChat(const MTPChat &data) { : Flag()) | (data.is_noforwards() ? Flag::NoForwards : Flag()) | (data.is_join_to_send() ? Flag::JoinToWrite : Flag()) - | (data.is_join_request() ? Flag::RequestToJoin : Flag()); + | (data.is_join_request() ? Flag::RequestToJoin : Flag()) + | ((data.is_forum() && data.is_megagroup()) + ? Flag::Forum + : Flag()); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setName( diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 9b138b621f..0e81d5f87d 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -57,7 +57,7 @@ enum class UserDataFlag { inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; -class UserData : public PeerData { +class UserData final : public PeerData { public: using Flag = UserDataFlag; using Flags = Data::Flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 729f595e5d..d677a81d81 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -394,15 +394,22 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) { return; } stopReorderPinned(); - //const auto mouseSelection = _mouseSelection; - //const auto lastMousePosition = _lastMousePosition; clearSelection(); _openedFolder = folder; refreshWithCollapsedRows(true); - // This doesn't work, because we clear selection in leaveEvent on hide. - //if (mouseSelection && lastMousePosition) { - // selectByMouse(*lastMousePosition); - //} + if (_loadMoreCallback) { + _loadMoreCallback(); + } +} + +void InnerWidget::changeOpenedForum(ChannelData *forum) { + if (_openedForum == forum) { + return; + } + stopReorderPinned(); + clearSelection(); + _openedForum = forum; + refreshWithCollapsedRows(true); if (_loadMoreCallback) { _loadMoreCallback(); } @@ -1160,6 +1167,8 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { } else if (qAbs(localPosition.y() - _dragStart.y()) < style::ConvertScale(kStartReorderThreshold)) { return; + } else if (_openedForum) { + return; // #TODO forum } _dragging = _pressed; if (updateReorderIndexGetCount() < 2) { @@ -1311,7 +1320,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto delta = [&] { if (localPosition.y() < _visibleTop) { return localPosition.y() - _visibleTop; - } else if ((_openedFolder || _filterId) + } else if ((_openedFolder || _openedForum || _filterId) && localPosition.y() > _visibleBottom) { return localPosition.y() - _visibleBottom; } @@ -2273,6 +2282,10 @@ Data::Folder *InnerWidget::shownFolder() const { return _openedFolder; } +ChannelData *InnerWidget::shownForum() const { + return _openedForum; +} + bool InnerWidget::needCollapsedRowsRefresh() const { const auto list = shownDialogs(); const auto archive = !list->empty() diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 75df037dd5..e37fc4a325 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -94,6 +94,7 @@ public: void clearSelection(); void changeOpenedFolder(Data::Folder *folder); + void changeOpenedForum(ChannelData *forum); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); @@ -109,6 +110,7 @@ public: void scrollToEntry(const RowDescriptor &entry); Data::Folder *shownFolder() const; + ChannelData *shownForum() const; int32 lastSearchDate() const; PeerData *lastSearchPeer() const; MsgId lastSearchId() const; @@ -179,6 +181,7 @@ private: Loading, NoContacts, EmptyFolder, + EmptyForum, }; Main::Session &session() const; @@ -348,6 +351,7 @@ private: Qt::MouseButton _pressButton = Qt::LeftButton; Data::Folder *_openedFolder = nullptr; + ChannelData *_openedForum = nullptr; std::vector> _collapsedRows; int _collapsedSelected = -1; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index f3befdde74..9da3065040 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -260,7 +260,10 @@ Widget::Widget( ) | rpl::start_with_next([=](const ChosenRow &row) { const auto openSearchResult = !controller->selectingPeer() && row.filteredRow; - if (const auto history = row.key.history()) { + const auto history = row.key.history(); + if (history && history->peer->isForum()) { + controller->openForum(history->peer->asChannel()); + } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller->uniqueChatsInSearchResults() ? ShowAtUnreadMsgId @@ -399,6 +402,15 @@ Widget::Widget( changeOpenedFolder(folder, anim::type::normal); }, lifetime()); + changeOpenedForum( + controller->openedForum().current(), + anim::type::instant); + + controller->openedForum().changes( + ) | rpl::start_with_next([=](ChannelData *forum) { + changeOpenedForum(forum, anim::type::normal); + }, lifetime()); + setupDownloadBar(); } @@ -597,14 +609,14 @@ void Widget::updateControlsVisibility(bool fast) { if (_forwardCancel) { _forwardCancel->show(); } - if (_openedFolder && _filter->hasFocus()) { + if ((_openedFolder || _openedForum) && _filter->hasFocus()) { setFocus(); } if (_updateTelegram) { _updateTelegram->show(); } - _searchControls->setVisible(!_openedFolder); - if (_openedFolder) { + _searchControls->setVisible(!_openedFolder && !_openedForum); + if (_openedFolder || _openedForum) { _folderTopBar->show(); } else { if (hasFocus()) { @@ -618,24 +630,26 @@ void Widget::updateControlsVisibility(bool fast) { _connecting->setForceHidden(false); } -void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { +void Widget::changeOpenedSubsection( + FnMut change, + bool fromRight, + anim::type animated) { _a_show.stop(); if (isHidden()) { animated = anim::type::instant; } if (animated == anim::type::normal) { - _showDirection = folder + _showDirection = fromRight ? Window::SlideDirection::FromRight : Window::SlideDirection::FromLeft; _showAnimationType = ShowAnimation::Internal; _connecting->setForceHidden(true); _cacheUnder = grabForFolderSlideAnimation(); } - _openedFolder = folder; + change(); refreshFolderTopBar(); updateControlsVisibility(true); - _inner->changeOpenedFolder(folder); if (animated == anim::type::normal) { _connecting->setForceHidden(true); _cacheOver = grabForFolderSlideAnimation(); @@ -644,15 +658,31 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { } } +void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { + changeOpenedSubsection([&] { + _openedFolder = folder; + _inner->changeOpenedFolder(folder); + }, (folder != nullptr), animated); +} + +void Widget::changeOpenedForum(ChannelData *forum, anim::type animated) { + changeOpenedSubsection([&] { + _openedForum = forum; + _inner->changeOpenedForum(forum); + }, (forum != nullptr), animated); +} + void Widget::refreshFolderTopBar() { - if (_openedFolder) { + if (_openedFolder || _openedForum) { if (!_folderTopBar) { _folderTopBar.create(this, controller()); updateControlsGeometry(); } _folderTopBar->setActiveChat( HistoryView::TopBarWidget::ActiveChat{ - .key = _openedFolder, + .key = (_openedFolder + ? Dialogs::Key(_openedFolder) + : Dialogs::Key(session().data().history(_openedForum))), .section = Dialogs::EntryState::Section::ChatsList, }, nullptr); @@ -713,7 +743,7 @@ void Widget::checkUpdateStatus() { } void Widget::setInnerFocus() { - if (_openedFolder) { + if (_openedFolder || _openedForum) { setFocus(); } else { _filter->setFocus(); @@ -853,6 +883,8 @@ void Widget::animationCallback() { void Widget::escape() { if (controller()->openedFolder().current()) { controller()->closeFolder(); + } else if (controller()->openedForum().current()) { + controller()->closeForum(); } else if (!cancelSearch()) { if (controller()->activeChatEntryCurrent().key) { controller()->content()->dialogsCancelled(); @@ -1633,6 +1665,7 @@ void Widget::updateLoadMoreChatsVisibility() { return; } const auto hidden = (_openedFolder != nullptr) + || (_openedForum != nullptr) || !_filter->getLastText().isEmpty(); if (_loadMoreChats->isHidden() != hidden) { _loadMoreChats->setVisible(!hidden); @@ -1778,6 +1811,8 @@ void Widget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { if (_openedFolder) { controller()->closeFolder(); + } else if (_openedForum) { + controller()->closeForum(); } else { e->ignore(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index b9440bf983..0c6799b9ab 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -153,7 +153,12 @@ private: void updateControlsGeometry(); void refreshFolderTopBar(); void checkUpdateStatus(); + void changeOpenedSubsection( + FnMut change, + bool fromRight, + anim::type animated); void changeOpenedFolder(Data::Folder *folder, anim::type animated); + void changeOpenedForum(ChannelData *forum, anim::type animated); QPixmap grabForFolderSlideAnimation(); void startSlideAnimation(); @@ -209,6 +214,7 @@ private: object_ptr _scrollToTop; Data::Folder *_openedFolder = nullptr; + ChannelData *_openedForum = nullptr; Dialogs::Key _searchInChat; History *_searchInMigrated = nullptr; PeerData *_searchFromAuthor = nullptr; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 59072f2be7..2524e6ad3d 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1152,6 +1152,10 @@ ServiceAction ParseServiceAction( qs(data.vcurrency())).toUtf8(); content.months = data.vmonths().v; result.content = content; + }, [&](const MTPDmessageActionTopicCreate &data) { + auto content = ActionTopicCreated(); + content.title = ParseString(data.vtitle()); + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 32a4a502b3..4e49d6be3b 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -499,6 +499,10 @@ struct ActionGiftPremium { int months; }; +struct ActionTopicCreated { + Utf8String title; +}; + struct ServiceAction { std::variant< v::null_t, @@ -531,7 +535,8 @@ struct ServiceAction { ActionSetChatTheme, ActionChatJoinedByRequest, ActionWebViewDataSent, - ActionGiftPremium> content; + ActionGiftPremium, + ActionTopicCreated> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index f2bb88d7e0..fd3f03e22f 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1137,13 +1137,19 @@ auto HtmlWriter::Wrap::pushMessage( + "» button to the bot"; }, [&](const ActionGiftPremium &data) { if (!data.months || data.cost.isEmpty()) { - return (serviceFrom + " sent you a gift."); + return serviceFrom + " sent you a gift."; } - return (serviceFrom + return serviceFrom + " sent you a gift for " + data.cost + ": Telegram Premium for " - + QString::number(data.months).toUtf8() + " months."); + + QString::number(data.months).toUtf8() + + " months."; + }, [&](const ActionTopicCreated &data) { + return serviceFrom + + " created topic «" + + SerializeString(data.title) + + "»"; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index f0cb2b6378..74298177c3 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -560,6 +560,10 @@ QByteArray SerializeMessage( if (data.months) { push("months", data.months); } + }, [&](const ActionTopicCreated &data) { + pushActor(); + pushAction("topic_created"); + push("title", data.title); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 2c5759efaa..0a973f7b89 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -633,6 +633,12 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareTopicCreate = [&](const MTPDmessageActionTopicCreate &action) { + auto result = PreparedText{}; + result.text = { "topic created" }; + return result; + }; + setServiceText(action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); @@ -702,6 +708,8 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { return prepareWebViewDataSent(data); }, [&](const MTPDmessageActionGiftPremium &data) { return prepareGiftPremium(data); + }, [&](const MTPDmessageActionTopicCreate &data) { + return prepareTopicCreate(data); }, [&](const MTPDmessageActionWebViewDataSentMe &data) { LOG(("API Error: messageActionWebViewDataSentMe received.")); return PreparedText{ diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index aebf1c12e4..723f471931 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -477,8 +477,9 @@ void TopBarWidget::paintTopBar(Painter &p) { if (folder || history->peer->sharedMediaInfo() || (_activeChat.section == Section::Scheduled) - || (_activeChat.section == Section::Pinned)) { - // #TODO feed name emoji. + || (_activeChat.section == Section::Pinned) + || (_activeChat.section == Section::ChatsList)) { + // #TODO forum name emoji. auto text = (_activeChat.section == Section::Scheduled) ? ((history && history->peer->isSelf()) ? tr::lng_reminder_messages(tr::now) @@ -489,7 +490,9 @@ void TopBarWidget::paintTopBar(Painter &p) { ? folder->chatListName() : history->peer->isSelf() ? tr::lng_saved_messages(tr::now) - : tr::lng_replies_messages(tr::now); + : history->peer->isRepliesChat() + ? tr::lng_replies_messages(tr::now) + : history->peer->name(); const auto textWidth = st::historySavedFont->width(text); if (availableWidth < textWidth) { text = st::historySavedFont->elided(text, availableWidth); @@ -686,6 +689,10 @@ void TopBarWidget::infoClicked() { void TopBarWidget::backClicked() { if (_activeChat.key.folder()) { _controller->closeFolder(); + } else if (_activeChat.section == Section::ChatsList + && _activeChat.key.history() + && _activeChat.key.history()->peer->isForum()) { + _controller->closeForum(); } else { _controller->showBackFromStack(); } @@ -933,7 +940,7 @@ void TopBarWidget::updateControlsVisibility() { const auto isOneColumn = _controller->adaptive().isOneColumn(); auto backVisible = isOneColumn || !_controller->content()->stackIsEmpty() - || _activeChat.key.folder(); + || (_activeChat.section == Section::ChatsList); _back->setVisible(backVisible && !_chooseForReportReason); _cancelChoose->setVisible(_chooseForReportReason.has_value()); if (_info) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index a42c67b34d..357f0bc0ed 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -676,6 +676,19 @@ SessionController::SessionController( closeFolder(); }, lifetime()); + _openedForum.changes( + ) | rpl::filter([](ChannelData *forum) { + return (forum != nullptr); + }) | rpl::map([](ChannelData *forum) { + return forum->flagsValue( + ) | rpl::filter([](ChannelData::Flags::Change change) { + return (change.diff & ChannelData::Flag::Forum) + && !(change.value & ChannelData::Flag::Forum); + }); + }) | rpl::flatten_latest() | rpl::start_with_next([=] { + closeForum(); + }, lifetime()); + session->data().chatsFilters().changed( ) | rpl::start_with_next([=] { checkOpenedFilter(); @@ -853,6 +866,7 @@ void SessionController::openFolder(not_null folder) { resetFakeUnreadWhileOpened(); } setActiveChatsFilter(0); + closeForum(); _openedFolder = folder.get(); } @@ -860,6 +874,21 @@ void SessionController::closeFolder() { _openedFolder = nullptr; } +void SessionController::openForum(not_null forum) { + Expects(forum->isForum()); + + if (_openedForum.current() != forum) { + resetFakeUnreadWhileOpened(); + } + setActiveChatsFilter(0); + closeFolder(); + _openedForum = forum.get(); +} + +void SessionController::closeForum() { + _openedForum = nullptr; +} + void SessionController::setupPremiumToast() { rpl::combine( Data::AmPremiumValue(&session()), @@ -889,6 +918,10 @@ const rpl::variable &SessionController::openedFolder() const { return _openedFolder; } +const rpl::variable &SessionController::openedForum() const { + return _openedForum; +} + void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) { const auto was = _activeChatEntry.current().key.history(); const auto now = row.key.history(); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index dab6cf8025..a4f578e74c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -340,10 +340,15 @@ public: // is changed in the Dialogs::Widget of the current window. rpl::variable searchInChat; bool uniqueChatsInSearchResults() const; + void openFolder(not_null folder); void closeFolder(); const rpl::variable &openedFolder() const; + void openForum(not_null forum); + void closeForum(); + const rpl::variable &openedForum() const; + void setActiveChatEntry(Dialogs::RowDescriptor row); void setActiveChatEntry(Dialogs::Key key); Dialogs::RowDescriptor activeChatEntryCurrent() const; @@ -594,6 +599,7 @@ private: PeerData *_showEditPeer = nullptr; rpl::variable _openedFolder; + rpl::variable _openedForum; rpl::event_stream<> _filtersMenuChanged;