From d116c8fea06eda1382b93c878b522d7c08734b81 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Jan 2024 13:38:38 +0400 Subject: [PATCH] Allow editing tag names in Saved Messages. --- Telegram/Resources/langs/lang.strings | 5 + .../chat_helpers/chat_helpers.style | 19 +++ .../data/data_message_reactions.cpp | 48 +++++++- .../SourceFiles/data/data_message_reactions.h | 4 + Telegram/SourceFiles/data/data_session.cpp | 32 +++++ Telegram/SourceFiles/data/data_session.h | 13 +- .../dialogs/dialogs_inner_widget.cpp | 6 +- .../dialogs/dialogs_search_tags.cpp | 7 +- .../view/history_view_context_menu.cpp | 116 ++++++++++++++++++ .../history/view/history_view_message.cpp | 27 +++- .../history/view/history_view_message.h | 1 + .../view/reactions/history_view_reactions.cpp | 24 ++-- .../view/reactions/history_view_reactions.h | 2 + 13 files changed, 289 insertions(+), 15 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4cd748139..b980101e3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2801,6 +2801,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_seen_reacted_all" = "Show All Reactions"; "lng_context_set_as_quick" = "Set as Quick"; "lng_context_filter_by_tag" = "Filter by Tag"; +"lng_context_tag_add_name" = "Add Name"; +"lng_context_tag_edit_name" = "Edit Name"; "lng_context_remove_tag" = "Remove Tag"; "lng_context_delete_from_disk" = "Delete from disk"; "lng_context_delete_all_files" = "Delete all files"; @@ -2810,6 +2812,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_read_hidden" = "read"; "lng_context_read_show" = "show when"; +"lng_edit_tag_about" = "You can label your emoji tag with a text name."; +"lng_edit_tag_name" = "Name"; + "lng_context_animated_emoji" = "This message contains emoji from **{name} pack**."; "lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**."; "lng_context_animated_emoji_many#other" = "This message contains emoji from **{count} packs**."; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 1cd58f181..c79dc5d46 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1307,3 +1307,22 @@ ttlMediaButton: RoundButton(defaultActiveButton) { textTop: 6px; } ttlMediaButtonBottomSkip: 14px; + +editTagAbout: FlatLabel(defaultFlatLabel) { + minWidth: 256px; +} +editTagField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(24px, 10px, 32px, 0px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + + heightMin: 32px; +} +editTagLimit: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 4c96025a5..d2107335c 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -356,6 +356,15 @@ const std::vector &Reactions::myTagsInfo() const { return _myTagsInfo; } +const QString &Reactions::myTagTitle(const ReactionId &id) const { + const auto i = ranges::find(_myTagsInfo, id, &::Data::MyTagInfo::id); + if (i != end(_myTagsInfo)) { + return i->title; + } + static const auto kEmpty = QString(); + return kEmpty; +} + ReactionId Reactions::favoriteId() const { return _favoriteId; } @@ -415,6 +424,23 @@ void Reactions::decrementMyTag(const ReactionId &id) { scheduleMyTagsUpdate(); } +void Reactions::renameTag(const ReactionId &id, const QString &name) { + auto i = ranges::find(_myTagsInfo, id, &MyTagInfo::id); + if (i == end(_myTagsInfo) || i->title == name) { + return; + } + i->title = name; + scheduleMyTagsUpdate(); + _myTagRenamed.fire_copy(id); + + using Flag = MTPmessages_UpdateSavedReactionTag::Flag; + _owner->session().api().request(MTPmessages_UpdateSavedReactionTag( + MTP_flags(name.isEmpty() ? Flag(0) : Flag::f_title), + ReactionToMTP(id), + MTP_string(name) + )).send(); +} + void Reactions::scheduleMyTagsUpdate() { _myTagsUpdateScheduled = true; crl::on_main(&session(), [=] { @@ -496,6 +522,10 @@ rpl::producer<> Reactions::tagsUpdates() const { return _tagsUpdated.events(); } +rpl::producer Reactions::myTagRenamed() const { + return _myTagRenamed.events(); +} + void Reactions::preloadImageFor(const ReactionId &id) { if (_images.contains(id) || id.emoji().isEmpty()) { return; @@ -850,9 +880,21 @@ void Reactions::updateGeneric(const MTPDmessages_stickerSet &data) { void Reactions::updateMyTags(const MTPDmessages_savedReactionTags &data) { _myTagsHash = data.vhash().v; - _myTagsInfo = ListFromMTP(data); + auto list = ListFromMTP(data); + auto renamed = base::flat_set(); + for (const auto &info : list) { + const auto j = ranges::find(_myTagsInfo, info.id, &MyTagInfo::id); + const auto was = (j != end(_myTagsInfo)) ? j->title : QString(); + if (info.title != was) { + renamed.emplace(info.id); + } + } + _myTagsInfo = std::move(list); _myTags = resolveByInfos(_myTagsInfo, _unresolvedMyTags); _myTagsUpdated.fire({}); + for (const auto &id : renamed) { + _myTagRenamed.fire_copy(id); + } } void Reactions::updateTags(const MTPDmessages_reactions &data) { @@ -1304,6 +1346,7 @@ void MessageReactions::remove(const ReactionId &id) { return; } i->my = false; + const auto tags = _item->reactionsAreTags(); const auto removed = !--i->count; if (removed) { _list.erase(i); @@ -1321,6 +1364,9 @@ void MessageReactions::remove(const ReactionId &id) { } } } + if (tags) { + history->owner().reactions().decrementMyTag(id); + } auto &owner = history->owner(); owner.reactions().send(_item, false); owner.notifyItemDataChange(_item); diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 1475df7b4..db423bc3e 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -93,11 +93,13 @@ public: }; [[nodiscard]] const std::vector &list(Type type) const; [[nodiscard]] const std::vector &myTagsInfo() const; + [[nodiscard]] const QString &myTagTitle(const ReactionId &id) const; [[nodiscard]] ReactionId favoriteId() const; [[nodiscard]] const Reaction *favorite() const; void setFavorite(const ReactionId &id); void incrementMyTag(const ReactionId &id); void decrementMyTag(const ReactionId &id); + void renameTag(const ReactionId &id, const QString &name); [[nodiscard]] DocumentData *chooseGenericAnimation( not_null custom) const; @@ -107,6 +109,7 @@ public: [[nodiscard]] rpl::producer<> favoriteUpdates() const; [[nodiscard]] rpl::producer<> myTagsUpdates() const; [[nodiscard]] rpl::producer<> tagsUpdates() const; + [[nodiscard]] rpl::producer myTagRenamed() const; enum class ImageSize { BottomInfo, @@ -223,6 +226,7 @@ private: rpl::event_stream<> _favoriteUpdated; rpl::event_stream<> _myTagsUpdated; rpl::event_stream<> _tagsUpdated; + rpl::event_stream _myTagRenamed; // We need &i->second stay valid while inserting new items. // So we use std::map instead of base::flat_map here. diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3592709c9..718421d96 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -295,6 +295,16 @@ Session::Session(not_null session) } }, _lifetime); + _reactions->myTagRenamed( + ) | rpl::start_with_next([=](const ReactionId &id) { + const auto i = _viewsByTag.find(id); + if (i != end(_viewsByTag)) { + for (const auto &view : i->second) { + notifyItemDataChange(view->data()); + } + } + }, _lifetime); + Spellchecker::HighlightReady( ) | rpl::start_with_next([=](uint64 processId) { highlightProcessDone(processId); @@ -4608,6 +4618,28 @@ rpl::producer> Session::peerDecorationsUpdated() const { return _peerDecorationsUpdated.events(); } +void Session::viewTagsChanged( + not_null view, + std::vector &&was, + std::vector &&now) { + for (const auto &id : now) { + const auto i = ranges::remove(was, id); + if (i != end(was)) { + was.erase(i, end(was)); + } else { + _viewsByTag[id].emplace(view); + } + } + for (const auto &id : was) { + const auto i = _viewsByTag.find(id); + if (i != end(_viewsByTag) + && i->second.remove(view) + && i->second.empty()) { + _viewsByTag.erase(i); + } + } +} + void Session::clearLocalStorage() { _cache->close(); _cache->clear(); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 244fa1524..0a779a4b8 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -62,6 +62,7 @@ class NotifySettings; class CustomEmojiManager; class Stories; class SavedMessages; +struct ReactionId; struct RepliesReadTillUpdate { FullMsgId id; @@ -742,6 +743,11 @@ public: void applyStatsDcId(not_null, MTP::DcId); [[nodiscard]] MTP::DcId statsDcId(not_null); + void viewTagsChanged( + not_null view, + std::vector &&was, + std::vector &&now); + void clearLocalStorage(); private: @@ -1005,9 +1011,14 @@ private: base::flat_map> _groupCalls; rpl::event_stream _invitesToCalls; - base::flat_map>> _invitedToCallUsers; + base::flat_map< + uint64, + base::flat_set>> _invitedToCallUsers; base::flat_set> _shownSpoilers; + base::flat_map< + ReactionId, + base::flat_set>> _viewsByTag; History *_topPromoted = nullptr; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 14c9552bb..484569224 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1430,8 +1430,10 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { }); } else if (_pressed) { auto row = _pressed; - const auto updateCallback = [this, row] { - if (!_pinnedShiftAnimation.animating()) { + const auto weak = Ui::MakeWeak(this); + const auto updateCallback = [weak, row] { + const auto strong = weak.data(); + if (!strong || !strong->_pinnedShiftAnimation.animating()) { row->entry()->updateChatListEntry(); } }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index 7bf857ba7..ece874547 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -110,7 +110,11 @@ void SearchTags::fill(const std::vector &list) { } }; for (const auto &reaction : list) { - push(reaction.id, ComposeText(reaction)); + if (reaction.count > 0 + || ranges::contains(_added, reaction.id) + || ranges::contains(selected, reaction.id)) { + push(reaction.id, ComposeText(reaction)); + } } for (const auto &reaction : _added) { if (!ranges::contains(_tags, reaction, &Tag::id)) { @@ -119,6 +123,7 @@ void SearchTags::fill(const std::vector &list) { } if (_width > 0) { layout(); + _repaintRequests.fire({}); } } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 52170fa51..181c60bfd 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -41,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_send.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/show_or_premium_box.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/power_saving.h" #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" #include "boxes/sticker_set_box.h" @@ -85,6 +87,7 @@ namespace HistoryView { namespace { constexpr auto kRescheduleLimit = 20; +constexpr auto kTagNameLimit = 12; bool HasEditMessageAction( const ContextMenuRequest &request, @@ -980,6 +983,112 @@ void AddCopyLinkAction( &st::menuIconCopy); } +void EditTagBox( + not_null box, + not_null controller, + const Data::ReactionId &id) { + const auto owner = &controller->session().data(); + const auto title = owner->reactions().myTagTitle(id); + box->setTitle(title.isEmpty() + ? tr::lng_context_tag_add_name() + : tr::lng_context_tag_edit_name()); + box->addRow(object_ptr( + box, + tr::lng_edit_tag_about(), + st::editTagAbout)); + const auto field = box->addRow(object_ptr( + box, + st::editTagField, + tr::lng_edit_tag_name(), + title)); + field->setMaxLength(kTagNameLimit * 2); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + struct State { + std::unique_ptr custom; + QImage image; + rpl::variable length; + }; + const auto state = field->lifetime().make_state(); + state->length = rpl::single( + int(title.size()) + ) | rpl::then(field->changes() | rpl::map([=] { + return int(field->getLastText().size()); + })); + + if (const auto customId = id.custom()) { + state->custom = owner->customEmojiManager().create( + customId, + [=] { field->update(); }); + } else { + owner->reactions().preloadImageFor(id); + } + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + const auto top = st::editTagField.textMargins.top(); + if (const auto custom = state->custom.get()) { + const auto inactive = !field->window()->isActiveWindow(); + custom->paint(p, { + .textColor = st::windowFg->c, + .now = crl::now(), + .position = QPoint(0, top), + .paused = inactive || On(PowerSaving::kEmojiChat), + }); + } else { + if (state->image.isNull()) { + state->image = owner->reactions().resolveImageFor( + id, + ::Data::Reactions::ImageSize::InlineList); + } + if (!state->image.isNull()) { + const auto size = st::reactionInlineSize; + const auto skip = (size - st::reactionInlineImage) / 2; + p.drawImage(skip, top + skip, state->image); + } + } + }, field->lifetime()); + const auto warning = Ui::CreateChild( + field, + state->length.value() | rpl::map([](int count) { + return (count > kTagNameLimit / 2) + ? QString::number(kTagNameLimit - count) + : QString(); + }), + st::editTagLimit); + state->length.value() | rpl::map( + rpl::mappers::_1 > kTagNameLimit + ) | rpl::start_with_next([=](bool exceeded) { + warning->setTextColorOverride(exceeded + ? st::attentionButtonFg->c + : std::optional()); + }, warning->lifetime()); + rpl::combine( + field->sizeValue(), + warning->sizeValue() + ) | rpl::start_with_next([=] { + warning->moveToRight(0, st::editTagField.textMargins.top()); + }, warning->lifetime()); + warning->setAttribute(Qt::WA_TransparentForMouseEvents); + + box->addButton(tr::lng_settings_save(), [=] { + const auto text = field->getLastText(); + if (text.size() > kTagNameLimit) { + field->showError(); + return; + } + const auto weak = Ui::MakeWeak(box); + controller->session().data().reactions().renameTag(id, text); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + } // namespace ContextMenuRequest::ContextMenuRequest( @@ -1342,6 +1451,13 @@ void ShowTagMenu( }); }, &st::menuIconFave); + const auto editLabel = owner->reactions().myTagTitle(id).isEmpty() + ? tr::lng_context_tag_add_name(tr::now) + : tr::lng_context_tag_edit_name(tr::now); + (*menu)->addAction(editLabel, [=] { + controller->show(Box(EditTagBox, controller, id)); + }, &st::menuIconEdit); + const auto removeTag = [=] { if (const auto item = owner->message(itemId)) { const auto &list = item->reactions(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 5234b53b3..4a974d619 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -435,6 +435,21 @@ Message::~Message() { _fromNameStatus = nullptr; checkHeavyPart(); } + setReactions(nullptr); +} + +void Message::setReactions(std::unique_ptr list) { + auto was = _reactions + ? _reactions->computeTagsList() + : std::vector(); + _reactions = std::move(list); + auto now = _reactions + ? _reactions->computeTagsList() + : std::vector(); + if (!was.empty() || !now.empty()) { + auto &owner = history()->owner(); + owner.viewTagsChanged(this, std::move(was), std::move(now)); + } } void Message::refreshRightBadge() { @@ -2958,7 +2973,7 @@ void Message::refreshReactions() { const auto item = data(); const auto &list = item->reactions(); if (list.empty() || embedReactionsInBottomInfo()) { - _reactions = nullptr; + setReactions(nullptr); return; } using namespace Reactions; @@ -2988,13 +3003,19 @@ void Message::refreshReactions() { } }); }; - _reactions = std::make_unique( + setReactions(std::make_unique( &item->history()->owner().reactions(), handlerFactory, [=] { customEmojiRepaint(); }, - std::move(reactionsData)); + std::move(reactionsData))); } else { + auto was = _reactions->computeTagsList(); _reactions->update(std::move(reactionsData), width()); + auto now = _reactions->computeTagsList(); + if (!was.empty() || !now.empty()) { + auto &owner = history()->owner(); + owner.viewTagsChanged(this, std::move(was), std::move(now)); + } } } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index e849d5f63..1f13056e8 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -289,6 +289,7 @@ private: [[nodiscard]] ClickHandlerPtr psaTooltipLink() const; void psaTooltipToggled(bool shown) const; + void setReactions(std::unique_ptr list); void refreshRightBadge(); void refreshReactions(); void validateFromNameText(PeerData *from) const; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index ab4c23c2e..17e83e554 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -88,6 +88,19 @@ void InlineList::removeSkipBlock() { _skipBlock = {}; } +bool InlineList::areTags() const { + return _data.flags & Data::Flag::Tags; +} + +std::vector InlineList::computeTagsList() const { + if (!areTags()) { + return {}; + } + return _buttons | ranges::views::transform( + &Button::id + ) | ranges::to_vector; +} + bool InlineList::hasCustomEmoji() const { return _hasCustomEmoji; } @@ -119,7 +132,7 @@ void InlineList::layoutButtons() { ) | ranges::views::transform([](const MessageReaction &reaction) { return not_null{ &reaction }; }) | ranges::to_vector; - const auto tags = _data.flags & Data::Flag::Tags; + const auto tags = areTags(); const auto &infos = _owner->myTagsInfo(); if (!tags) { const auto &list = _owner->list(::Data::Reactions::Type::All); @@ -148,10 +161,7 @@ void InlineList::layoutButtons() { ? std::move(*i) : prepareButtonWithId(id)); if (tags) { - const auto i = ranges::find(infos, id, &::Data::MyTagInfo::id); - setButtonTag( - buttons.back(), - (i == end(infos)) ? QString() : i->title); + setButtonTag(buttons.back(), _owner->myTagTitle(id)); } else if (const auto j = _data.recent.find(id) ; j != end(_data.recent) && !j->second.empty()) { setButtonUserpics(buttons.back(), j->second); @@ -367,7 +377,7 @@ void InlineList::paint( const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; const auto skip = (size - st::reactionInlineImage) / 2; - const auto tags = (_data.flags & Data::Flag::Tags); + const auto tags = areTags(); const auto inbubble = (_data.flags & Data::Flag::InBubble); const auto flipped = (_data.flags & Data::Flag::Flipped); p.setFont(tags ? st::reactionInlineTagFont : st::semiboldFont); @@ -613,7 +623,7 @@ void InlineList::paintSingleBg( const QColor &color, float64 opacity) const { p.setOpacity(opacity); - if (!(_data.flags & Data::Flag::Tags)) { + if (!areTags()) { const auto radius = fill.height() / 2.; p.setBrush(color); p.drawRoundedRect(fill, radius, radius); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index f7b01e3f1..be7aa4e64 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -70,6 +70,8 @@ public: void updateSkipBlock(int width, int height); void removeSkipBlock(); + [[nodiscard]] bool areTags() const; + [[nodiscard]] std::vector computeTagsList() const; [[nodiscard]] bool hasCustomEmoji() const; void unloadCustomEmoji();