diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 57db15d92a..ab4b5f30b9 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1560,6 +1560,8 @@ void Updates::feedUpdate(const MTPUpdate &update) { if (const auto local = owner.message(id)) { if (local->isScheduled()) { session().data().scheduledMessages().apply(d, local); + } else if (local->isBusinessShortcut()) { + session().data().shortcutMessages().apply(d, local); } else { const auto existing = session().data().message( id.peer, diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 026e6a99e9..8b59309bff 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -128,6 +128,94 @@ void ShortcutMessages::clearOldRequests() { } } +void ShortcutMessages::updateShortcuts(const QVector &list) { + auto shortcuts = parseShortcuts(list); + auto changes = std::vector(); + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcuts.list.contains(id)) { + continue; + } + auto foundId = BusinessShortcutId(); + for (auto &[realId, real] : shortcuts.list) { + if (real.name == shortcut.name) { + foundId = realId; + break; + } + } + if (foundId) { + mergeMessagesFromTo(id, foundId); + changes.push_back({ .oldId = id, .newId = foundId }); + } else { + shortcuts.list.emplace(id, shortcut); + } + } + const auto changed = !_shortcutsLoaded + || (shortcuts != _shortcuts); + if (changed) { + _shortcuts = std::move(shortcuts); + _shortcutsLoaded = true; + for (const auto &change : changes) { + _shortcutIdChanges.fire_copy(change); + } + _shortcutsChanged.fire({}); + } else { + Assert(changes.empty()); + } +} + +void ShortcutMessages::mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId) { + auto &to = _data[toId]; + const auto i = _data.find(fromId); + if (i == end(_data)) { + return; + } + + auto &from = i->second; + auto destroy = base::flat_set>(); + for (auto &item : from.items) { + if (item->isSending() || item->hasFailed()) { + item->setRealShortcutId(toId); + to.items.push_back(std::move(item)); + } else { + destroy.emplace(item.get()); + } + } + for (const auto &item : destroy) { + item->destroy(); + } + _data.remove(fromId); + + cancelRequest(fromId); + + _updates.fire_copy(toId); + if (!destroy.empty()) { + cancelRequest(toId); + request(toId); + } +} + +Shortcuts ShortcutMessages::parseShortcuts( + const QVector &list) const { + auto result = Shortcuts(); + for (const auto &reply : list) { + const auto shortcut = parseShortcut(reply); + result.list.emplace(shortcut.id, shortcut); + } + return result; +} + +Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const { + const auto &data = reply.data(); + return Shortcut{ + .id = BusinessShortcutId(data.vshortcut_id().v), + .count = data.vcount().v, + .name = qs(data.vshortcut()), + .topMessageId = localMessageId(data.vtop_message().v), + }; +} + MsgId ShortcutMessages::localMessageId(MsgId remoteId) const { return RemoteToLocalMsgId(remoteId); } @@ -146,11 +234,60 @@ int ShortcutMessages::count(BusinessShortcutId shortcutId) const { } void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) { + updateShortcuts(update.vquick_replies().v); + scheduleShortcutsReload(); +} +void ShortcutMessages::scheduleShortcutsReload() { + const auto hasUnknownMessages = [&] { + const auto selfId = _session->userPeerId(); + for (const auto &[id, shortcut] : _shortcuts.list) { + if (!_session->data().message({ selfId, shortcut.topMessageId })) { + return true; + } + } + return false; + }; + if (hasUnknownMessages()) { + _shortcutsLoaded = false; + const auto cancelledId = base::take(_shortcutsRequestId); + _session->api().request(cancelledId).cancel(); + crl::on_main(_session, [=] { + if (cancelledId || hasUnknownMessages()) { + preloadShortcuts(); + } + }); + } } void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) { - + const auto selfId = _session->userPeerId(); + const auto &reply = update.vquick_reply(); + auto foundId = BusinessShortcutId(); + const auto shortcut = parseShortcut(reply); + for (auto &[id, existing] : _shortcuts.list) { + if (id == shortcut.id) { + foundId = id; + break; + } else if (existing.name == shortcut.name) { + foundId = id; + break; + } + } + if (foundId == shortcut.id) { + auto &already = _shortcuts.list[shortcut.id]; + if (already != shortcut) { + already = shortcut; + _shortcutsChanged.fire({}); + } + return; + } else if (foundId) { + _shortcuts.list.emplace(shortcut.id, shortcut); + mergeMessagesFromTo(foundId, shortcut.id); + _shortcuts.list.remove(foundId); + _shortcutIdChanges.fire({ foundId, shortcut.id }); + _shortcutsChanged.fire({}); + } } void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { @@ -159,10 +296,30 @@ void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { if (!shortcutId) { return; } + const auto loaded = _data.contains(shortcutId); auto &list = _data[shortcutId]; append(shortcutId, list, message); sort(list); _updates.fire_copy(shortcutId); + updateCount(shortcutId); + if (!loaded) { + request(shortcutId); + } +} + +void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + const auto j = _shortcuts.list.find(shortcutId); + if (j == end(_shortcuts.list)) { + return; + } + const auto count = (i != end(_data)) + ? int(i->second.itemById.size()) + : 0; + if (j->second.count != count) { + _shortcuts.list[shortcutId].count = count; + _shortcutsChanged.fire({}); + } } void ShortcutMessages::apply( @@ -187,6 +344,10 @@ void ShortcutMessages::apply( } } _updates.fire_copy(shortcutId); + updateCount(shortcutId); + + cancelRequest(shortcutId); + request(shortcutId); } void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { @@ -195,12 +356,17 @@ void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { return; } auto i = _data.find(shortcutId); - while (i != end(_data)) { - Assert(!i->second.itemById.empty()); + while (i != end(_data) && !i->second.itemById.empty()) { i->second.itemById.back().second->destroy(); i = _data.find(shortcutId); } _updates.fire_copy(shortcutId); + if (_data.contains(shortcutId)) { + updateCount(shortcutId); + } else { + _shortcuts.list.remove(shortcutId); + _shortcutIdChanges.fire({ shortcutId, 0 }); + } } void ShortcutMessages::apply( @@ -283,30 +449,7 @@ void ShortcutMessages::preloadShortcuts() { owner->processMessages( data.vmessages(), NewMessageType::Existing); - auto shortcuts = Shortcuts(); - const auto messages = &owner->shortcutMessages(); - for (const auto &reply : data.vquick_replies().v) { - const auto &data = reply.data(); - const auto id = BusinessShortcutId(data.vshortcut_id().v); - shortcuts.list.emplace(id, Shortcut{ - .name = qs(data.vshortcut()), - .topMessageId = messages->localMessageId( - data.vtop_message().v), - .count = data.vcount().v, - }); - } - for (auto &[id, shortcut] : _shortcuts.list) { - if (id < 0) { - shortcuts.list.emplace(id, shortcut); - } - } - const auto changed = !_shortcutsLoaded - || (shortcuts != _shortcuts); - if (changed) { - _shortcuts = std::move(shortcuts); - _shortcutsLoaded = true; - _shortcutsChanged.fire({}); - } + updateShortcuts(data.vquick_replies().v); }, [&](const MTPDmessages_quickRepliesNotModified &) { if (!_shortcutsLoaded) { _shortcutsLoaded = true; @@ -328,6 +471,11 @@ rpl::producer<> ShortcutMessages::shortcutsChanged() const { return _shortcutsChanged.events(); } +auto ShortcutMessages::shortcutIdChanged() const +-> rpl::producer { + return _shortcutIdChanges.events(); +} + BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { Expects(_shortcutsLoaded); @@ -337,7 +485,7 @@ BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { } } const auto result = --_localShortcutId; - _shortcuts.list.emplace(result, Shortcut{ name }); + _shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name }); return result; } @@ -348,6 +496,14 @@ Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { return i->second; } +void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) { + const auto j = _requests.find(shortcutId); + if (j != end(_requests)) { + _session->api().request(j->second.requestId).cancel(); + _requests.erase(j); + } +} + void ShortcutMessages::request(BusinessShortcutId shortcutId) { auto &request = _requests[shortcutId]; if (request.requestId || TooEarlyForRequest(request.lastReceived)) { @@ -512,6 +668,7 @@ void ShortcutMessages::remove(not_null item) { _data.erase(i); } _updates.fire_copy(shortcutId); + updateCount(shortcutId); } uint64 ShortcutMessages::countListHash(const List &list) const { @@ -537,11 +694,10 @@ uint64 ShortcutMessages::countListHash(const List &list) const { MTPInputQuickReplyShortcut ShortcutIdToMTP( not_null session, BusinessShortcutId id) { - if (id >= 0) { - return MTP_inputQuickReplyShortcutId(MTP_int(id)); - } - return MTP_inputQuickReplyShortcut(MTP_string( - session->data().shortcutMessages().lookupShortcut(id).name)); + return id + ? MTP_inputQuickReplyShortcut(MTP_string( + session->data().shortcutMessages().lookupShortcut(id).name)) + : MTPInputQuickReplyShortcut(); } } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h index 57997e7e0a..6b7343b578 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.h +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -22,15 +22,21 @@ class Session; struct MessagesSlice; struct Shortcut { + BusinessShortcutId id = 0; + int count = 0; QString name; MsgId topMessageId = 0; - int count = 0; friend inline bool operator==( const Shortcut &a, const Shortcut &b) = default; }; +struct ShortcutIdChange { + BusinessShortcutId oldId = 0; + BusinessShortcutId newId = 0; +}; + struct Shortcuts { base::flat_map list; @@ -69,6 +75,7 @@ public: [[nodiscard]] const Shortcuts &shortcuts() const; [[nodiscard]] bool shortcutsLoaded() const; [[nodiscard]] rpl::producer<> shortcutsChanged() const; + [[nodiscard]] rpl::producer shortcutIdChanged() const; [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; @@ -100,6 +107,17 @@ private: void remove(not_null item); [[nodiscard]] uint64 countListHash(const List &list) const; void clearOldRequests(); + void cancelRequest(BusinessShortcutId shortcutId); + void updateCount(BusinessShortcutId shortcutId); + + void scheduleShortcutsReload(); + void mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId); + void updateShortcuts(const QVector &list); + [[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const; + [[nodiscard]] Shortcuts parseShortcuts( + const QVector &list) const; const not_null _session; const not_null _history; @@ -111,6 +129,7 @@ private: Shortcuts _shortcuts; rpl::event_stream<> _shortcutsChanged; + rpl::event_stream _shortcutIdChanges; BusinessShortcutId _localShortcutId = 0; uint64 _shortcutsHash = 0; mtpRequestId _shortcutsRequestId = 0; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 84986c28a9..807563fc09 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1542,6 +1542,10 @@ bool HistoryItem::isBusinessShortcut() const { return _shortcutId != 0; } +void HistoryItem::setRealShortcutId(BusinessShortcutId id) { + _shortcutId = id; +} + void HistoryItem::destroy() { _history->destroyMessage(this); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index f287bb0399..d75182680c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -196,6 +196,7 @@ public: [[nodiscard]] bool isUserpicSuggestion() const; [[nodiscard]] BusinessShortcutId shortcutId() const; [[nodiscard]] bool isBusinessShortcut() const; + void setRealShortcutId(BusinessShortcutId id); void addLogEntryOriginal( WebPageId localId, diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index c008fac993..cfb31eabe4 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -442,7 +442,7 @@ void BottomInfo::paintReactions( } QSize BottomInfo::countCurrentSize(int newWidth) { - if (newWidth >= maxWidth()) { + if (newWidth >= maxWidth() || (_data.flags & Data::Flag::Shortcut)) { return optimalSize(); } const auto dateHeight = (_data.flags & Data::Flag::Sponsored) @@ -509,7 +509,8 @@ void BottomInfo::layoutRepliesText() { if (!_data.replies || !*_data.replies || (_data.flags & Data::Flag::RepliesContext) - || (_data.flags & Data::Flag::Sending)) { + || (_data.flags & Data::Flag::Sending) + || (_data.flags & Data::Flag::Shortcut)) { _replies.clear(); return; } @@ -549,6 +550,9 @@ void BottomInfo::layoutReactionsText() { } QSize BottomInfo::countOptimalSize() { + if (_data.flags & Data::Flag::Shortcut) { + return { st::historySendStateSpace / 2, st::msgDateFont->height }; + } auto width = 0; if (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { width += st::historySendStateSpace; @@ -654,6 +658,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { if (item->isPinned() && message->context() != Context::Pinned) { result.flags |= Flag::Pinned; } + if (message->context() == Context::ShortcutMessages) { + result.flags |= Flag::Shortcut; + } if (const auto msgsigned = item->Get()) { if (!msgsigned->isAnonymousRank) { result.author = msgsigned->postAuthor; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index d593063ef2..efdab33347 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -44,6 +44,7 @@ public: Sponsored = 0x10, Pinned = 0x20, Imported = 0x40, + Shortcut = 0x80, //Unread, // We don't want to pass and update it in Date for now. }; friend inline constexpr bool is_flag_type(Flag) { return true; }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 3c09635a1a..31e6b4e9ec 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1937,6 +1937,9 @@ int ListWidget::resizeGetHeight(int newWidth) { _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0; + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } return _itemsTop + _itemsHeight + st::historyPaddingBottom; } @@ -3934,6 +3937,9 @@ void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { void ListWidget::setEmptyInfoWidget(base::unique_qptr &&w) { _emptyInfo = std::move(w); + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } } ListWidget::~ListWidget() { diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index d9a422adb1..1e5f34c2f4 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" -#include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" @@ -721,25 +720,7 @@ bool TopBar::computeCanToggleStoryPin() const { } Ui::StringWithNumbers TopBar::generateSelectedText() const { - using Type = Storage::SharedMediaType; - const auto phrase = [&] { - switch (_selectedItems.type) { - case Type::Photo: return tr::lng_media_selected_photo; - case Type::GIF: return tr::lng_media_selected_gif; - case Type::Video: return tr::lng_media_selected_video; - case Type::File: return tr::lng_media_selected_file; - case Type::MusicFile: return tr::lng_media_selected_song; - case Type::Link: return tr::lng_media_selected_link; - case Type::RoundVoiceFile: return tr::lng_media_selected_audio; - case Type::PhotoVideo: return tr::lng_stories_row_count; - } - Unexpected("Type in TopBar::generateSelectedText()"); - }(); - return phrase( - tr::now, - lt_count, - _selectedItems.list.size(), - Ui::StringWithNumbers::FromString); + return _selectedItems.title(_selectedItems.list.size()); } bool TopBar::selectionMode() const { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index bf03f71317..ddab6ef518 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "mainwidget.h" #include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_info.h" #include "styles/style_profile.h" @@ -64,6 +65,26 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { && section.settingsType()->hasCustomTopBar(); } +[[nodiscard]] Fn SelectedTitleForMedia( + Section::MediaType type) { + return [type](int count) { + using Type = Storage::SharedMediaType; + return [&] { + switch (type) { + case Type::Photo: return tr::lng_media_selected_photo; + case Type::GIF: return tr::lng_media_selected_gif; + case Type::Video: return tr::lng_media_selected_video; + case Type::File: return tr::lng_media_selected_file; + case Type::MusicFile: return tr::lng_media_selected_song; + case Type::Link: return tr::lng_media_selected_link; + case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + case Type::PhotoVideo: return tr::lng_stories_row_count; + } + Unexpected("Type in TopBar::generateSelectedText()"); + }()(tr::now, lt_count, count, Ui::StringWithNumbers::FromString); + }; +} + } // namespace struct WrapWidget::StackItem { @@ -71,6 +92,10 @@ struct WrapWidget::StackItem { // std::shared_ptr anotherTab; }; +SelectedItems::SelectedItems(Section::MediaType mediaType) +: title(SelectedTitleForMedia(mediaType)) { +} + WrapWidget::WrapWidget( QWidget *parent, not_null window, @@ -609,7 +634,12 @@ void WrapWidget::finishShowContent() { _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); _desiredBottomShadowVisibilities.fire( _content->desiredBottomShadowVisibility()); - _selectedLists.fire(_content->selectedListValue()); + if (auto selection = _content->selectedListValue()) { + _selectedLists.fire(std::move(selection)); + } else { + _selectedLists.fire(rpl::single( + SelectedItems(Storage::SharedMediaType::Photo))); + } _scrollTillBottomChanges.fire(_content->scrollTillBottomChanges()); _topShadow->raise(); _topShadow->finishAnimating(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index f102cc8349..0759c0729a 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -20,6 +20,7 @@ class PlainShadow; class PopupMenu; class IconButton; class RoundRect; +struct StringWithNumbers; } // namespace Ui namespace Window { @@ -61,11 +62,10 @@ struct SelectedItem { }; struct SelectedItems { - explicit SelectedItems(Storage::SharedMediaType type) - : type(type) { - } + SelectedItems() = default; + explicit SelectedItems(Storage::SharedMediaType type); - Storage::SharedMediaType type; + Fn title; std::vector list; }; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 2cddcf63e7..85878baf1c 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -248,6 +248,14 @@ void Widget::enableBackButton() { _flexibleScroll.backButtonEnables.fire({}); } +rpl::producer Widget::selectedListValue() const { + return _inner->selectedListValue(); +} + +void Widget::selectionAction(SelectionAction action) { + _inner->selectionAction(action); +} + void Widget::saveState(not_null memento) { memento->setScrollTop(scrollTopSave()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index d2eb636152..7ab7968d54 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -80,6 +80,9 @@ public: void enableBackButton() override; + rpl::producer selectedListValue() const override; + void selectionAction(SelectionAction action) override; + private: void saveState(not_null memento); void restoreState(not_null memento); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 6c820afa9d..180145716b 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -38,7 +38,6 @@ public: ~AwayMessage(); [[nodiscard]] rpl::producer title() override; - [[nodiscard]] rpl::producer sectionShowOther() override; private: diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 7e5ad6b1e2..81580801ca 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -41,6 +41,7 @@ public: ~Greeting(); [[nodiscard]] rpl::producer title() override; + [[nodiscard]] rpl::producer sectionShowOther() override; const Ui::RoundRect *bottomSkipRounding() const { return &_bottomSkipRounding; @@ -176,6 +177,10 @@ rpl::producer Greeting::title() { return tr::lng_greeting_title(); } +rpl::producer Greeting::sectionShowOther() { + return _showOther.events(); +} + void Greeting::setupContent( not_null controller) { using namespace rpl::mappers; diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index dc8927bc25..e13f822a2c 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -8,16 +8,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_quick_replies.h" #include "core/application.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { @@ -31,12 +37,13 @@ public: ~QuickReplies(); [[nodiscard]] rpl::producer title() override; + [[nodiscard]] rpl::producer sectionShowOther() override; private: void setupContent(not_null controller); void save(); - rpl::variable _recipients; + rpl::event_stream _showOther; }; @@ -57,6 +64,10 @@ rpl::producer QuickReplies::title() { return tr::lng_replies_title(); } +rpl::producer QuickReplies::sectionShowOther() { + return _showOther.events(); +} + void QuickReplies::setupContent( not_null controller) { using namespace rpl::mappers; @@ -73,24 +84,82 @@ void QuickReplies::setupContent( }); Ui::AddSkip(content); - const auto enabled = content->add(object_ptr( + const auto add = content->add(object_ptr( content, tr::lng_replies_add(), st::settingsButtonNoIcon )); - enabled->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto messages = &owner->shortcutMessages(); + add->setClickedCallback([=] { + controller->show(Box([=](not_null box) { + box->setTitle(tr::lng_replies_add_title()); + box->addRow(object_ptr( + box, + tr::lng_replies_add_shortcut(), + st::settingsAddReplyLabel)); + const auto field = box->addRow(object_ptr( + box, + st::settingsAddReplyField, + tr::lng_replies_add_placeholder(), + QString())); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + const auto submit = [=] { + const auto weak = Ui::MakeWeak(box); + const auto name = field->getLastText().trimmed(); + if (name.isEmpty()) { + field->showError(); + } else { + const auto id = messages->emplaceShortcut(name); + _showOther.fire(ShortcutMessagesId(id)); + } + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + field->submits( + ) | rpl::start_with_next(submit, field->lifetime()); + box->addButton(tr::lng_settings_save(), submit); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + })); }); - const auto wrap = content->add( - object_ptr>( - content, - object_ptr(content))); - const auto inner = wrap->entity(); + Ui::AddSkip(content); + Ui::AddDivider(content); + Ui::AddSkip(content); - Ui::AddSkip(inner); - Ui::AddDivider(inner); + const auto inner = content->add( + object_ptr(content)); + rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::start_with_next([=] { + while (inner->count()) { + delete inner->widgetAt(0); + } + const auto &shortcuts = messages->shortcuts(); + auto i = 0; + for (const auto &shortcut : shortcuts.list) { + const auto name = shortcut.second.name; + AddButtonWithLabel( + inner, + rpl::single('/' + name), + tr::lng_forum_messages( + lt_count, + rpl::single(1. * shortcut.second.count)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto id = messages->emplaceShortcut(name); + _showOther.fire(ShortcutMessagesId(id)); + }); + } + }, content->lifetime()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index df5f778ba9..e2186a172c 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -30,14 +30,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_sticker_toast.h" #include "history/history.h" #include "history/history_item.h" +#include "info/info_wrap_widget.h" #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" #include "main/main_session.h" #include "menu/menu_send.h" #include "settings/business/settings_recipients_helper.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/storage_media_prepare.h" +#include "storage/storage_shared_media.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -72,13 +75,16 @@ public: [[nodiscard]] static Type Id(BusinessShortcutId shortcutId); [[nodiscard]] Type id() const final override { - return Id(_shortcutId); + return Id(_shortcutId.current()); } [[nodiscard]] rpl::producer title() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; void setInnerFocus() override; + rpl::producer selectedListValue() override; + void selectionAction(Info::SelectionAction action) override; + bool paintOuter( not_null outer, int maxVisibleHeight, @@ -238,8 +244,9 @@ private: const not_null _controller; const not_null _session; const not_null _scroll; - const BusinessShortcutId _shortcutId; const not_null _history; + rpl::variable _shortcutId; + rpl::variable _shortcut; std::shared_ptr _style; std::shared_ptr _theme; QPointer _inner; @@ -248,6 +255,9 @@ private: rpl::event_stream<> _showBackRequests; bool _skipScrollEvent = false; + rpl::variable _selectedItems + = Info::SelectedItems(Storage::SharedMediaType::kCount); + std::unique_ptr _stickerToast; FullMsgId _lastShownAt; @@ -287,12 +297,31 @@ ShortcutMessages::ShortcutMessages( , _controller(controller) , _session(&controller->session()) , _scroll(scroll) -, _shortcutId(shortcutId) , _history(_session->data().history(_session->user()->id)) +, _shortcutId(shortcutId) +, _shortcut( + _session->data().shortcutMessages().lookupShortcut(shortcutId).name) , _cornerButtons( _scroll, controller->chatStyle(), static_cast(this)) { + const auto messages = &_session->data().shortcutMessages(); + + messages->shortcutIdChanged( + ) | rpl::start_with_next([=](Data::ShortcutIdChange change) { + if (change.oldId == _shortcutId.current()) { + if (change.newId) { + _shortcutId = change.newId; + } else { + _showBackRequests.fire({}); + } + } + }, lifetime()); + messages->shortcutsChanged( + ) | rpl::start_with_next([=] { + _shortcut = messages->lookupShortcut(_shortcutId.current()).name; + }, lifetime()); + controller->chatStyle()->paletteChanged( ) | rpl::start_with_next([=] { _scroll->updateBars(); @@ -351,7 +380,13 @@ Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { } rpl::producer ShortcutMessages::title() { - return rpl::single(u"Editing messages list"_q); + return _shortcut.value() | rpl::map([=](const QString &shortcut) { + return (shortcut == u"away"_q) + ? tr::lng_away_title() + : (shortcut == u"hello"_q) + ? tr::lng_greeting_title() + : rpl::single('/' + shortcut); + }) | rpl::flatten_latest(); } void ShortcutMessages::processScroll() { @@ -379,6 +414,18 @@ void ShortcutMessages::setInnerFocus() { _composeControls->focus(); } +rpl::producer ShortcutMessages::selectedListValue() { + return _selectedItems.value(); +} + +void ShortcutMessages::selectionAction(Info::SelectionAction action) { + switch (action) { + case Info::SelectionAction::Clear: clearSelected(); return; + case Info::SelectionAction::Delete: confirmDeleteSelected(); return; + } + Unexpected("Action in ShortcutMessages::selectionAction."); +} + bool ShortcutMessages::paintOuter( not_null outer, int maxVisibleHeight, @@ -572,7 +619,7 @@ bool ShortcutMessages::listScrollTo(int top, bool syntetic) { void ShortcutMessages::listCancelRequest() { if (_inner && !_inner->getSelectedItems().empty()) { - //clearSelected(); + clearSelected(); return; } else if (_composeControls->handleCancelRequest()) { return; @@ -592,13 +639,15 @@ rpl::producer ShortcutMessages::listSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { - const auto data = &_controller->session().data(); - return rpl::single(rpl::empty) | rpl::then( - data->shortcutMessages().updates(_shortcutId) - ) | rpl::map([=] { - return data->shortcutMessages().list(_shortcutId); - }); - return rpl::never(); + const auto messages = &_session->data().shortcutMessages(); + return _shortcutId.value( + ) | rpl::map([=](BusinessShortcutId shortcutId) { + return rpl::single(rpl::empty) | rpl::then( + messages->updates(shortcutId) + ) | rpl::map([=] { + return messages->list(shortcutId); + }); + }) | rpl::flatten_latest(); } bool ShortcutMessages::listAllowsMultiSelect() { @@ -617,6 +666,24 @@ bool ShortcutMessages::listIsLessInOrder( } void ShortcutMessages::listSelectionChanged(SelectedItems &&items) { + auto value = Info::SelectedItems(); + value.title = [](int count) { + return tr::lng_forum_messages( + tr::now, + lt_count, + count, + Ui::StringWithNumbers::FromString); + }; + value.list = items | ranges::views::transform([](SelectedItem item) { + auto result = Info::SelectedItem(GlobalMsgId{ item.msgId }); + result.canDelete = item.canDelete; + return result; + }) | ranges::to_vector; + _selectedItems = std::move(value); + + if (items.empty()) { + doSetInnerFocus(); + } } void ShortcutMessages::listMarkReadTill(not_null item) { @@ -784,20 +851,21 @@ bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { } void ShortcutMessages::pushReplyReturn(not_null item) { - if (item->shortcutId() == _shortcutId) { + if (item->shortcutId() == _shortcutId.current()) { _cornerButtons.pushReplyReturn(item); } } void ShortcutMessages::checkReplyReturns() { const auto currentTop = _scroll->scrollTop(); + const auto shortcutId = _shortcutId.current(); while (const auto replyReturn = _cornerButtons.replyReturn()) { const auto position = replyReturn->position(); const auto scrollTop = _inner->scrollTopForPosition(position); const auto below = scrollTop ? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax())) : _inner->isBelowPosition(position); - if (below) { + if (replyReturn->shortcutId() != shortcutId || below) { _cornerButtons.calculateNextReplyReturn(); } else { break; @@ -858,7 +926,7 @@ Api::SendAction ShortcutMessages::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); - result.options.shortcutId = _shortcutId; + result.options.shortcutId = _shortcutId.current(); result.options.sendAs = _composeControls->sendAsPeer(); return result; } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 41fb908e0c..fcdd0a7cdc 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -616,3 +616,19 @@ settingsWorkingHoursPicker: 200px; settingsWorkingHoursPickerItemHeight: 40px; settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px); + +settingsAddReplyLabel: FlatLabel(defaultFlatLabel) { + minWidth: 256px; +} +settingsAddReplyField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 10px, 0px, 2px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + + heightMin: 36px; +} \ No newline at end of file diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 02beb10033..e401ad419f 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -17,6 +17,11 @@ namespace anim { enum class repeat : uchar; } // namespace anim +namespace Info { +struct SelectedItems; +enum class SelectionAction; +} // namespace Info + namespace Main { class Session; } // namespace Main @@ -91,6 +96,13 @@ public: virtual void setStepDataReference(std::any &data) { } + [[nodiscard]] virtual auto selectedListValue() + -> rpl::producer { + return nullptr; + } + virtual void selectionAction(Info::SelectionAction action) { + } + virtual bool paintOuter( not_null outer, int maxVisibleHeight,