diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1aaf90769..f97b8f37e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -170,6 +170,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_pinned_pin" = "Pin"; "lng_pinned_unpin" = "Unpin"; "lng_pinned_notify" = "Notify all members"; +"lng_pinned_messages_title#one" = "{count} pinned message"; +"lng_pinned_messages_title#other" = "{count} pinned messages"; +"lng_pinned_hide_all" = "Don't show pinned messages"; +"lng_pinned_unpin_all#one" = "Unpin {count} message"; +"lng_pinned_unpin_all#other" = "Unpin all {count} messages"; +"lng_pinned_unpin_all_sure" = "Do you want to unpin all messages?"; +"lng_pinned_hide_all_sure" = "Do you want to hide the pinned messages bar? It will stay hidden until a new message is pinned."; +"lng_pinned_hide_all_hide" = "Hide"; "lng_edit_media_album_error" = "This file cannot be saved as a part of an album."; "lng_edit_media_invalid_file" = "Sorry, no way to use this file."; @@ -316,8 +324,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_events_title" = "Events"; "lng_settings_events_joined" = "Contact joined Telegram"; "lng_settings_events_pinned" = "Pinned messages"; -"lng_pinned_messages_title#one" = "{count} pinned message"; -"lng_pinned_messages_title#other" = "{count} pinned messages"; "lng_notification_preview" = "You have a new message"; "lng_notification_reply" = "Reply"; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index b9341eb76..2b817d695 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -435,6 +435,7 @@ QString PeerData::computeUnavailableReason() const { return (first != filtered.end()) ? first->text : QString(); } +// This is duplicated in CanPinMessagesValue(). bool PeerData::canPinMessages() const { if (const auto user = asUser()) { return user->fullFlags() & MTPDuserFull::Flag::f_can_pin_message; @@ -1028,4 +1029,16 @@ MsgId ResolveTopPinnedId(not_null peer) { return slice.messageIds.empty() ? 0 : slice.messageIds.back(); } +std::optional ResolvePinnedCount(not_null peer) { + const auto slice = peer->session().storage().snapshot( + Storage::SharedMediaQuery( + Storage::SharedMediaKey( + peer->id, + Storage::SharedMediaType::Pinned, + 0), + 0, + 0)); + return slice.count; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 4fa630510..2ffb3cbaf 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -444,5 +444,6 @@ std::optional RestrictionError( void SetTopPinnedMessageId(not_null peer, MsgId messageId); [[nodiscard]] MsgId ResolveTopPinnedId(not_null peer); +[[nodiscard]] std::optional ResolvePinnedCount(not_null peer); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 5522d5ff9..4074bd889 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -252,7 +252,71 @@ rpl::producer CanWriteValue(not_null peer) { } else if (auto channel = peer->asChannel()) { return CanWriteValue(channel); } - Unexpected("Bad peer value in CanWriteValue()"); + Unexpected("Bad peer value in CanWriteValue"); +} + +// This is duplicated in PeerData::canPinMessages(). +rpl::producer CanPinMessagesValue(not_null peer) { + using namespace rpl::mappers; + if (const auto user = peer->asUser()) { + return PeerFullFlagsValue( + user, + MTPDuserFull::Flag::f_can_pin_message + ) | rpl::map(_1 != MTPDuserFull::Flag(0)); + } else if (const auto chat = peer->asChat()) { + const auto mask = 0 + | MTPDchat::Flag::f_deactivated + | MTPDchat_ClientFlag::f_forbidden + | MTPDchat::Flag::f_left + | MTPDchat::Flag::f_creator + | MTPDchat::Flag::f_kicked; + return rpl::combine( + PeerFlagsValue(chat, mask), + AdminRightValue(chat, ChatAdminRight::f_pin_messages), + DefaultRestrictionValue(chat, ChatRestriction::f_pin_messages), + []( + MTPDchat::Flags flags, + bool adminRightAllows, + bool defaultRestriction) { + const auto amOutFlags = 0 + | MTPDchat::Flag::f_deactivated + | MTPDchat_ClientFlag::f_forbidden + | MTPDchat::Flag::f_left + | MTPDchat::Flag::f_kicked; + return !(flags & amOutFlags) + && ((flags & MTPDchat::Flag::f_creator) + || adminRightAllows + || !defaultRestriction); + }); + } else if (const auto megagroup = peer->asMegagroup()) { + if (megagroup->amCreator()) { + return rpl::single(true); + } + return rpl::combine( + AdminRightValue(megagroup, ChatAdminRight::f_pin_messages), + DefaultRestrictionValue(megagroup, ChatRestriction::f_pin_messages), + PeerFlagValue(megagroup, MTPDchannel::Flag::f_username), + PeerFullFlagValue(megagroup, MTPDchannelFull::Flag::f_location), + megagroup->restrictionsValue() + ) | rpl::map([=]( + bool adminRightAllows, + bool defaultRestriction, + bool hasUsername, + bool hasLocation, + Data::Flags::Change restrictions) { + return adminRightAllows + || (!hasUsername + && !hasLocation + && !defaultRestriction + && !(restrictions.value & ChatRestriction::f_pin_messages)); + }); + } else if (const auto channel = peer->asChannel()) { + if (channel->amCreator()) { + return rpl::single(true); + } + return AdminRightValue(channel, ChatAdminRight::f_edit_messages); + } + Unexpected("Peer type in CanPinMessagesValue."); } TimeId SortByOnlineValue(not_null user, TimeId now) { diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index 339e031df..c913761f8 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -84,6 +84,7 @@ template < typename = typename PeerType::FullFlags::Change> inline auto PeerFullFlagsValue(PeerType *peer) { Expects(peer != nullptr); + return peer->fullFlagsValue(); } @@ -105,10 +106,11 @@ inline auto PeerFullFlagValue( return SingleFlagValue(PeerFullFlagsValue(peer), flag); } -rpl::producer CanWriteValue(UserData *user); -rpl::producer CanWriteValue(ChatData *chat); -rpl::producer CanWriteValue(ChannelData *channel); -rpl::producer CanWriteValue(not_null peer); +[[nodiscard]] rpl::producer CanWriteValue(UserData *user); +[[nodiscard]] rpl::producer CanWriteValue(ChatData *chat); +[[nodiscard]] rpl::producer CanWriteValue(ChannelData *channel); +[[nodiscard]] rpl::producer CanWriteValue(not_null peer); +[[nodiscard]] rpl::producer CanPinMessagesValue(not_null peer); [[nodiscard]] TimeId SortByOnlineValue(not_null user, TimeId now); [[nodiscard]] crl::time OnlineChangeTimeout(TimeId online, TimeId now); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index de25e5ee2..02b024b62 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5565,15 +5565,15 @@ void HistoryWidget::hidePinnedMessage() { { peerToChannel(_peer->id), id.message }, false); } else { - const auto top = Data::ResolveTopPinnedId(_peer); - if (top) { - session().settings().setHiddenPinnedMessageId(_peer->id, top); - session().saveSettingsDelayed(); - - checkPinnedBarState(); - } else { - session().api().requestFullPeer(_peer); - } + const auto callback = [=] { + if (_pinnedTracker) { + checkPinnedBarState(); + } + }; + Window::HidePinnedBar( + controller(), + _peer, + crl::guard(this, callback)); } } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index c1feda60f..b0a7fd185 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -77,13 +77,17 @@ MsgId ItemIdAcrossData(not_null item) { return session->data().scheduledMessages().lookupId(item); } -bool HasEditMessageAction(const ContextMenuRequest &request) { +bool HasEditMessageAction( + const ContextMenuRequest &request, + not_null list) { const auto item = request.item; + const auto context = list->elementContext(); if (!item || item->isSending() || item->hasFailed() || item->isEditingMedia() - || !request.selectedItems.empty()) { + || !request.selectedItems.empty() + || (context != Context::History && context != Context::Replies)) { return false; } const auto peer = item->history()->peer; @@ -441,8 +445,10 @@ bool AddSendNowMessageAction( bool AddRescheduleMessageAction( not_null menu, - const ContextMenuRequest &request) { - if (!HasEditMessageAction(request) || !request.item->isScheduled()) { + const ContextMenuRequest &request, + not_null list) { + if (!HasEditMessageAction(request, list) + || !request.item->isScheduled()) { return false; } const auto owner = &request.item->history()->owner(); @@ -551,7 +557,7 @@ bool AddEditMessageAction( not_null menu, const ContextMenuRequest &request, not_null list) { - if (!HasEditMessageAction(request)) { + if (!HasEditMessageAction(request, list)) { return false; } const auto item = request.item; @@ -591,6 +597,29 @@ bool AddPinMessageAction( return true; } +bool AddGoToMessageAction( + not_null menu, + const ContextMenuRequest &request, + not_null list) { + const auto context = list->elementContext(); + const auto view = request.view; + if (!view + || !IsServerMsgId(view->data()->id) + || context != Context::Pinned + || !view->hasOutLayout()) { + return false; + } + const auto itemId = view->data()->fullId(); + const auto controller = list->controller(); + menu->addAction(tr::lng_context_to_msg(tr::now), crl::guard(controller, [=] { + const auto item = controller->session().data().message(itemId); + if (item) { + goToMessageClickHandler(item)->onClick(ClickContext{}); + } + })); + return true; +} + void AddSendNowAction( not_null menu, const ContextMenuRequest &request, @@ -774,6 +803,7 @@ void AddTopMessageActions( const ContextMenuRequest &request, not_null list) { AddReplyToMessageAction(menu, request, list); + AddGoToMessageAction(menu, request, list); AddViewRepliesAction(menu, request, list); AddEditMessageAction(menu, request, list); AddPinMessageAction(menu, request, list); @@ -789,7 +819,7 @@ void AddMessageActions( AddDeleteAction(menu, request, list); AddReportAction(menu, request, list); AddSelectionAction(menu, request, list); - AddRescheduleMessageAction(menu, request); + AddRescheduleMessageAction(menu, request, list); } void AddCopyLinkAction( diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index a53544ce8..4a6d38f81 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1316,6 +1316,7 @@ void ListWidget::updateItemsGeometry() { view->setDisplayDate(false); } else { view->setDisplayDate(true); + view->setAttachToPrevious(false); return i; } } @@ -2611,6 +2612,9 @@ void ListWidget::refreshAttachmentsFromTill(int from, int till) { view = next; } } + if (till == int(_items.size())) { + _items.back()->setAttachToNext(false); + } updateSize(); } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 767e1c58f..2e1355bc4 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "history/history_item.h" #include "boxes/confirm_box.h" +#include "data/data_peer_values.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/layers/generic_box.h" @@ -93,6 +94,10 @@ PinnedWidget::PinnedWidget( , _topBar(this, controller) , _topBarShadow(this) , _scroll(std::make_unique(this, st::historyScroll, false)) +, _clearButton(std::make_unique( + this, + QString(), + st::historyComposeButton)) , _scrollDown(_scroll.get(), st::historyToDown) { _topBar->setActiveChat( _history, @@ -129,6 +134,7 @@ PinnedWidget::PinnedWidget( _scroll->show(); connect(_scroll.get(), &Ui::ScrollArea::scrolled, [=] { onScroll(); }); + setupClearButton(); setupScrollDownButton(); } @@ -149,6 +155,28 @@ void PinnedWidget::setupScrollDownButton() { updateScrollDownVisibility(); } +void PinnedWidget::setupClearButton() { + Data::CanPinMessagesValue( + _history->peer + ) | rpl::start_with_next([=] { + refreshClearButtonText(); + }, _clearButton->lifetime()); + + _clearButton->setClickedCallback([=] { + if (!_history->peer->canPinMessages()) { + const auto callback = [=] { + controller()->showBackFromStack(); + }; + Window::HidePinnedBar( + controller(), + _history->peer, + crl::guard(this, callback)); + } else { + Window::UnpinAllMessages(controller(), _history); + } + }); +} + void PinnedWidget::scrollDownClicked() { if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) { showAtEnd(); @@ -357,6 +385,26 @@ void PinnedWidget::recountChatWidth() { } } +void PinnedWidget::setMessagesCount(int count) { + if (_messagesCount == count) { + return; + } + _messagesCount = count; + _topBar->setCustomTitle( + tr::lng_pinned_messages_title(tr::now, lt_count, count)); + refreshClearButtonText(); +} + +void PinnedWidget::refreshClearButtonText() { + const auto can = _history->peer->canPinMessages(); + _clearButton->setText(can + ? tr::lng_pinned_unpin_all( + tr::now, + lt_count, + std::max(_messagesCount, 1)).toUpper() + : tr::lng_pinned_hide_all(tr::now).toUpper()); +} + void PinnedWidget::updateControlsGeometry() { const auto contentWidth = width(); @@ -366,7 +414,9 @@ void PinnedWidget::updateControlsGeometry() { _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); - const auto bottom = height(); + const auto bottom = height() - _clearButton->height(); + _clearButton->resizeToWidth(width()); + _clearButton->move(0, bottom); const auto controlsHeight = 0; const auto scrollY = _topBar->height(); const auto scrollHeight = bottom - scrollY - controlsHeight; @@ -479,8 +529,7 @@ rpl::producer PinnedWidget::listSource( if (!count.has_value()) { return true; } else if (*count != 0) { - _topBar->setCustomTitle( - tr::lng_pinned_messages_title(tr::now, lt_count, *count)); + setMessagesCount(*count); return true; } else { controller()->showBackFromStack(); diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index a3d5dd56a..d6984d44f 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -120,6 +120,7 @@ private: HistoryItem *originItem, anim::type animated = anim::type::normal); + void setupClearButton(); void setupScrollDownButton(); void scrollDownClicked(); void scrollDownAnimationFinish(); @@ -131,6 +132,9 @@ private: void clearSelected(); void recountChatWidth(); + void setMessagesCount(int count); + void refreshClearButtonText(); + const not_null _history; QPointer _inner; object_ptr _topBar; @@ -138,13 +142,14 @@ private: bool _skipScrollEvent = false; std::unique_ptr _scroll; - object_ptr _clearButton = { nullptr }; + std::unique_ptr _clearButton; Ui::Animations::Simple _scrollDownShown; bool _scrollDownIsShown = false; object_ptr _scrollDown; Data::MessagesSlice _lastSlice; + int _messagesCount = -1; }; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 6bf313f7a..336188181 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1150,6 +1150,53 @@ void ToggleMessagePinned( } } +void HidePinnedBar( + not_null navigation, + not_null peer, + Fn onHidden) { + Ui::show(Box(tr::lng_pinned_hide_all_sure(tr::now), tr::lng_pinned_hide_all_hide(tr::now), crl::guard(navigation, [=] { + Ui::hideLayer(); + auto &session = peer->session(); + const auto top = Data::ResolveTopPinnedId(peer); + if (top) { + session.settings().setHiddenPinnedMessageId(peer->id, top); + session.saveSettingsDelayed(); + if (onHidden) { + onHidden(); + } + } else { + session.api().requestFullPeer(peer); + } + }))); +} + +void UnpinAllMessages( + not_null navigation, + not_null history) { + Ui::show(Box(tr::lng_pinned_unpin_all_sure(tr::now), tr::lng_pinned_unpin(tr::now), crl::guard(navigation, [=] { + Ui::hideLayer(); + const auto api = &history->session().api(); + const auto peer = history->peer; + const auto sendRequest = [=](auto self) -> void { + api->request(MTPmessages_UnpinAllMessages( + peer->input + )).done([=](const MTPmessages_AffectedHistory &result) { + const auto offset = api->applyAffectedHistory(peer, result); + if (offset > 0) { + self(self); + } else { + peer->session().storage().remove( + Storage::SharedMediaRemoveAll( + peer->id, + Storage::SharedMediaType::Pinned)); + peer->setHasPinnedMessages(false); + } + }).send(); + }; + sendRequest(sendRequest); + }))); +} + void PeerMenuAddMuteAction( not_null peer, const PeerMenuCallback &addAction) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 7bbfc7c8e..e733211a4 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -115,5 +115,12 @@ void ToggleMessagePinned( not_null navigation, FullMsgId itemId, bool pin); +void HidePinnedBar( + not_null navigation, + not_null peer, + Fn onHidden); +void UnpinAllMessages( + not_null navigation, + not_null history); } // namespace Window