From 00cdae0369ef75318eaad5478a35bb66ed224295 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 Aug 2020 14:01:55 +0400 Subject: [PATCH] Implement replies list request. --- Telegram/CMakeLists.txt | 4 + Telegram/Resources/icons/history_replies.png | Bin 0 -> 315 bytes .../Resources/icons/history_replies@2x.png | Bin 0 -> 553 bytes .../Resources/icons/history_replies@3x.png | Bin 0 -> 573 bytes Telegram/Resources/langs/lang.strings | 8 + .../calls/calls_box_controller.cpp | 6 +- Telegram/SourceFiles/data/data_changes.h | 5 +- Telegram/SourceFiles/data/data_histories.cpp | 3 + .../SourceFiles/data/data_replies_list.cpp | 371 +++++ Telegram/SourceFiles/data/data_replies_list.h | 60 + Telegram/SourceFiles/data/data_session.cpp | 3 + Telegram/SourceFiles/history/history.cpp | 9 +- Telegram/SourceFiles/history/history.style | 5 + .../history/history_inner_widget.cpp | 12 + .../history/view/history_view_message.cpp | 17 +- .../view/history_view_replies_section.cpp | 1218 +++++++++++++++++ .../view/history_view_replies_section.h | 263 ++++ .../view/history_view_scheduled_section.h | 2 +- .../view/history_view_top_bar_widget.cpp | 14 +- .../view/history_view_top_bar_widget.h | 3 + .../history/view/media/history_view_gif.cpp | 2 +- .../view/media/history_view_location.cpp | 2 +- .../view/media/history_view_media_grouped.cpp | 2 +- .../history/view/media/history_view_photo.cpp | 2 +- 24 files changed, 1991 insertions(+), 20 deletions(-) create mode 100644 Telegram/Resources/icons/history_replies.png create mode 100644 Telegram/Resources/icons/history_replies@2x.png create mode 100644 Telegram/Resources/icons/history_replies@3x.png create mode 100644 Telegram/SourceFiles/data/data_replies_list.cpp create mode 100644 Telegram/SourceFiles/data/data_replies_list.h create mode 100644 Telegram/SourceFiles/history/view/history_view_replies_section.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_replies_section.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 36cf2e9603..f644141483 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -467,6 +467,8 @@ PRIVATE data/data_poll.h data/data_pts_waiter.cpp data/data_pts_waiter.h + data/data_replies_list.cpp + data/data_replies_list.h data/data_reply_preview.cpp data/data_reply_preview.h data/data_search_controller.cpp @@ -588,6 +590,8 @@ PRIVATE history/view/history_view_message.cpp history/view/history_view_message.h history/view/history_view_object.h + history/view/history_view_replies_section.cpp + history/view/history_view_replies_section.h history/view/history_view_schedule_box.cpp history/view/history_view_schedule_box.h history/view/history_view_scheduled_section.cpp diff --git a/Telegram/Resources/icons/history_replies.png b/Telegram/Resources/icons/history_replies.png new file mode 100644 index 0000000000000000000000000000000000000000..e3df0b27c54266d945a9564bdf1d0e8ad8683d00 GIT binary patch literal 315 zcmV-B0mS}^P)Q5#d8>W0YSWiClCY!K?Mb|5i!wP>}6yb-DP(_ zR2O`zU-P>F0PwCT2eBj?*;#50GUU9twis?|>vp_+`g&etQ`o$3X~zG)=*E zT^Pp^k|Y7cFhCSVc!@CvaU4SwMer_^BngLM;IGf~jEph-PGK0{N6<74>$=8m+mKR< zFEAm5_viD8Wm$5puIu-IaICKHdt8%g{ckYyQ)qPS#T z*OhlgQBIG%4;>K8vVc;00W?hmRaHUTwx`FpaGCr;f*`=Y@1GL@@Bt(5;Ut&aaI^pb N002ovPDHLkV1n$HfPnx2 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/history_replies@2x.png b/Telegram/Resources/icons/history_replies@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..274b59e61a72b4f7caccd225fa426fd6c6b02cf2 GIT binary patch literal 553 zcmV+^0@nSBP)#V(&CW1q&Soad0RX}e;+LJ&!C(-Q$)w|Kk;Mc-z}0Gns;Z(S zNlY<}_O)X)8pX|KgC=IP+4m99D_^hIB>DAv#dJFTJp|q>KOT>mN~N4kZcTtzc_x!_ zIys%dd*!k$qbQ1>NRGeXTS62?m`o-Ri^a%0S(YJ@NI*Co29Dz_+ezP3RTa+XGb|Ph zIGs-R+K}gYTrQXB1QbQVR;y+22)te|YMSN<0Tbi#7&(r!B;W#oKc*DAe22pU3WdTy zc@T{wP!z?GbidzIZMWMQu{!*IKZ%dW9i2%>@b@?P)_|z@ADTiwZ@iZfv)SW#kX88nTt-6B+xX?Zo{9cSS&)L(O@rPTGZD}r_*02 zMCp~!=ee)IOeTZl@!0iOSg+R@i^bSe$Pa{Rn%od%vR13PLgaEeet@7LsCXY$E|HD3wYqAzh^?isxsThx%s_qB>0Q2>`r>N9ok-_3UQMvMjElp7!61 zy5W!1z^7KFQgHxKO+*SnmSy|J*ztI@3s~26&prQdr~g{f1^|E`9AQv`?wGES00000 LNkvXXu0mjf5eEi^ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d477b138b8..f037f64011 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1344,6 +1344,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_scheduled_send_now_many#one" = "Send {count} message now?"; "lng_scheduled_send_now_many#other" = "Send {count} messages now?"; +"lng_replies_view#one" = "View {count} Reply"; +"lng_replies_view#other" = "View {count} Replies"; +"lng_replies_view_thread" = "View Thread"; +"lng_replies_header#one" = "{count} reply"; +"lng_replies_header#other" = "{count} replies"; +"lng_replies_header_none" = "No replies"; +"lng_replies_send_placeholder" = "Reply"; + "lng_archived_name" = "Archived chats"; "lng_archived_add" = "Archive"; "lng_archived_remove" = "Unarchive"; diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index c077b5c8af..4d5f4400d7 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -272,8 +272,10 @@ void BoxController::prepare() { }, lifetime()); session().changes().messageUpdates( - Data::MessageUpdate::Flag::CallAdded - ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + Data::MessageUpdate::Flag::NewAdded + ) | rpl::filter([=](const Data::MessageUpdate &update) { + return (update.item->media()->call() != nullptr); + }) | rpl::start_with_next([=](const Data::MessageUpdate &update) { insertRow(update.item, InsertWay::Prepend); }, lifetime()); diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2437686fea..72908d445b 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -133,11 +133,12 @@ struct MessageUpdate { Destroyed = (1 << 1), DialogRowRepaint = (1 << 2), DialogRowRefresh = (1 << 3), - CallAdded = (1 << 4), + NewAdded = (1 << 4), ReplyMarkup = (1 << 5), BotCallbackSent = (1 << 6), + NewMaybeAdded = (1 << 7), - LastUsedBit = (1 << 6), + LastUsedBit = (1 << 7), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index aeda2a45e5..05c54ff15b 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -676,6 +676,9 @@ void Histories::checkPostponed(not_null history, int id) { } void Histories::cancelRequest(int id) { + if (!id) { + return; + } const auto history = _historyByRequest.take(id); if (!history) { return; diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp new file mode 100644 index 0000000000..4c8aac09af --- /dev/null +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -0,0 +1,371 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_replies_list.h" + +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "data/data_histories.h" +#include "data/data_session.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_messages.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kMessagesPerPage = 4; // #TODO replies + +} // namespace + +struct RepliesList::Viewer { + MessagesSlice slice; + MsgId around = 0; + int limitBefore = 0; + int limitAfter = 0; +}; + +RepliesList::RepliesList(not_null history, MsgId rootId) +: _history(history) +, _rootId(rootId) { +} + +RepliesList::~RepliesList() { + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); +} + +rpl::producer RepliesList::source( + MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto around = aroundId.fullId.msg; + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto viewer = lifetime.make_state(); + const auto push = [=] { + if (buildFromData(viewer)) { + consumer.put_next_copy(viewer->slice); + } + }; + viewer->around = around; + viewer->limitBefore = limitBefore; + viewer->limitAfter = limitAfter; + + _history->session().changes().messageUpdates( + MessageUpdate::Flag::NewAdded + | MessageUpdate::Flag::NewMaybeAdded + | MessageUpdate::Flag::Destroyed + ) | rpl::filter([=](const MessageUpdate &update) { + return applyUpdate(viewer, update); + }) | rpl::start_with_next([=] { + crl::on_main(this, push); + }, lifetime); + + _partLoaded.events( + ) | rpl::start_with_next([=] { + crl::on_main(this, push); + }, lifetime); + + push(); + return lifetime; + }; +} + +rpl::producer RepliesList::fullCount() const { + return _fullCount.value() | rpl::filter_optional(); +} + +bool RepliesList::buildFromData(not_null viewer) { + if (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) { + viewer->slice.ids.clear(); + viewer->slice.fullCount + = viewer->slice.skippedBefore + = viewer->slice.skippedAfter + = 0; + return true; + } + const auto around = viewer->around; + if (_list.empty() + || (!around && _skippedAfter != 0) + || (around > 0 && around < _list.back()) + || (around > _list.front())) { + loadAround(around); + return false; + } + const auto i = around + ? ranges::lower_bound(_list, around, std::greater<>()) + : end(_list); + const auto availableBefore = (i - begin(_list)); + const auto availableAfter = (end(_list) - i); + const auto useBefore = std::min(availableBefore, viewer->limitBefore); + const auto useAfter = std::min(availableAfter, viewer->limitAfter + 1); + const auto slice = &viewer->slice; + if (_skippedBefore.has_value()) { + slice->skippedBefore + = (*_skippedBefore + (availableBefore - useBefore)); + } + if (_skippedAfter.has_value()) { + slice->skippedAfter + = (*_skippedAfter + (availableAfter - useAfter)); + } + const auto channelId = _history->channelId(); + slice->ids.clear(); + slice->ids.reserve(useBefore + useAfter); + for (auto j = i - useBefore, e = i + useAfter; j != e; ++j) { + slice->ids.emplace_back(channelId, *j); + } + ranges::reverse(slice->ids); + slice->fullCount = _fullCount.current(); + if (_skippedBefore != 0 && useBefore < viewer->limitBefore) { + loadBefore(); + } + if (_skippedAfter != 0 && useAfter < viewer->limitAfter + 1) { + loadAfter(); + } + return true; +} + +bool RepliesList::applyUpdate( + not_null viewer, + const MessageUpdate &update) { + if (update.item->history() != _history + || update.item->replyToTop() != _rootId) { + return false; + } + const auto id = update.item->id; + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + if (update.flags & MessageUpdate::Flag::Destroyed) { + if (i == end(_list) || *i != id) { + return false; + } + _list.erase(i); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + if (*known > 0) { + _fullCount = (*known - 1); + } + } + } else if (_skippedAfter != 0) { + return false; + } else { + if (i != end(_list) && *i == id) { + return false; + } + _list.insert(i, id); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + _fullCount = *known + 1; + } + } + return true; +} + +Histories &RepliesList::histories() { + return _history->owner().histories(); +} + +void RepliesList::loadAround(MsgId id) { + if (_loadingAround && *_loadingAround == id) { + return; + } + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + + const auto send = [=](Fn finish) { + return _history->session().api().request(MTPmessages_GetReplies( + _history->peer->input, + MTP_int(_rootId), + MTP_int(id), + MTP_int(id ? (-kMessagesPerPage / 2) : 0), + MTP_int(kMessagesPerPage), + MTP_int(0), + MTP_int(0), + MTP_int(0) + )).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + + if (!id) { + _skippedAfter = 0; + } else { + _skippedAfter = std::nullopt; + } + _skippedBefore = std::nullopt; + _list.clear(); + if (processMessagesIsEmpty(result)) { + _fullCount = _skippedBefore = _skippedAfter = 0; + } + }).fail([=](const RPCError &error) { + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + }).send(); + }; + _loadingAround = id; + _beforeId = histories().sendRequest( + _history, + Histories::RequestType::History, + send); +} + +void RepliesList::loadBefore() { + Expects(!_list.empty()); + + if (_loadingAround) { + histories().cancelRequest(base::take(_beforeId)); + } else if (_beforeId) { + return; + } + + const auto last = _list.back(); + const auto send = [=](Fn finish) { + return _history->session().api().request(MTPmessages_GetReplies( + _history->peer->input, + MTP_int(_rootId), + MTP_int(last), + MTP_int(0), + MTP_int(kMessagesPerPage), + MTP_int(0), + MTP_int(0), + MTP_int(0) + )).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.back() != last) { + loadBefore(); + } else if (processMessagesIsEmpty(result)) { + _skippedBefore = 0; + if (_skippedAfter == 0) { + _fullCount = _list.size(); + } + } + }).fail([=](const RPCError &error) { + _beforeId = 0; + finish(); + }).send(); + }; + _beforeId = histories().sendRequest( + _history, + Histories::RequestType::History, + send); +} + +void RepliesList::loadAfter() { + Expects(!_list.empty()); + + if (_afterId) { + return; + } + + const auto first = _list.front(); + const auto send = [=](Fn finish) { + return _history->session().api().request(MTPmessages_GetReplies( + _history->peer->input, + MTP_int(_rootId), + MTP_int(first + 1), + MTP_int(-kMessagesPerPage), + MTP_int(kMessagesPerPage), + MTP_int(0), + MTP_int(0), + MTP_int(0) + )).done([=](const MTPmessages_Messages &result) { + _afterId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.front() != first) { + loadAfter(); + } else if (processMessagesIsEmpty(result)) { + _skippedAfter = 0; + if (_skippedBefore == 0) { + _fullCount = _list.size(); + } + } + }).fail([=](const RPCError &error) { + _afterId = 0; + finish(); + }).send(); + }; + _afterId = histories().sendRequest( + _history, + Histories::RequestType::History, + send); +} + +bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) { + const auto guard = gsl::finally([&] { _partLoaded.fire({}); }); + + _fullCount = result.match([&](const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return 0; + }, [&](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [&](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [&](const MTPDmessages_channelMessages &data) { + if (_history->peer->isChannel()) { + _history->peer->asChannel()->ptsReceived(data.vpts().v); + } else { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (HistoryWidget::messagesReceived)")); + } + return data.vcount().v; + }); + + auto &owner = _history->owner(); + const auto list = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return QVector(); + }, [&](const auto &data) { + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); + return data.vmessages().v; + }); + if (list.isEmpty()) { + return true; + } + + const auto id = IdFromMessage(list.front()); + const auto toFront = !_list.empty() && (id > _list.front()); + const auto clientFlags = MTPDmessage_ClientFlags(); + const auto type = NewMessageType::Existing; + auto refreshed = std::vector(); + if (toFront) { + refreshed.reserve(_list.size() + list.size()); + } + for (const auto &message : list) { + if (const auto item = owner.addNewMessage(message, clientFlags, type)) { + if (item->replyToTop() == _rootId) { + if (toFront) { + refreshed.push_back(item->id); + } else { + _list.push_back(item->id); + } + } + } + } + if (toFront) { + refreshed.insert(refreshed.end(), _list.begin(), _list.end()); + _list = std::move(refreshed); + } + return false; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h new file mode 100644 index 0000000000..76d9fa1b74 --- /dev/null +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -0,0 +1,60 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/weak_ptr.h" + +class History; + +namespace Data { + +class Histories; +struct MessagePosition; +struct MessagesSlice; +struct MessageUpdate; + +class RepliesList final : public base::has_weak_ptr { +public: + RepliesList(not_null history, MsgId rootId); + ~RepliesList(); + + [[nodiscard]] rpl::producer source( + MessagePosition aroundId, + int limitBefore, + int limitAfter); + + [[nodiscard]] rpl::producer fullCount() const; + +private: + struct Viewer; + + [[nodiscard]] Histories &histories(); + + [[nodiscard]] bool buildFromData(not_null viewer); + [[nodiscard]] bool applyUpdate( + not_null viewer, + const MessageUpdate &update); + bool processMessagesIsEmpty(const MTPmessages_Messages &result); + void loadAround(MsgId id); + void loadBefore(); + void loadAfter(); + + const not_null _history; + const MsgId _rootId = 0; + std::vector _list; + std::optional _skippedBefore; + std::optional _skippedAfter; + rpl::variable> _fullCount; + rpl::event_stream<> _partLoaded; + std::optional _loadingAround; + int _beforeId = 0; + int _afterId = 0; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 127f93e000..90c58d711c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1685,6 +1685,9 @@ bool Session::checkEntitiesAndViewsUpdate(const MTPDmessage &data) { if (result) { stickers().checkSavedGif(existing); } + session().changes().messageUpdated( + existing, + Data::MessageUpdate::Flag::NewMaybeAdded); return result; } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 983520e42f..040dc6add0 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -1060,6 +1060,9 @@ void History::applyMessageChanges( applyServiceChanges(item, data.c_messageService()); } owner().stickers().checkSavedGif(item); + session().changes().messageUpdated( + item, + Data::MessageUpdate::Flag::NewAdded); } void History::applyServiceChanges( @@ -1247,12 +1250,6 @@ void History::applyServiceChanges( }); } } break; - - case mtpc_messageActionPhoneCall: { - item->history()->session().changes().messageUpdated( - item, - Data::MessageUpdate::Flag::CallAdded); - } break; } } diff --git a/Telegram/SourceFiles/history/history.style b/Telegram/SourceFiles/history/history.style index 9b7bdbf425..2b794c8ed6 100644 --- a/Telegram/SourceFiles/history/history.style +++ b/Telegram/SourceFiles/history/history.style @@ -159,6 +159,11 @@ historyViewsInSelectedIcon: icon {{ "history_views", msgInDateFgSelected }}; historyViewsOutIcon: icon {{ "history_views", historyOutIconFg }}; historyViewsOutSelectedIcon: icon {{ "history_views", historyOutIconFgSelected }}; historyViewsInvertedIcon: icon {{ "history_views", historySendingInvertedIconFg }}; +historyRepliesInIcon: icon {{ "history_replies", msgInDateFg }}; +historyRepliesInSelectedIcon: icon {{ "history_replies", msgInDateFgSelected }}; +historyRepliesOutIcon: icon {{ "history_replies", historyOutIconFg }}; +historyRepliesOutSelectedIcon: icon {{ "history_replies", historyOutIconFgSelected }}; +historyRepliesInvertedIcon: icon {{ "history_replies", historySendingInvertedIconFg }}; historyComposeField: InputField(defaultInputField) { font: msgFont; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 7dc560440a..95a72f462c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_context_menu.h" +#include "history/view/history_view_replies_section.h" #include "ui/widgets/popup_menu.h" #include "ui/image/image.h" #include "ui/toast/toast.h" @@ -1540,6 +1541,17 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _widget->replyToMessage(itemId); }); } + if (IsServerMsgId(item->id) && item->repliesCount() > 0) { + _menu->addAction(tr::lng_replies_view(tr::now, lt_count, item->repliesCount()), [=] { + controller->showSection( + HistoryView::RepliesMemento(_history, itemId.msg)); + }); + } else if (const auto replyToTop = item->replyToTop()) { + _menu->addAction(tr::lng_replies_view_thread(tr::now), [=] { + controller->showSection( + HistoryView::RepliesMemento(_history, replyToTop)); + }); + } if (item->allowsEdit(base::unixtime::now())) { _menu->addAction(tr::lng_context_edit_msg(tr::now), [=] { _widget->editMessage(itemId); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 207bf8068e..3fc502acd4 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1343,14 +1343,25 @@ void Message::drawInfo( } if (auto views = item->Get()) { + const auto showReplies = (views->views < 0) && (views->replies > 0); auto icon = [&] { if (item->id > 0) { if (outbg) { - return &(invertedsprites ? st::historyViewsInvertedIcon : (selected ? st::historyViewsOutSelectedIcon : st::historyViewsOutIcon)); + return &(invertedsprites + ? (showReplies ? st::historyRepliesInvertedIcon : st::historyViewsInvertedIcon) + : selected + ? (showReplies ? st::historyRepliesOutSelectedIcon : st::historyViewsOutSelectedIcon) + : (showReplies ? st::historyRepliesOutIcon : st::historyViewsOutIcon)); } - return &(invertedsprites ? st::historyViewsInvertedIcon : (selected ? st::historyViewsInSelectedIcon : st::historyViewsInIcon)); + return &(invertedsprites + ? (showReplies ? st::historyRepliesInvertedIcon : st::historyViewsInvertedIcon) + : selected + ? (showReplies ? st::historyRepliesInSelectedIcon : st::historyViewsInSelectedIcon) + : (showReplies ? st::historyRepliesInIcon : st::historyViewsInIcon)); } - return &(invertedsprites ? st::historyViewsSendingInvertedIcon : st::historyViewsSendingIcon); + return &(invertedsprites + ? st::historyViewsSendingInvertedIcon + : st::historyViewsSendingIcon); }(); if (item->id > 0) { icon->paint(p, infoRight - infoW, infoBottom + st::historyViewsTop, width); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp new file mode 100644 index 0000000000..b2bfdb9ee6 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -0,0 +1,1218 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_replies_section.h" + +#include "history/view/history_view_compose_controls.h" +#include "history/view/history_view_top_bar_widget.h" +#include "history/view/history_view_list_widget.h" +#include "history/view/history_view_schedule_box.h" +#include "history/history.h" +#include "history/history_drag_area.h" +#include "history/history_item.h" +#include "chat_helpers/send_context_menu.h" // SendMenu::Type. +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/shadow.h" +#include "ui/layers/generic_box.h" +#include "ui/text_options.h" +#include "ui/toast/toast.h" +#include "ui/special_buttons.h" +#include "ui/ui_utility.h" +#include "api/api_common.h" +#include "api/api_editing.h" +#include "api/api_sending.h" +#include "apiwrap.h" +#include "boxes/confirm_box.h" +#include "boxes/edit_caption_box.h" +#include "boxes/send_files_box.h" +#include "window/window_session_controller.h" +#include "window/window_peer_menu.h" +#include "base/event_filter.h" +#include "base/call_delayed.h" +#include "core/file_utilities.h" +#include "main/main_session.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/data_replies_list.h" +#include "data/data_user.h" +#include "storage/storage_media_prepare.h" +#include "storage/storage_account.h" +#include "inline_bots/inline_bot_result.h" +#include "platform/platform_specific.h" +#include "lang/lang_keys.h" +#include "facades.h" +#include "app.h" +#include "styles/style_history.h" +#include "styles/style_window.h" +#include "styles/style_info.h" +#include "styles/style_boxes.h" + +#include + +namespace HistoryView { +namespace { + +void ShowErrorToast(const QString &text) { + Ui::Toast::Show(Ui::Toast::Config{ + .text = { text }, + .st = &st::historyErrorToast, + .multiline = true, + }); +} + +bool CanSendFiles(not_null data) { + if (data->hasImage()) { + return true; + } else if (const auto urls = data->urls(); !urls.empty()) { + if (ranges::all_of(urls, &QUrl::isLocalFile)) { + return true; + } + } + return false; +} + +} // namespace + +object_ptr RepliesMemento::createWidget( + QWidget *parent, + not_null controller, + Window::Column column, + const QRect &geometry) { + if (column == Window::Column::Third) { + return nullptr; + } + auto result = object_ptr( + parent, + controller, + _history, + _rootId); + result->setInternalState(geometry, this); + return result; +} + +RepliesWidget::RepliesWidget( + QWidget *parent, + not_null controller, + not_null history, + MsgId rootId) +: Window::SectionWidget(parent, controller) +, _history(history) +, _rootId(rootId) +, _scroll(this, st::historyScroll, false) +, _topBar(this, controller) +, _topBarShadow(this) +, _composeControls(std::make_unique( + this, + controller, + ComposeControls::Mode::Normal)) +, _scrollDown(_scroll, st::historyToDown) { + _topBar->setActiveChat(_history, TopBarWidget::Section::Replies); + + _topBar->move(0, 0); + _topBar->resizeToWidth(width()); + _topBar->show(); + + _topBar->sendNowSelectionRequest( + ) | rpl::start_with_next([=] { + confirmSendNowSelected(); + }, _topBar->lifetime()); + _topBar->deleteSelectionRequest( + ) | rpl::start_with_next([=] { + confirmDeleteSelected(); + }, _topBar->lifetime()); + _topBar->clearSelectionRequest( + ) | rpl::start_with_next([=] { + clearSelected(); + }, _topBar->lifetime()); + + _topBarShadow->raise(); + updateAdaptiveLayout(); + subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); + + _inner = _scroll->setOwnedWidget(object_ptr( + this, + controller, + static_cast(this))); + _scroll->move(0, _topBar->height()); + _scroll->show(); + connect(_scroll, &Ui::ScrollArea::scrolled, [=] { onScroll(); }); + + _inner->editMessageRequested( + ) | rpl::start_with_next([=](auto fullId) { + if (const auto item = session().data().message(fullId)) { + const auto media = item->media(); + if (media && !media->webpage()) { + if (media->allowsEditCaption()) { + Ui::show(Box(controller, item)); + } + } else { + _composeControls->editMessage(fullId); + } + } + }, _inner->lifetime()); + + setupScrollDownButton(); + setupComposeControls(); +} + +RepliesWidget::~RepliesWidget() = default; + +void RepliesWidget::setupComposeControls() { + _composeControls->setHistory(_history); + + _composeControls->height( + ) | rpl::start_with_next([=] { + const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); + updateControlsGeometry(); + if (wasMax) { + listScrollTo(_scroll->scrollTopMax()); + } + }, lifetime()); + + _composeControls->cancelRequests( + ) | rpl::start_with_next([=] { + listCancelRequest(); + }, lifetime()); + + _composeControls->sendRequests( + ) | rpl::start_with_next([=] { + send(); + }, lifetime()); + + const auto saveEditMsgRequestId = lifetime().make_state(0); + _composeControls->editRequests( + ) | rpl::start_with_next([=](auto data) { + if (const auto item = session().data().message(data.fullId)) { + edit(item, data.options, saveEditMsgRequestId); + } + }, lifetime()); + + _composeControls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=] { + _choosingAttach = true; + base::call_delayed( + st::historyAttach.ripple.hideDuration, + this, + [=] { _choosingAttach = false; chooseAttach(); }); + }, lifetime()); + + using Selector = ChatHelpers::TabbedSelector; + + _composeControls->fileChosen( + ) | rpl::start_with_next([=](Selector::FileChosen chosen) { + sendExistingDocument(chosen.document); + }, lifetime()); + + _composeControls->photoChosen( + ) | rpl::start_with_next([=](Selector::PhotoChosen chosen) { + sendExistingPhoto(chosen.photo); + }, lifetime()); + + _composeControls->inlineResultChosen( + ) | rpl::start_with_next([=](Selector::InlineChosen chosen) { + sendInlineResult(chosen.result, chosen.bot); + }, lifetime()); + + _composeControls->scrollRequests( + ) | rpl::start_with_next([=](Data::MessagePosition pos) { + showAtPosition(pos); + }, lifetime()); + + _composeControls->keyEvents( + ) | rpl::start_with_next([=](not_null e) { + if (e->key() == Qt::Key_Up) { + if (!_composeControls->isEditingMessage()) { + // #TODO replies + //auto &messages = session().data().scheduledMessages(); + //if (const auto item = messages.lastSentMessage(_history)) { + // _inner->editMessageRequestNotify(item->fullId()); + //} else { + // _scroll->keyPressEvent(e); + //} + } else { + _scroll->keyPressEvent(e); + } + e->accept(); + } else if (e->key() == Qt::Key_Down) { + _scroll->keyPressEvent(e); + e->accept(); + } + }, lifetime()); + + _composeControls->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return CanSendFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + return confirmSendingFiles( + data, + CompressConfirm::Auto, + data->text()); + } + Unexpected("action in MimeData hook."); + }); +} + +void RepliesWidget::chooseAttach() { + if (const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media)) { + ShowErrorToast(*error); + return; + } + + const auto filter = FileDialog::AllFilesFilter() + + qsl(";;Image files (*") + + cImgExtensions().join(qsl(" *")) + + qsl(")"); + + FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto animated = false; + auto image = App::readImage( + result.remoteContent, + nullptr, + false, + &animated); + if (!image.isNull() && !animated) { + confirmSendingFiles( + std::move(image), + std::move(result.remoteContent), + CompressConfirm::Auto); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize); + if (list.allFilesForCompress || list.albumIsPossible) { + confirmSendingFiles(std::move(list), CompressConfirm::Auto); + } else if (!showSendingFilesError(list)) { + confirmSendingFiles(std::move(list), CompressConfirm::No); + } + } + }), nullptr); +} + +bool RepliesWidget::confirmSendingFiles( + not_null data, + CompressConfirm compressed, + const QString &insertTextOnCancel) { + const auto hasImage = data->hasImage(); + + if (const auto urls = data->urls(); !urls.empty()) { + auto list = Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize); + if (list.error != Storage::PreparedList::Error::NonLocalUrl) { + if (list.error == Storage::PreparedList::Error::None + || !hasImage) { + const auto emptyTextOnCancel = QString(); + confirmSendingFiles( + std::move(list), + compressed, + emptyTextOnCancel); + return true; + } + } + } + + if (hasImage) { + auto image = Platform::GetImageFromClipboard(); + if (image.isNull()) { + image = qvariant_cast(data->imageData()); + } + if (!image.isNull()) { + confirmSendingFiles( + std::move(image), + QByteArray(), + compressed, + insertTextOnCancel); + return true; + } + } + return false; +} + +bool RepliesWidget::confirmSendingFiles( + Storage::PreparedList &&list, + CompressConfirm compressed, + const QString &insertTextOnCancel) { + if (showSendingFilesError(list)) { + return false; + } + + const auto noCompressOption = (list.files.size() > 1) + && !list.allFilesForCompress + && !list.albumIsPossible; + const auto boxCompressConfirm = noCompressOption + ? CompressConfirm::None + : compressed; + + //const auto cursor = _field->textCursor(); + //const auto position = cursor.position(); + //const auto anchor = cursor.anchor(); + const auto text = _composeControls->getTextWithAppliedMarkdown();//_field->getTextWithTags(); + using SendLimit = SendFilesBox::SendLimit; + auto box = Box( + controller(), + std::move(list), + text, + boxCompressConfirm, + _history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many, + CanScheduleUntilOnline(_history->peer) + ? Api::SendType::ScheduledToUser + : Api::SendType::Scheduled, + SendMenu::Type::Disabled); + //_field->setTextWithTags({}); + + box->setConfirmedCallback(crl::guard(this, [=]( + Storage::PreparedList &&list, + SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + if (showSendingFilesError(list)) { + return; + } + const auto type = (way == SendFilesWay::Files) + ? SendMediaType::File + : SendMediaType::Photo; + const auto album = (way == SendFilesWay::Album) + ? std::make_shared() + : nullptr; + uploadFilesAfterConfirmation( + std::move(list), + type, + std::move(caption), + MsgId(0),//replyToId(), + options, + album); + })); + //box->setCancelledCallback(crl::guard(this, [=] { + // _field->setTextWithTags(text); + // auto cursor = _field->textCursor(); + // cursor.setPosition(anchor); + // if (position != anchor) { + // cursor.setPosition(position, QTextCursor::KeepAnchor); + // } + // _field->setTextCursor(cursor); + // if (!insertTextOnCancel.isEmpty()) { + // _field->textCursor().insertText(insertTextOnCancel); + // } + //})); + + //ActivateWindow(controller()); + const auto shown = Ui::show(std::move(box)); + shown->setCloseByOutsideClick(false); + + return true; +} + +bool RepliesWidget::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + CompressConfirm compressed, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + return confirmSendingFiles( + std::move(list), + compressed, + insertTextOnCancel); +} + +void RepliesWidget::uploadFilesAfterConfirmation( + Storage::PreparedList &&list, + SendMediaType type, + TextWithTags &&caption, + MsgId replyTo, + Api::SendOptions options, + std::shared_ptr album) { + const auto isAlbum = (album != nullptr); + const auto compressImages = (type == SendMediaType::Photo); + if (_history->peer->slowmodeApplied() + && ((list.files.size() > 1 && !album) + || (!list.files.empty() + && !caption.text.isEmpty() + && !list.canAddCaption(isAlbum, compressImages)))) { + ShowErrorToast(tr::lng_slowmode_no_many(tr::now)); + return; + } + auto action = Api::SendAction(_history); + action.replyTo = replyTo; + action.options = options; + session().api().sendFiles( + std::move(list), + type, + std::move(caption), + album, + action); +} + +void RepliesWidget::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + const auto callback = [=](Api::SendOptions options) { + auto action = Api::SendAction(_history); + //action.replyTo = replyToId(); + action.options = options; + session().api().sendFile(fileContent, type, action); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +bool RepliesWidget::showSendingFilesError( + const Storage::PreparedList &list) const { + const auto text = [&] { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media); + if (error) { + return *error; + } + using Error = Storage::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return tr::lng_send_image_too_large( + tr::now, + lt_name, + list.errorData); + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } + + ShowErrorToast(text); + return true; +} + +void RepliesWidget::send() { + if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { + return; + } + const auto callback = [=](Api::SendOptions options) { send(options); }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +void RepliesWidget::send(Api::SendOptions options) { + const auto webPageId = _composeControls->webPageId();/* _previewCancelled + ? CancelledWebPageId + : ((_previewData && _previewData->pendingTill >= 0) + ? _previewData->id + : WebPageId(0));*/ + + auto message = ApiWrap::MessageToSend(_history); + message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); + message.action.options = options; + //message.action.replyTo = replyToId(); + message.webPageId = webPageId; + + //const auto error = GetErrorTextForSending( + // _peer, + // _toForward, + // message.textWithTags); + //if (!error.isEmpty()) { + // ShowErrorToast(error); + // return; + //} + + session().api().sendMessage(std::move(message)); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + _composeControls->hidePanelsAnimated(); + + //if (_previewData && _previewData->pendingTill) previewCancel(); + _composeControls->focus(); +} + +void RepliesWidget::edit( + not_null item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId) { + if (*saveEditMsgRequestId) { + return; + } + const auto textWithTags = _composeControls->getTextWithAppliedMarkdown(); + const auto prepareFlags = Ui::ItemTextOptions( + _history, + session().user()).flags; + auto sending = TextWithEntities(); + auto left = TextWithEntities { + textWithTags.text, + TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; + TextUtilities::PrepareForSending(left, prepareFlags); + + if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { + if (item) { + Ui::show(Box(item, false)); + } else { + _composeControls->focus(); + } + return; + } else if (!left.text.isEmpty()) { + Ui::show(Box(tr::lng_edit_too_long(tr::now))); + return; + } + + lifetime().add([=] { + if (!*saveEditMsgRequestId) { + return; + } + session().api().request(base::take(*saveEditMsgRequestId)).cancel(); + }); + + const auto done = [=](const MTPUpdates &result, mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + _composeControls->cancelEditMessage(); + } + }; + + const auto fail = [=](const RPCError &error, mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + } + + const auto &err = error.type(); + if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) { + Ui::show(Box(tr::lng_edit_error(tr::now))); + } else if (err == u"MESSAGE_NOT_MODIFIED"_q) { + _composeControls->cancelEditMessage(); + } else if (err == u"MESSAGE_EMPTY"_q) { + _composeControls->focus(); + } else { + Ui::show(Box(tr::lng_edit_error(tr::now))); + } + update(); + return true; + }; + + *saveEditMsgRequestId = Api::EditTextMessage( + item, + sending, + options, + crl::guard(this, done), + crl::guard(this, fail)); + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); +} + +void RepliesWidget::sendExistingDocument( + not_null document) { + const auto callback = [=](Api::SendOptions options) { + sendExistingDocument(document, options); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +bool RepliesWidget::sendExistingDocument( + not_null document, + Api::SendOptions options) { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_stickers); + if (error) { + Ui::show(Box(*error), Ui::LayerOption::KeepOther); + return false; + } + + auto message = Api::MessageToSend(_history); + //message.action.replyTo = replyToId(); + message.action.options = options; + Api::SendExistingDocument(std::move(message), document); + + //if (_fieldAutocomplete->stickersShown()) { + // clearFieldText(); + // //_saveDraftText = true; + // //_saveDraftStart = crl::now(); + // //onDraftSave(); + // onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft + //} + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); + return true; +} + +void RepliesWidget::sendExistingPhoto(not_null photo) { + const auto callback = [=](Api::SendOptions options) { + sendExistingPhoto(photo, options); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +bool RepliesWidget::sendExistingPhoto( + not_null photo, + Api::SendOptions options) { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media); + if (error) { + Ui::show(Box(*error), Ui::LayerOption::KeepOther); + return false; + } + + auto message = Api::MessageToSend(_history); + //message.action.replyTo = replyToId(); + message.action.options = options; + Api::SendExistingPhoto(std::move(message), photo); + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); + return true; +} + +void RepliesWidget::sendInlineResult( + not_null result, + not_null bot) { + const auto errorText = result->getErrorOnSend(_history); + if (!errorText.isEmpty()) { + Ui::show(Box(errorText)); + return; + } + const auto callback = [=](Api::SendOptions options) { + sendInlineResult(result, bot, options); + }; + Ui::show( + PrepareScheduleBox(this, sendMenuType(), callback), + Ui::LayerOption::KeepOther); +} + +void RepliesWidget::sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options) { + auto action = Api::SendAction(_history); + //action.replyTo = replyToId(); + action.options = options; + action.generateLocal = true; + session().api().sendInlineResult(bot, result, action); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + bot->session().local().writeRecentHashtagsAndBots(); + } + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); +} + +SendMenu::Type RepliesWidget::sendMenuType() const { + return _history->peer->isSelf() + ? SendMenu::Type::Reminder + : HistoryView::CanScheduleUntilOnline(_history->peer) + ? SendMenu::Type::ScheduledToUser + : SendMenu::Type::Scheduled; +} + +void RepliesWidget::setupScrollDownButton() { + _scrollDown->setClickedCallback([=] { + scrollDownClicked(); + }); + base::install_event_filter(_scrollDown, [=](not_null event) { + if (event->type() != QEvent::Wheel) { + return base::EventFilterResult::Continue; + } + return _scroll->viewportEvent(event) + ? base::EventFilterResult::Cancel + : base::EventFilterResult::Continue; + }); + updateScrollDownVisibility(); +} + +void RepliesWidget::scrollDownClicked() { + showAtPosition(Data::MaxMessagePosition); +} + +void RepliesWidget::showAtPosition(Data::MessagePosition position) { + if (showAtPositionNow(position)) { + if (const auto highlight = base::take(_highlightMessageId)) { + _inner->highlightMessage(highlight); + } + } else { + _nextAnimatedScrollPosition = position; + _nextAnimatedScrollDelta = _inner->isBelowPosition(position) + ? -_scroll->height() + : _inner->isAbovePosition(position) + ? _scroll->height() + : 0; + auto memento = HistoryView::ListMemento(position); + _inner->restoreState(&memento); + } +} + +bool RepliesWidget::showAtPositionNow(Data::MessagePosition position) { + if (const auto scrollTop = _inner->scrollTopForPosition(position)) { + const auto currentScrollTop = _scroll->scrollTop(); + const auto wanted = snap(*scrollTop, 0, _scroll->scrollTopMax()); + const auto fullDelta = (wanted - currentScrollTop); + const auto limit = _scroll->height(); + const auto scrollDelta = snap(fullDelta, -limit, limit); + _inner->animatedScrollTo( + wanted, + position, + scrollDelta, + (std::abs(fullDelta) > limit + ? HistoryView::ListWidget::AnimatedScroll::Part + : HistoryView::ListWidget::AnimatedScroll::Full)); + return true; + } + return false; +} + +void RepliesWidget::updateScrollDownVisibility() { + if (animating()) { + return; + } + + const auto scrollDownIsVisible = [&]() -> std::optional { + const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + if (top < _scroll->scrollTopMax()) { + return true; + } + if (_inner->loadedAtBottomKnown()) { + return !_inner->loadedAtBottom(); + } + return std::nullopt; + }; + const auto scrollDownIsShown = scrollDownIsVisible(); + if (!scrollDownIsShown) { + return; + } + if (_scrollDownIsShown != *scrollDownIsShown) { + _scrollDownIsShown = *scrollDownIsShown; + _scrollDownShown.start( + [=] { updateScrollDownPosition(); }, + _scrollDownIsShown ? 0. : 1., + _scrollDownIsShown ? 1. : 0., + st::historyToDownDuration); + } +} + +void RepliesWidget::updateScrollDownPosition() { + // _scrollDown is a child widget of _scroll, not me. + auto top = anim::interpolate( + 0, + _scrollDown->height() + st::historyToDownPosition.y(), + _scrollDownShown.value(_scrollDownIsShown ? 1. : 0.)); + _scrollDown->moveToRight( + st::historyToDownPosition.x(), + _scroll->height() - top); + auto shouldBeHidden = !_scrollDownIsShown && !_scrollDownShown.animating(); + if (shouldBeHidden != _scrollDown->isHidden()) { + _scrollDown->setVisible(!shouldBeHidden); + } +} + +void RepliesWidget::scrollDownAnimationFinish() { + _scrollDownShown.stop(); + updateScrollDownPosition(); +} + +void RepliesWidget::updateAdaptiveLayout() { + _topBarShadow->moveToLeft( + Adaptive::OneColumn() ? 0 : st::lineWidth, + _topBar->height()); +} + +not_null RepliesWidget::history() const { + return _history; +} + +Dialogs::RowDescriptor RepliesWidget::activeChat() const { + return { + _history, + FullMsgId(_history->channelId(), ShowAtUnreadMsgId) + }; +} + +QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { + _topBar->updateControlsVisibility(); + if (params.withTopBarShadow) _topBarShadow->hide(); + _composeControls->showForGrab(); + auto result = Ui::GrabWidget(this); + if (params.withTopBarShadow) _topBarShadow->show(); + return result; +} + +void RepliesWidget::doSetInnerFocus() { + _composeControls->focus(); +} + +bool RepliesWidget::showInternal( + not_null memento, + const Window::SectionShow ¶ms) { + if (auto logMemento = dynamic_cast(memento.get())) { + if (logMemento->getHistory() == history()) { + restoreState(logMemento); + return true; + } + } + return false; +} + +void RepliesWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +bool RepliesWidget::pushTabbedSelectorToThirdSection( + not_null peer, + const Window::SectionShow ¶ms) { + return _composeControls->pushTabbedSelectorToThirdSection(peer, params); +} + +bool RepliesWidget::returnTabbedSelector() { + return _composeControls->returnTabbedSelector(); +} + +std::unique_ptr RepliesWidget::createMemento() { + auto result = std::make_unique(history(), _rootId); + saveState(result.get()); + return result; +} + +void RepliesWidget::saveState(not_null memento) { + memento->setReplies(_replies); + _inner->saveState(memento->list()); +} + +void RepliesWidget::restoreState(not_null memento) { + const auto setReplies = [&](std::shared_ptr replies) { + _replies = std::move(replies); + + rpl::single( + tr::lng_contacts_loading() + ) | rpl::then(_replies->fullCount( + ) | rpl::map([=](int count) { + return count + ? tr::lng_replies_header( + lt_count, + rpl::single(count) | tr::to_count()) + : tr::lng_replies_header_none(); + })) | rpl::flatten_latest( + ) | rpl::start_with_next([=](const QString &text) { + _topBar->setCustomTitle(text); + }, lifetime()); + }; + if (auto replies = memento->getReplies()) { + setReplies(std::move(replies)); + } else if (!_replies) { + setReplies(std::make_shared(_history, _rootId)); + } + _inner->restoreState(memento->list()); +} + +void RepliesWidget::resizeEvent(QResizeEvent *e) { + if (!width() || !height()) { + return; + } + _composeControls->resizeToWidth(width()); + updateControlsGeometry(); +} + +void RepliesWidget::updateControlsGeometry() { + const auto contentWidth = width(); + + const auto newScrollTop = _scroll->isHidden() + ? std::nullopt + : base::make_optional(_scroll->scrollTop() + topDelta()); + _topBar->resizeToWidth(contentWidth); + _topBarShadow->resize(contentWidth, st::lineWidth); + + const auto bottom = height(); + const auto controlsHeight = _composeControls->heightCurrent(); + const auto scrollHeight = bottom - _topBar->height() - controlsHeight; + const auto scrollSize = QSize(contentWidth, scrollHeight); + if (_scroll->size() != scrollSize) { + _skipScrollEvent = true; + _scroll->resize(scrollSize); + _inner->resizeToWidth(scrollSize.width(), _scroll->height()); + _skipScrollEvent = false; + } + if (!_scroll->isHidden()) { + if (newScrollTop) { + _scroll->scrollToY(*newScrollTop); + } + updateInnerVisibleArea(); + } + _composeControls->move(0, bottom - controlsHeight); + + updateScrollDownPosition(); +} + +void RepliesWidget::paintEvent(QPaintEvent *e) { + if (animating()) { + SectionWidget::paintEvent(e); + return; + } + if (Ui::skipPaintEvent(this, e)) { + return; + } + //if (hasPendingResizedItems()) { + // updateListSize(); + //} + + //auto ms = crl::now(); + //_historyDownShown.step(ms); + + SectionWidget::PaintBackground(controller(), this, e->rect()); +} + +void RepliesWidget::onScroll() { + if (_skipScrollEvent) { + return; + } + updateInnerVisibleArea(); +} + +void RepliesWidget::updateInnerVisibleArea() { + const auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + updateScrollDownVisibility(); +} + +void RepliesWidget::showAnimatedHook( + const Window::SectionSlideParams ¶ms) { + _topBar->setAnimatingMode(true); + if (params.withTopBarShadow) { + _topBarShadow->show(); + } + _composeControls->showStarted(); +} + +void RepliesWidget::showFinishedHook() { + _topBar->setAnimatingMode(false); + _composeControls->showFinished(); + + // We should setup the drag area only after + // the section animation is finished, + // because after that the method showChildren() is called. + setupDragArea(); +} + +bool RepliesWidget::floatPlayerHandleWheelEvent(QEvent *e) { + return _scroll->viewportEvent(e); +} + +QRect RepliesWidget::floatPlayerAvailableRect() { + return mapToGlobal(_scroll->geometry()); +} + +Context RepliesWidget::listContext() { + return Context::History; +} + +void RepliesWidget::listScrollTo(int top) { + if (_scroll->scrollTop() != top) { + _scroll->scrollToY(top); + } else { + updateInnerVisibleArea(); + } +} + +void RepliesWidget::listCancelRequest() { + if (_inner && !_inner->getSelectedItems().empty()) { + clearSelected(); + return; + } + if (_composeControls->isEditingMessage()) { + _composeControls->cancelEditMessage(); + return; + } + controller()->showBackFromStack(); +} + +void RepliesWidget::listDeleteRequest() { + confirmDeleteSelected(); +} + +rpl::producer RepliesWidget::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + return _replies->source(aroundId, limitBefore, limitAfter); +} + +void RepliesWidget::highlightSingleNewMessage( + const Data::MessagesSlice &slice) { + const auto guard = gsl::finally([&] { _lastSlice = slice; }); + if (_lastSlice.ids.empty() + || (slice.ids.size() != _lastSlice.ids.size() + 1)) { + return; + } + auto firstDifferent = 0; + while (firstDifferent != _lastSlice.ids.size()) { + if (slice.ids[firstDifferent] != _lastSlice.ids[firstDifferent]) { + break; + } + ++firstDifferent; + } + auto lastDifferent = slice.ids.size() - 1; + while (lastDifferent != firstDifferent) { + if (slice.ids[lastDifferent] != _lastSlice.ids[lastDifferent - 1]) { + break; + } + --lastDifferent; + } + if (firstDifferent != lastDifferent) { + return; + } + const auto newId = slice.ids[firstDifferent]; + if (const auto item = session().data().message(newId)) { + // _highlightMessageId = newId; + showAtPosition(item->position()); + } +} + +bool RepliesWidget::listAllowsMultiSelect() { + return true; +} + +bool RepliesWidget::listIsItemGoodForSelection( + not_null item) { + return !item->isSending() && !item->hasFailed(); +} + +bool RepliesWidget::listIsLessInOrder( + not_null first, + not_null second) { + return first->position() < second->position(); +} + +void RepliesWidget::listSelectionChanged(SelectedItems &&items) { + HistoryView::TopBarWidget::SelectedState state; + state.count = items.size(); + for (const auto item : items) { + if (item.canDelete) { + ++state.canDeleteCount; + } + if (item.canSendNow) { + ++state.canSendNowCount; + } + } + _topBar->showSelected(state); +} + +void RepliesWidget::listVisibleItemsChanged(HistoryItemsList &&items) { +} + +std::optional RepliesWidget::listUnreadBarView( + const std::vector> &elements) { + return std::nullopt; +} + +void RepliesWidget::listContentRefreshed() { +} + +ClickHandlerPtr RepliesWidget::listDateLink(not_null view) { + return nullptr; +} + +void RepliesWidget::confirmSendNowSelected() { + auto items = _inner->getSelectedItems(); + if (items.empty()) { + return; + } + const auto navigation = controller(); + Window::ShowSendNowMessagesBox( + navigation, + _history, + std::move(items), + [=] { navigation->showBackFromStack(); }); +} + +void RepliesWidget::confirmDeleteSelected() { + auto items = _inner->getSelectedItems(); + if (items.empty()) { + return; + } + const auto weak = Ui::MakeWeak(this); + const auto box = Ui::show(Box( + &_history->session(), + std::move(items))); + box->setDeleteConfirmedCallback([=] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }); +} + +void RepliesWidget::clearSelected() { + _inner->cancelSelection(); +} + +void RepliesWidget::setupDragArea() { + const auto areas = DragArea::SetupDragAreaToContainer( + this, + [=](not_null d) { return _history; }, + nullptr, + [=] { updateControlsGeometry(); }); + + const auto droppedCallback = [=](CompressConfirm compressed) { + return [=](const QMimeData *data) { + confirmSendingFiles(data, compressed); + Window::ActivateWindow(controller()); + }; + }; + areas.document->setDroppedCallback(droppedCallback(CompressConfirm::No)); + areas.photo->setDroppedCallback(droppedCallback(CompressConfirm::Yes)); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h new file mode 100644 index 0000000000..ab3fd7a054 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -0,0 +1,263 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "window/section_widget.h" +#include "window/section_memento.h" +#include "history/view/history_view_list_widget.h" +#include "data/data_messages.h" + +class History; +enum class CompressConfirm; +enum class SendMediaType; +struct SendingAlbum; + +namespace SendMenu { +enum class Type; +} // namespace SendMenu + +namespace Api { +struct SendOptions; +} // namespace Api + +namespace Storage { +struct PreparedList; +} // namespace Storage + +namespace Ui { +class ScrollArea; +class PlainShadow; +class FlatButton; +class HistoryDownButton; +} // namespace Ui + +namespace Profile { +class BackButton; +} // namespace Profile + +namespace InlineBots { +class Result; +} // namespace InlineBots + +namespace Data { +class RepliesList; +} // namespace Data + +namespace HistoryView { + +class Element; +class TopBarWidget; +class RepliesMemento; +class ComposeControls; + +class RepliesWidget final + : public Window::SectionWidget + , private ListDelegate { +public: + RepliesWidget( + QWidget *parent, + not_null controller, + not_null history, + MsgId rootId); + ~RepliesWidget(); + + [[nodiscard]] not_null history() const; + Dialogs::RowDescriptor activeChat() const override; + + bool hasTopBarShadow() const override { + return true; + } + + QPixmap grabForShowAnimation( + const Window::SectionSlideParams ¶ms) override; + + bool showInternal( + not_null memento, + const Window::SectionShow ¶ms) override; + std::unique_ptr createMemento() override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + // Tabbed selector management. + bool pushTabbedSelectorToThirdSection( + not_null peer, + const Window::SectionShow ¶ms) override; + bool returnTabbedSelector() override; + + // Float player interface. + bool floatPlayerHandleWheelEvent(QEvent *e) override; + QRect floatPlayerAvailableRect() override; + + // ListDelegate interface. + Context listContext() override; + void listScrollTo(int top) override; + void listCancelRequest() override; + void listDeleteRequest() override; + rpl::producer listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsItemGoodForSelection(not_null item) override; + bool listIsLessInOrder( + not_null first, + not_null second) override; + void listSelectionChanged(SelectedItems &&items) override; + void listVisibleItemsChanged(HistoryItemsList &&items) override; + std::optional listUnreadBarView( + const std::vector> &elements) override; + void listContentRefreshed() override; + ClickHandlerPtr listDateLink(not_null view) override; + +protected: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + + void showAnimatedHook( + const Window::SectionSlideParams ¶ms) override; + void showFinishedHook() override; + void doSetInnerFocus() override; + +private: + void onScroll(); + void updateInnerVisibleArea(); + void updateControlsGeometry(); + void updateAdaptiveLayout(); + void saveState(not_null memento); + void restoreState(not_null memento); + void showAtPosition(Data::MessagePosition position); + bool showAtPositionNow(Data::MessagePosition position); + + void setupComposeControls(); + + void setupDragArea(); + + void setupScrollDownButton(); + void scrollDownClicked(); + void scrollDownAnimationFinish(); + void updateScrollDownVisibility(); + void updateScrollDownPosition(); + + void confirmSendNowSelected(); + void confirmDeleteSelected(); + void clearSelected(); + + void send(); + void send(Api::SendOptions options); + void edit( + not_null item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId); + void highlightSingleNewMessage(const Data::MessagesSlice &slice); + void chooseAttach(); + [[nodiscard]] SendMenu::Type sendMenuType() const; + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + CompressConfirm compressed, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + Storage::PreparedList &&list, + CompressConfirm compressed, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + not_null data, + CompressConfirm compressed, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Storage::PreparedList &list) const; + void uploadFilesAfterConfirmation( + Storage::PreparedList &&list, + SendMediaType type, + TextWithTags &&caption, + MsgId replyTo, + Api::SendOptions options, + std::shared_ptr album); + + void sendExistingDocument(not_null document); + bool sendExistingDocument( + not_null document, + Api::SendOptions options); + void sendExistingPhoto(not_null photo); + bool sendExistingPhoto( + not_null photo, + Api::SendOptions options); + void sendInlineResult( + not_null result, + not_null bot); + void sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options); + + const not_null _history; + const MsgId _rootId = 0; + std::shared_ptr _replies; + object_ptr _scroll; + QPointer _inner; + object_ptr _topBar; + object_ptr _topBarShadow; + std::unique_ptr _composeControls; + bool _skipScrollEvent = false; + + FullMsgId _highlightMessageId; + std::optional _nextAnimatedScrollPosition; + int _nextAnimatedScrollDelta = 0; + + Ui::Animations::Simple _scrollDownShown; + bool _scrollDownIsShown = false; + object_ptr _scrollDown; + + Data::MessagesSlice _lastSlice; + bool _choosingAttach = false; + +}; + +class RepliesMemento : public Window::SectionMemento { +public: + RepliesMemento(not_null history, MsgId rootId) + : _history(history) + , _rootId(rootId) { + } + + object_ptr createWidget( + QWidget *parent, + not_null controller, + Window::Column column, + const QRect &geometry) override; + + [[nodiscard]] not_null getHistory() const { + return _history; + } + [[nodiscard]] MsgId getRootId() const { + return _rootId; + } + + void setReplies(std::shared_ptr replies) { + _replies = std::move(replies); + } + [[nodiscard]] std::shared_ptr getReplies() const { + return _replies; + } + + [[nodiscard]] not_null list() { + return &_list; + } + +private: + const not_null _history; + const MsgId _rootId = 0; + ListMemento _list; + std::shared_ptr _replies; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 3429d2a6b3..4ef6b9dbe7 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -240,4 +240,4 @@ private: }; -} // namespace HistoryScheduled +} // namespace HistoryView 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 34eb81b8ee..2587d7a5cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -330,9 +330,12 @@ void TopBarWidget::paintTopBar(Painter &p) { const auto folder = _activeChat.folder(); if (folder || history->peer->isSelf() - || (_section == Section::Scheduled)) { + || (_section == Section::Scheduled) + || !_customTitleText.isEmpty()) { // #TODO feed name emoji. - auto text = (_section == Section::Scheduled) + auto text = !_customTitleText.isEmpty() + ? _customTitleText + : (_section == Section::Scheduled) ? ((history && history->peer->isSelf()) ? tr::lng_reminder_messages(tr::now) : tr::lng_scheduled_messages(tr::now)) @@ -497,6 +500,13 @@ void TopBarWidget::setActiveChat(Dialogs::Key chat, Section section) { refreshUnreadBadge(); } +void TopBarWidget::setCustomTitle(const QString &title) { + if (_customTitleText != title) { + _customTitleText = title; + update(); + } +} + void TopBarWidget::refreshInfoButton() { if (const auto peer = _activeChat.peer()) { auto info = object_ptr( diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 218323c539..33cc263a77 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -44,6 +44,7 @@ public: enum class Section { History, Scheduled, + Replies, }; TopBarWidget( @@ -62,6 +63,7 @@ public: void setAnimatingMode(bool enabled); void setActiveChat(Dialogs::Key chat, Section section); + void setCustomTitle(const QString &title); rpl::producer<> forwardSelectionRequest() const { return _forwardSelection.events(); @@ -125,6 +127,7 @@ private: const not_null _controller; Dialogs::Key _activeChat; Section _section = Section::History; + QString _customTitleText; int _selectedCount = 0; bool _canDelete = false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 9c1c725aa5..5508e503bd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1137,7 +1137,7 @@ bool Gif::needsBubble() const { } const auto item = _parent->data(); return item->viaBot() - || item->Has() + || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName(); return false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp index ece2a3e946..212dee5b1d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp @@ -320,7 +320,7 @@ bool Location::needsBubble() const { } const auto item = _parent->data(); return item->viaBot() - || item->Has() + || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName(); return false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index 9b6778fc7b..d08bd4190e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -454,7 +454,7 @@ bool GroupedMedia::computeNeedBubble() const { } if (const auto item = _parent->data()) { if (item->viaBot() - || item->Has() + || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName() ) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index f825c91e0c..9870c1cfae 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -802,7 +802,7 @@ bool Photo::needsBubble() const { const auto item = _parent->data(); if (item->toHistoryMessage()) { return item->viaBot() - || item->Has() + || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName(); }