From 43b44991251183d807c08b6f108ffcd06b1a5a4a Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 12 May 2025 12:41:00 +0400 Subject: [PATCH] Add monoforum sender bar divider. --- Telegram/Resources/langs/lang.strings | 2 + .../boxes/peers/edit_peer_info_box.cpp | 4 +- Telegram/SourceFiles/data/data_channel.cpp | 6 - Telegram/SourceFiles/data/data_channel.h | 1 - .../data/data_chat_participant_status.h | 5 +- Telegram/SourceFiles/data/data_histories.cpp | 6 +- Telegram/SourceFiles/data/data_peer.cpp | 43 ++++++- Telegram/SourceFiles/data/data_peer.h | 33 ++++- Telegram/SourceFiles/data/data_session.cpp | 4 + .../SourceFiles/dialogs/dialogs_widget.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 5 +- Telegram/SourceFiles/history/history_item.cpp | 3 +- .../history/history_item_components.h | 82 +++++++------ .../SourceFiles/history/history_widget.cpp | 17 ++- .../view/history_view_chat_section.cpp | 10 +- .../history/view/history_view_element.cpp | 114 +++++++++++++++++- .../history/view/history_view_element.h | 38 ++++-- .../history/view/history_view_message.cpp | 30 +++++ .../history/view/history_view_message.h | 8 +- .../view/history_view_service_message.cpp | 27 +++++ .../view/history_view_top_bar_widget.cpp | 8 +- .../info/profile/info_profile_cover.cpp | 16 ++- .../profile/info_profile_inner_widget.cpp | 4 +- .../SourceFiles/overview/overview_layout.h | 2 +- Telegram/SourceFiles/ui/chat/chat.style | 1 + .../ui/controls/userpic_button.cpp | 4 +- .../SourceFiles/ui/controls/userpic_button.h | 3 +- 27 files changed, 388 insertions(+), 90 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2d1ccfd8cc..0ad0b60ee9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6091,6 +6091,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_forum_messages#other" = "{count} messages"; "lng_forum_show_topics_list" = "Show Topics List"; +"lng_monoforum_choose_to_reply" = "Choose a message to reply."; + "lng_request_peer_requirements" = "Requirements"; "lng_request_peer_rights" = "You must have these admin rights: {rights}."; "lng_request_peer_rights_and" = "{rights} and {last}"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 4bc44e3bd6..4028efa07a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -2936,7 +2936,9 @@ bool EditPeerInfoBox::Available(not_null peer) { // canViewAdmins() is removed, because in supergroups it is // always true and in channels it is equal to canViewBanned(). - + if (channel->isMonoforum()) { + return false; + } return false //|| channel->canViewMembers() //|| channel->canViewAdmins() diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index fc0842c896..0159fcca80 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -341,12 +341,6 @@ ChannelData *ChannelData::monoforumLink() const { return _monoforumLink; } -bool ChannelData::requiresMonoforumPeer() const { - return isMonoforum() - && _monoforumLink - && (_monoforumLink->amCreator() || _monoforumLink->hasAdminRights()); -} - void ChannelData::setMembersCount(int newMembersCount) { if (_membersCount != newMembersCount) { if (isMegagroup() diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 9b3150de8f..6dc802dcfc 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -429,7 +429,6 @@ public: void setMonoforumLink(ChannelData *link); [[nodiscard]] ChannelData *monoforumLink() const; - [[nodiscard]] bool requiresMonoforumPeer() const; void ptsInit(int32 pts) { _ptsWaiter.init(pts); diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index b3db584a4e..17a6ddfe7d 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -190,18 +190,21 @@ struct SendError { struct Args { QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; }; SendError(Args &&args) : text(std::move(args.text)) , boostsToLift(args.boostsToLift) + , monoforumAdmin(args.monoforumAdmin) , premiumToLift(args.premiumToLift) , frozen(args.frozen) { } QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; @@ -210,7 +213,7 @@ struct SendError { } explicit operator bool() const { - return !text.isEmpty(); + return monoforumAdmin || !text.isEmpty(); } [[nodiscard]] bool has_value() const { return !text.isEmpty(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index e7c5a14da4..bd7093579c 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -65,8 +65,7 @@ MTPInputReplyTo ReplyToForMTP( : replyTo.monoforumPeerId ? history->owner().peer(replyTo.monoforumPeerId).get() : history->session().user().get(); - const auto replyToMonoforumPeer = (history->peer->isChannel() - && history->peer->asChannel()->requiresMonoforumPeer()) + const auto replyToMonoforumPeer = history->peer->amMonoforumAdmin() ? possibleMonoforumPeer : nullptr; const auto external = replyTo.messageId @@ -98,8 +97,7 @@ MTPInputReplyTo ReplyToForMTP( (replyToMonoforumPeer ? replyToMonoforumPeer->input : MTPInputPeer())); - } else if (history->peer->isChannel() - && history->peer->asChannel()->requiresMonoforumPeer() + } else if (history->peer->amMonoforumAdmin() && replyTo.monoforumPeerId) { const auto replyToMonoforumPeer = replyTo.monoforumPeerId ? history->owner().peer(replyTo.monoforumPeerId) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 851fd9a0c4..b6191a4dfe 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -427,10 +427,12 @@ QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const { void PeerData::paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle) const { + const PaintUserpicContext &context) const { + if (const auto broadcast = monoforumBroadcast()) { + broadcast->paintUserpic(p, view, context); + return; + } + const auto size = context.size; const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); Ui::ValidateUserpicCache( @@ -438,8 +440,8 @@ void PeerData::paintUserpic( cloud, cloud ? nullptr : ensureEmptyUserpic().get(), size * ratio, - !forceCircle && (isForum() || isMonoforum())); - p.drawImage(QRect(x, y, size, size), view.cached); + context.forumLayout); + p.drawImage(QRect(context.position, QSize(size, size)), view.cached); } void PeerData::loadUserpic() { @@ -1118,6 +1120,16 @@ const ChannelData *PeerData::asChannelOrMigrated() const { return migrateTo(); } +ChannelData *PeerData::asMonoforum() { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + +const ChannelData *PeerData::asMonoforum() const { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + ChatData *PeerData::migrateFrom() const { if (const auto megagroup = asMegagroup()) { return megagroup->amIn() @@ -1150,6 +1162,16 @@ not_null PeerData::migrateToOrMe() const { return this; } +ChannelData *PeerData::monoforumBroadcast() const { + const auto monoforum = asMonoforum(); + return monoforum ? monoforum->monoforumLink() : nullptr; +} + +ChannelData *PeerData::broadcastMonoforum() const { + const auto broadcast = asBroadcast(); + return broadcast ? broadcast->monoforumLink() : nullptr; +} + const QString &PeerData::topBarNameText() const { if (const auto to = migrateTo()) { return to->topBarNameText(); @@ -1572,12 +1594,21 @@ bool PeerData::canManageGroupCall() const { return chat->amCreator() || (chat->adminRights() & ChatAdminRight::ManageCall); } else if (const auto group = asChannel()) { + if (group->isMonoforum()) { + return false; + } return group->amCreator() || (group->adminRights() & ChatAdminRight::ManageCall); } return false; } +bool PeerData::amMonoforumAdmin() const { + const auto broadcast = monoforumBroadcast(); + return broadcast + && (broadcast->amCreator() || broadcast->hasAdminRights()); +} + int PeerData::starsPerMessage() const { if (const auto user = asUser()) { return user->starsPerMessage(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index a4a1ebe531..e185fc58ea 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -277,6 +277,7 @@ public: [[nodiscard]] rpl::producer slowmodeAppliedValue() const; [[nodiscard]] int slowmodeSecondsLeft() const; [[nodiscard]] bool canManageGroupCall() const; + [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] int starsPerMessage() const; [[nodiscard]] int starsPerMessageChecked() const; @@ -297,12 +298,20 @@ public: [[nodiscard]] const ChatData *asChatNotMigrated() const; [[nodiscard]] ChannelData *asChannelOrMigrated(); [[nodiscard]] const ChannelData *asChannelOrMigrated() const; + [[nodiscard]] ChannelData *asMonoforum(); + [[nodiscard]] const ChannelData *asMonoforum() const; [[nodiscard]] ChatData *migrateFrom() const; [[nodiscard]] ChannelData *migrateTo() const; [[nodiscard]] not_null migrateToOrMe(); [[nodiscard]] not_null migrateToOrMe() const; + // isMonoforum() ? monoforumLink() : nullptr + [[nodiscard]] ChannelData *monoforumBroadcast() const; + + // isMonoforum() ? nullptr : monoforumLink() + [[nodiscard]] ChannelData *broadcastMonoforum() const; + void updateFull(); void updateFullForced(); void fullUpdated(); @@ -332,13 +341,29 @@ public: const ImageLocation &location, bool hasVideo); void setUserpicPhoto(const MTPPhoto &data); + + struct PaintUserpicContext { + QPoint position; + int size = 0; + bool forumLayout = false; + }; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle = false) const; + const PaintUserpicContext &context) const; + void paintUserpic( + Painter &p, + Ui::PeerUserpicView &view, + int x, + int y, + int size, + bool forceCircle = false) const { + paintUserpic(p, view, { + .position = { x, y }, + .size = size, + .forumLayout = !forceCircle && (isForum() || isMonoforum()), + }); + } void paintUserpicLeft( Painter &p, Ui::PeerUserpicView &view, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index de96c6c58a..d995d42083 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4657,6 +4657,10 @@ void Session::refreshChatListEntry(Dialogs::Key key) { if (const auto forum = history->peer->forum()) { forum->preloadTopics(); } + if (history->peer->isMonoforum() + && !history->peer->monoforumBroadcast()) { + history->peer->updateFull(); + } } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index b593d960b5..6dea61f38a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -941,7 +941,7 @@ void Widget::chosenRow(const ChosenRow &row) { } return; } else if (history - && history->isMonoforum() + && history->peer->amMonoforumAdmin() && !row.message.fullId && !controller()->adaptive().isOneColumn()) { const auto monoforum = history->peer->monoforum(); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 641adcae73..9e433de7c2 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2789,7 +2789,10 @@ bool History::shouldBeInChatList() const { } else if (isPinnedDialog(FilterId())) { return true; } else if (const auto channel = peer->asChannel()) { - if (!channel->amIn()) { + if (channel->isMonoforum()) { + return !lastMessageKnown() + || (lastMessage() != nullptr); + } else if (!channel->amIn()) { return isTopPromoted(); } } else if (const auto chat = peer->asChat()) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 2f9bf79236..e084f47e6e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3770,8 +3770,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } - const auto requiresMonoforumPeer = _history->peer->isChannel() - && _history->peer->asChannel()->requiresMonoforumPeer(); + const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); if (_history->peer->isSelf() || config.savedSublistPeer || requiresMonoforumPeer) { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 57dbeba73f..33fa873334 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -57,7 +57,7 @@ struct BotKeyboardButton; extern const char kOptionFastButtonsMode[]; [[nodiscard]] bool FastButtonsMode(); -struct HistoryMessageVia : public RuntimeComponent { +struct HistoryMessageVia : RuntimeComponent { void create(not_null owner, UserId userId); void resize(int32 availw) const; @@ -68,7 +68,8 @@ struct HistoryMessageVia : public RuntimeComponent { +struct HistoryMessageViews +: RuntimeComponent { static constexpr auto kMaxRecentRepliers = 3; struct Part { @@ -87,13 +88,15 @@ struct HistoryMessageViews : public RuntimeComponent { +struct HistoryMessageSigned +: RuntimeComponent { QString author; UserData *viaBusinessBot = nullptr; bool isAnonymousRank = false; }; -struct HistoryMessageEdited : public RuntimeComponent { +struct HistoryMessageEdited +: RuntimeComponent { TimeId date = 0; }; @@ -134,7 +137,8 @@ private: }; -struct HistoryMessageForwarded : public RuntimeComponent { +struct HistoryMessageForwarded +: RuntimeComponent { void create( const HistoryMessageVia *via, not_null item) const; @@ -162,12 +166,14 @@ struct HistoryMessageForwarded : public RuntimeComponent { +struct HistoryMessageSavedMediaData +: RuntimeComponent { TextWithEntities text; std::unique_ptr media; }; -struct HistoryMessageSaved : public RuntimeComponent { +struct HistoryMessageSaved +: RuntimeComponent { Data::SavedSublist *sublist = nullptr; }; @@ -274,7 +280,7 @@ struct ReplyFields { const MTPInputReplyTo &reply); struct HistoryMessageReply - : public RuntimeComponent { +: RuntimeComponent { HistoryMessageReply(); HistoryMessageReply(const HistoryMessageReply &other) = delete; HistoryMessageReply(HistoryMessageReply &&other) = delete; @@ -358,7 +364,7 @@ private: }; struct HistoryMessageTranslation - : public RuntimeComponent { +: RuntimeComponent { TextWithEntities text; LanguageId to; bool requested = false; @@ -367,7 +373,7 @@ struct HistoryMessageTranslation }; struct HistoryMessageReplyMarkup - : public RuntimeComponent { +: RuntimeComponent { using Button = HistoryMessageMarkupButton; void createForwarded(const HistoryMessageReplyMarkup &original); @@ -565,7 +571,7 @@ private: // Special type of Component for the channel actions log. struct HistoryMessageLogEntryOriginal -: public RuntimeComponent { +: RuntimeComponent { HistoryMessageLogEntryOriginal(); HistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal &&other); HistoryMessageLogEntryOriginal &operator=(HistoryMessageLogEntryOriginal &&other); @@ -597,19 +603,19 @@ struct MessageFactcheck { const tl::conditional &factcheck); struct HistoryMessageFactcheck -: public RuntimeComponent { +: RuntimeComponent { MessageFactcheck data; WebPageData *page = nullptr; bool requested = false; }; struct HistoryMessageRestrictions -: public RuntimeComponent { +: RuntimeComponent { std::vector reasons; }; struct HistoryServiceData -: public RuntimeComponent { +: RuntimeComponent { std::vector textLinks; }; @@ -625,13 +631,13 @@ struct HistoryServiceDependentData { }; struct HistoryServicePinned -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceTopicInfo -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { QString title; DocumentId iconId = 0; bool closed = false; @@ -652,14 +658,14 @@ struct HistoryServiceTopicInfo }; struct HistoryServiceGameScore -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { int score = 0; }; struct HistoryServicePayment -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { QString slug; TextWithEntities amount; ClickHandlerPtr invoiceLink; @@ -669,22 +675,22 @@ struct HistoryServicePayment }; struct HistoryServiceSameBackground -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceGiveawayResults -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceCustomLink -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; struct HistoryServicePaymentRefund -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; PeerData *peer = nullptr; QString transactionId; @@ -707,7 +713,7 @@ struct TimeToLiveSingleView { }; struct HistoryServiceSelfDestruct -: public RuntimeComponent { +: RuntimeComponent { using Type = HistorySelfDestructType; Type type = Type::Photo; @@ -716,24 +722,25 @@ struct HistoryServiceSelfDestruct }; struct HistoryServiceOngoingCall -: public RuntimeComponent { +: RuntimeComponent { CallId id = 0; ClickHandlerPtr link; rpl::lifetime lifetime; }; struct HistoryServiceChatThemeChange -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; struct HistoryServiceTTLChange -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; class FileClickHandler; -struct HistoryDocumentThumbed : public RuntimeComponent { +struct HistoryDocumentThumbed +: RuntimeComponent { std::shared_ptr linksavel; std::shared_ptr linkopenwithl; std::shared_ptr linkcancell; @@ -745,13 +752,15 @@ struct HistoryDocumentThumbed : public RuntimeComponent { +struct HistoryDocumentCaptioned +: RuntimeComponent { HistoryDocumentCaptioned(); Ui::Text::String caption; }; -struct HistoryDocumentNamed : public RuntimeComponent { +struct HistoryDocumentNamed +: RuntimeComponent { Ui::Text::String name; }; @@ -763,7 +772,8 @@ struct HistoryDocumentVoicePlayback { Ui::Animations::Basic progressAnimation; }; -class HistoryDocumentVoice : public RuntimeComponent { +class HistoryDocumentVoice +: public RuntimeComponent { // We don't use float64 because components should align to pointer even on 32bit systems. static constexpr float64 kFloatToIntMultiplier = 65536.; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 85682e38eb..0ce8c1becd 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6633,6 +6633,12 @@ int HistoryWidget::countAutomaticScrollTop() { } Data::SendError HistoryWidget::computeSendRestriction() const { + if (!_canSendMessages && _peer->amMonoforumAdmin()) { + return Data::SendError({ + .text = tr::lng_monoforum_choose_to_reply(tr::now), + .monoforumAdmin = true, + }); + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; return (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls)) @@ -8753,10 +8759,17 @@ bool HistoryWidget::updateCanSendMessage() { const auto topic = resolveReplyToTopic(); const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; - const auto newCanSendMessages = topic + const auto onlyReplies = _peer->amMonoforumAdmin(); + const auto restrictedOnlyReplies = onlyReplies + && (!_replyTo.messageId || _replyTo.messageId.peer != _peer->id); + const auto newCanSendMessages = restrictedOnlyReplies + ? false + : topic ? Data::CanSendAnyOf(topic, allWithoutPolls) : Data::CanSendAnyOf(_peer, allWithoutPolls); - const auto newCanSendTexts = topic + const auto newCanSendTexts = restrictedOnlyReplies + ? false + : topic ? Data::CanSend(topic, ChatRestriction::SendOther) : Data::CanSend(_peer, ChatRestriction::SendOther); if (_canSendMessages == newCanSendMessages diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 50d61f25d7..6883918710 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2702,7 +2702,11 @@ QRect ChatWidget::floatPlayerAvailableRect() { } Context ChatWidget::listContext() { - return _sublist ? Context::SavedSublist : Context::Replies; + return !_sublist + ? Context::Replies + : _sublist->parentChat() + ? Context::Monoforum + : Context::SavedSublist; } bool ChatWidget::listScrollTo(int top, bool syntetic) { @@ -2883,9 +2887,7 @@ void ChatWidget::listMarkReadTill(not_null item) { void ChatWidget::listMarkContentsRead( const base::flat_set> &items) { - if (!_sublist) { - session().api().markContentsRead(items); - } + session().api().markContentsRead(items); } MessagesBarData ChatWidget::listMessagesBar( diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 63faa7f783..21f0946ef0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -41,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/rect.h" #include "data/components/sponsored_messages.h" +#include "data/data_channel.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" @@ -475,6 +477,83 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } +void MonoforumSenderBar::init( + not_null parentChat, + not_null peer) { + author = peer; + text.setText(st::semiboldTextStyle, peer->name()); + const auto skip = st::monoforumBarUserpicSkip; + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + width = skip + userpic + skip * 2 + text.maxWidth() + st::msgServicePadding.right(); +} + +int MonoforumSenderBar::height() const { + return st::msgServiceMargin.top() + + st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + + st::msgServiceMargin.bottom(); +} + +void MonoforumSenderBar::paint( + Painter &p, + not_null st, + int y, + int w, + bool chatWide) const { + Expects(author != nullptr); + + int left = st::msgServiceMargin.left(); + const auto maxwidth = chatWide + ? std::min(w, WideChatWidth()) + : w; + w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); + + const auto use = std::min(w, width); + + left += (w - use) / 2; + int h = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); + ServiceMessagePainter::PaintBubble( + p, + st->msgServiceBg(), + st->serviceBgCornersNormal(), + QRect(left, y + st::msgServiceMargin.top(), use, h)); + + const auto skip = st::monoforumBarUserpicSkip; + { + auto pen = st->msgServiceBg()->p; + pen.setWidthF(skip); + pen.setCapStyle(Qt::RoundCap); + pen.setDashPattern({ 2., 2. }); + p.setPen(pen); + const auto top = y + st::msgServiceMargin.top() + (h / 2); + p.drawLine(0, top, left, top); + p.drawLine(left + use, top, 2 * w, top); + } + + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + const auto available = use - (skip + userpic + skip * 2 + st::msgServicePadding.right()); + + author->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); + + p.setFont(st::msgServiceFont); + p.setPen(st->msgServiceFg()); + text.draw(p, { + .position = { + left + skip + userpic + skip * 2, + y + st::msgServiceMargin.top() + st::msgServicePadding.top(), + }, + .availableWidth = available, + .elisionLines = 1, + }); +} + void ServicePreMessage::init(PreparedServiceText string) { text = Ui::Text::String( st::serviceTextStyle, @@ -1220,6 +1299,7 @@ void Element::validateTextSkipBlock(bool has, int width, int height) { } void Element::previousInBlocksChanged() { + recountMonoforumSenderBarInBlocks(); recountDisplayDateInBlocks(); recountAttachToPreviousInBlocks(); } @@ -1255,7 +1335,8 @@ bool Element::computeIsAttachToPrevious(not_null previous) { const auto item = data(); if (!Has() && !Has() - && !Has()) { + && !Has() + && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1385,6 +1466,37 @@ void Element::recountAttachToPreviousInBlocks() { setAttachToPrevious(attachToPrevious, previous); } +void Element::recountMonoforumSenderBarInBlocks() { + const auto item = data(); + const auto sublist = item->savedSublist(); + const auto parentChat = sublist ? sublist->parentChat() : nullptr; + const auto barPeer = [&]() -> PeerData* { + if (!parentChat + || isHidden() + || item->isEmpty() + || item->isSponsored()) { + return nullptr; + } + const auto peer = sublist->peer(); + if (const auto previous = previousDisplayedInBlocks()) { + const auto prev = previous->data(); + if (const auto prevSublist = prev->savedSublist()) { + Assert(prevSublist->parentChat() == parentChat); + if (prevSublist->peer() == peer) { + return nullptr; + } + } + } + return peer; + }(); + if (barPeer && !Has()) { + AddComponents(MonoforumSenderBar::Bit()); + Get()->init(parentChat, barPeer); + } else if (!barPeer && Has()) { + RemoveComponents(MonoforumSenderBar::Bit()); + } +} + void Element::recountDisplayDateInBlocks() { setDisplayDate([&] { const auto item = data(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index af62b3f3b6..e7c5ac94f9 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/runtime_composer.h" #include "base/flags.h" #include "base/weak_ptr.h" +#include "ui/userpic_view.h" class History; class HistoryBlock; @@ -58,6 +59,7 @@ enum class Context : char { Pinned, AdminLog, ContactPreview, + Monoforum, SavedSublist, TTLViewer, ShortcutMessages, @@ -220,7 +222,7 @@ QString DateTooltipText(not_null view); // Any HistoryView::Element can have this Component for // displaying the unread messages bar above the message. -struct UnreadBar : public RuntimeComponent { +struct UnreadBar : RuntimeComponent { void init(const QString &string); static int height(); @@ -241,7 +243,7 @@ struct UnreadBar : public RuntimeComponent { // Any HistoryView::Element can have this Component for // displaying the day mark above the message. -struct DateBadge : public RuntimeComponent { +struct DateBadge : RuntimeComponent { void init(const QString &date); int height() const; @@ -257,10 +259,27 @@ struct DateBadge : public RuntimeComponent { }; +struct MonoforumSenderBar : RuntimeComponent { + void init(not_null parentChat, not_null peer); + + int height() const; + void paint( + Painter &p, + not_null st, + int y, + int w, + bool chatWide) const; + + PeerData *author = nullptr; + Ui::Text::String text; + ClickHandlerPtr link; + mutable Ui::PeerUserpicView view; + int width = 0; +}; + // Any HistoryView::Element can have this Component for // displaying some text in layout of a service message above the message. -struct ServicePreMessage - : public RuntimeComponent { +struct ServicePreMessage : RuntimeComponent { void init(PreparedServiceText string); int resizeToWidth(int newWidth, bool chatWide); @@ -281,7 +300,7 @@ struct ServicePreMessage }; -struct FakeBotAboutTop : public RuntimeComponent { +struct FakeBotAboutTop : RuntimeComponent { void init(); Ui::Text::String text; @@ -289,7 +308,7 @@ struct FakeBotAboutTop : public RuntimeComponent { int height = 0; }; -struct PurchasedTag : public RuntimeComponent { +struct PurchasedTag : RuntimeComponent { Ui::Text::String text; }; @@ -629,14 +648,17 @@ protected: std::unique_ptr _reactions; private: + void recountMonoforumSenderBarInBlocks(); + // This should be called only from previousInBlocksChanged() // to add required bits to the Composer mask // after that always use Has(). void recountDisplayDateInBlocks(); // This should be called only from previousInBlocksChanged() or when - // DateBadge or UnreadBar bit is changed in the Composer mask - // then the result should be cached in a client side flag + // DateBadge or UnreadBar or MonoforumSenderBar bit + // is changed in the Composer mask then the result + // should be cached in a client side flag // HistoryView::Element::Flag::AttachedToPrevious. void recountAttachToPreviousInBlocks(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 9d7c809974..0482f59ec9 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1088,6 +1088,9 @@ int Message::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto monoforumBar = Get()) { + result += monoforumBar->height(); + } if (const auto service = Get()) { result += service->height; } @@ -1146,6 +1149,27 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } + if (const auto monoforumBar = Get()) { + auto barh = monoforumBar->height(); + auto skip = 0; + if (const auto date = Get()) { + skip += date->height(); + } + if (const auto bar = Get()) { + skip += bar->height(); + } + if (context.clip.intersects(QRect(0, skip, width(), barh))) { + p.translate(0, skip); + monoforumBar->paint( + p, + context.st, + 0, + width(), + delegate()->elementIsChatWide()); + p.translate(0, -skip); + } + } + if (const auto service = Get()) { service->paint(p, context, g, delegate()->elementIsChatWide()); } @@ -2458,6 +2482,8 @@ bool Message::hasFromPhoto() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return delegate()->elementIsChatWide(); case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -3685,6 +3711,8 @@ bool Message::hasFromName() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return false; case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -3953,6 +3981,8 @@ bool Message::displayFastShare() const { bool Message::displayGoToOriginal() const { if (isPinnedContext()) { return !hasOutLayout(); + } else if (context() == Context::Monoforum) { + return false; } const auto item = data(); if (const auto forwarded = item->Get()) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index a68c5fa971..efade0c7fa 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -35,8 +35,7 @@ class InlineList; } // namespace Reactions // Special type of Component for the channel actions log. -struct LogEntryOriginal - : public RuntimeComponent { +struct LogEntryOriginal : RuntimeComponent { LogEntryOriginal(); LogEntryOriginal(LogEntryOriginal &&other); LogEntryOriginal &operator=(LogEntryOriginal &&other); @@ -45,13 +44,12 @@ struct LogEntryOriginal std::unique_ptr page; }; -struct Factcheck -: public RuntimeComponent { +struct Factcheck : RuntimeComponent { std::unique_ptr page; bool expanded = false; }; -struct PsaTooltipState : public RuntimeComponent { +struct PsaTooltipState : RuntimeComponent { QString type; mutable ClickHandlerPtr link; mutable Ui::Animations::Simple buttonVisibleAnimation; diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 02a4aed234..9acf977d62 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -452,6 +452,9 @@ QSize Service::performCountCurrentSize(int newWidth) { if (const auto bar = Get()) { newHeight += bar->height(); } + if (const auto monoforumBar = Get()) { + newHeight += monoforumBar->height(); + } data()->resolveDependent(); @@ -525,6 +528,9 @@ int Service::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto monoforumBar = Get()) { + result += monoforumBar->height(); + } return result; } @@ -557,6 +563,27 @@ void Service::draw(Painter &p, const PaintContext &context) const { } } + if (const auto monoforumBar = Get()) { + auto barh = monoforumBar->height(); + auto skip = 0; + if (const auto date = Get()) { + skip += date->height(); + } + if (const auto bar = Get()) { + skip += bar->height(); + } + if (context.clip.intersects(QRect(0, skip, width(), barh))) { + p.translate(0, skip); + monoforumBar->paint( + p, + context.st, + 0, + width(), + delegate()->elementIsChatWide()); + p.translate(0, -skip); + } + } + if (isHidden()) { return; } 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 af060ea552..978b95796b 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -492,6 +492,9 @@ void TopBarWidget::paintTopBar(Painter &p) { ? history->peer.get() : sublist ? sublist->peer().get() : nullptr; + const auto broadcastForMonoforum = history + ? history->peer->monoforumBroadcast() + : nullptr; if (topic && _activeChat.section == Section::Replies) { p.setPen(st::dialogsNameFg); topic->chatListNameText().drawElided( @@ -515,9 +518,12 @@ void TopBarWidget::paintTopBar(Painter &p) { } } else if (folder || (peer && (peer->sharedMediaInfo() || peer->isVerifyCodes())) + || broadcastForMonoforum || (_activeChat.section == Section::Scheduled) || (_activeChat.section == Section::Pinned)) { - auto text = (_activeChat.section == Section::Scheduled) + auto text = broadcastForMonoforum + ? broadcastForMonoforum->name() + u" Messages"_q AssertIsDebug() + : (_activeChat.section == Section::Scheduled) ? ((peer && peer->isSelf()) ? tr::lng_reminder_messages(tr::now) : tr::lng_scheduled_messages(tr::now)) diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index aaf8a82689..e8a1226cfe 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -628,10 +628,13 @@ Cover::Cover( : object_ptr( this, controller, - _peer, + (_peer->monoforumBroadcast() + ? _peer->monoforumBroadcast() + : _peer), Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, - _st.photo)) + _st.photo, + _peer->monoforumBroadcast() != nullptr)) , _changePersonal((role == Role::Info || topic || !_peer->isUser() @@ -647,6 +650,9 @@ Cover::Cover( , _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { _peer->updateFull(); + if (const auto broadcast = _peer->monoforumBroadcast()) { + broadcast->updateFull(); + } _name->setSelectable(true); _name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now)); @@ -979,6 +985,12 @@ void Cover::refreshStatusText() { chat->count, int(chat->participants.size())); return { .text = ChatStatusText(fullCount, onlineCount, true) }; + } else if (auto broadcast = _peer->monoforumBroadcast()) { + auto result = ChatStatusText( + qMax(broadcast->membersCount(), 1), + 0, + false); + return TextWithEntities{ .text = result }; } else if (auto channel = _peer->asChannel()) { const auto onlineCount = _onlineCount.current(); const auto fullCount = qMax(channel->membersCount(), 1); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 42594dd010..aa8b1862fe 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -99,7 +99,9 @@ object_ptr InnerWidget::setupContent( result->add(std::move(actions)); } if (_peer->isChat() || _peer->isMegagroup()) { - setupMembers(result.data()); + if (!_peer->isMonoforum()) { + setupMembers(result.data()); + } } return result; } diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index 489e2666e5..ff0273d1d6 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -181,7 +181,7 @@ private: }; -struct Info : public RuntimeComponent { +struct Info : RuntimeComponent { int top = 0; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0ae4d9ba54..ef30b941de 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -49,6 +49,7 @@ msgReplyBarSize: size(2px, 36px); msgReplyBarSkip: 10px; msgServicePadding: margins(12px, 3px, 12px, 4px); msgServiceMargin: margins(10px, 10px, 10px, 2px); +monoforumBarUserpicSkip: 2px; msgDateSpace: 12px; msgDateDelta: point(2px, 5px); diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 9c4bb61a35..967ad72fa6 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -180,12 +180,14 @@ UserpicButton::UserpicButton( not_null peer, Role role, Source source, - const style::UserpicButton &st) + const style::UserpicButton &st, + bool forceForumShape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(controller) , _window(&controller->window()) , _peer(peer) +, _forceForumShape(forceForumShape) , _role(role) , _source(source) { if (_source == Source::Custom) { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 07abeec1a2..6bd96f330e 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -69,7 +69,8 @@ public: not_null peer, Role role, Source source, - const style::UserpicButton &st); + const style::UserpicButton &st, + bool forceForumShape = false); UserpicButton( QWidget *parent, not_null peer, // Role::Custom, Source::PeerPhoto