diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 3fe58f5305..3277cb44aa 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_scheduled_messages.h" #include "base/unixtime.h" +#include "data/data_forum_topic.h" #include "data/data_peer.h" #include "data/data_session.h" #include "api/api_hash.h" @@ -173,6 +174,16 @@ int ScheduledMessages::count(not_null history) const { return (i != end(_data)) ? i->second.items.size() : 0; } +bool ScheduledMessages::hasFor(not_null topic) const { + const auto i = _data.find(topic->owningHistory()); + if (i == end(_data)) { + return false; + } + return ranges::any_of(i->second.items, [&](const OwnedItem &item) { + return item->topic() == topic; + }); +} + void ScheduledMessages::sendNowSimpleMessage( const MTPDupdateShortSentMessage &update, not_null local) { @@ -374,7 +385,8 @@ rpl::producer<> ScheduledMessages::updates(not_null history) { }) | rpl::to_empty; } -Data::MessagesSlice ScheduledMessages::list(not_null history) { +Data::MessagesSlice ScheduledMessages::list( + not_null history) const { auto result = Data::MessagesSlice(); const auto i = _data.find(history); if (i == end(_data)) { @@ -396,6 +408,31 @@ Data::MessagesSlice ScheduledMessages::list(not_null history) { return result; } +Data::MessagesSlice ScheduledMessages::list( + not_null topic) const { + auto result = Data::MessagesSlice(); + const auto i = _data.find(topic->Data::Thread::owningHistory()); + if (i == end(_data)) { + const auto i = _requests.find(topic->Data::Thread::owningHistory()); + if (i == end(_requests)) { + return result; + } + result.fullCount = result.skippedAfter = result.skippedBefore = 0; + return result; + } + const auto &list = i->second.items; + result.skippedAfter = result.skippedBefore = 0; + result.fullCount = int(list.size()); + result.ids = ranges::views::all( + list + ) | ranges::views::filter([&](const OwnedItem &item) { + return item->topic() == topic; + }) | ranges::views::transform( + &HistoryItem::fullId + ) | ranges::to_vector; + return result; +} + void ScheduledMessages::request(not_null history) { const auto peer = history->peer; if (peer->isBroadcast() && !Data::CanSendAnything(peer)) { diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.h b/Telegram/SourceFiles/data/data_scheduled_messages.h index e4abea70a5..5daacd099c 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.h +++ b/Telegram/SourceFiles/data/data_scheduled_messages.h @@ -34,6 +34,7 @@ public: [[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const; [[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const; [[nodiscard]] int count(not_null history) const; + [[nodiscard]] bool hasFor(not_null topic) const; [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; void checkEntitiesAndUpdate(const MTPDmessage &data); @@ -51,7 +52,9 @@ public: not_null local); [[nodiscard]] rpl::producer<> updates(not_null history); - [[nodiscard]] Data::MessagesSlice list(not_null history); + [[nodiscard]] Data::MessagesSlice list(not_null history) const; + [[nodiscard]] Data::MessagesSlice list( + not_null topic) const; private: using OwnedItem = std::unique_ptr; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 8a5fd3dfa1..b480d1dea2 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2723,6 +2723,9 @@ void HistoryWidget::setupScheduledToggle() { ) | rpl::map([=](Dialogs::Key key) -> rpl::producer<> { if (const auto history = key.history()) { return session().data().scheduledMessages().updates(history); + } else if (const auto topic = key.topic()) { + return session().data().scheduledMessages().updates( + topic->owningHistory()); } return rpl::never(); }) | rpl::flatten_latest( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 8eddbd40d5..8fe8752c53 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -850,9 +850,36 @@ ComposeControls::ComposeControls( descriptor.stickerOrEmojiChosen ) | rpl::start_to_stream(_stickerOrEmojiChosen, _wrap->lifetime()); } + if (descriptor.scheduledToggleValue) { + std::move( + descriptor.scheduledToggleValue + ) | rpl::start_with_next([=](bool hasScheduled) { + if (!_scheduled && hasScheduled) { + _scheduled = base::make_unique_q( + _wrap.get(), + st::historyScheduledToggle); + _scheduled->show(); + _scheduled->clicks( + ) | rpl::filter( + rpl::mappers::_1 == Qt::LeftButton + ) | rpl::to_empty | rpl::start_to_stream( + _showScheduledRequests, + _scheduled->lifetime()); + orderControls(); // Raise drag areas to the top. + updateControlsVisibility(); + updateControlsGeometry(_wrap->size()); + } else if (_scheduled && !hasScheduled) { + _scheduled = nullptr; + } + }, _wrap->lifetime()); + } init(); } +rpl::producer<> ComposeControls::showScheduledRequests() const { + return _showScheduledRequests.events(); +} + ComposeControls::~ComposeControls() { saveFieldToHistoryLocalDraft(); unregisterDraftSources(); @@ -2497,7 +2524,7 @@ void ComposeControls::finishAnimating() { void ComposeControls::updateControlsGeometry(QSize size) { // (_attachToggle|_replaceMedia) (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel - // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_silent|_botCommandStart) _tabbedSelectorToggle _send + // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_botCommandStart) _tabbedSelectorToggle _send const auto fieldWidth = size.width() - _attachToggle->width() @@ -2508,6 +2535,7 @@ void ComposeControls::updateControlsGeometry(QSize size) { - (_likeShown ? _like->width() : 0) - (_botCommandShown ? _botCommandStart->width() : 0) - (_silent ? _silent->width() : 0) + - (_scheduled ? _scheduled->width() : 0) - (_ttlInfo ? _ttlInfo->width() : 0); { const auto oldFieldHeight = _field->height(); @@ -2566,6 +2594,10 @@ void ComposeControls::updateControlsGeometry(QSize size) { _silent->moveToRight(right, buttonsTop); right += _silent->width(); } + if (_scheduled) { + _scheduled->moveToRight(right, buttonsTop); + right += _scheduled->width(); + } if (_ttlInfo) { _ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop); } @@ -2595,6 +2627,9 @@ void ComposeControls::updateControlsVisibility() { } else { _attachToggle->show(); } + if (_scheduled) { + _scheduled->setVisible(!isEditingMessage()); + } } bool ComposeControls::updateLikeShown() { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 82d1a24dea..779c8c2d87 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -112,6 +112,7 @@ struct ComposeControlsDescriptor { QString voiceCustomCancelText; bool voiceLockFromBottom = false; ChatHelpers::ComposeFeatures features; + rpl::producer scheduledToggleValue; }; class ComposeControls final { @@ -172,6 +173,7 @@ public: [[nodiscard]] auto replyNextRequests() const -> rpl::producer; [[nodiscard]] rpl::producer<> focusRequests() const; + [[nodiscard]] rpl::producer<> showScheduledRequests() const; using MimeDataHook = Fn data, @@ -382,6 +384,7 @@ private: std::unique_ptr _silent; std::unique_ptr _ttlInfo; base::unique_qptr _charsLimitation; + base::unique_qptr _scheduled; std::unique_ptr _inlineResults; std::unique_ptr _tabbedPanel; @@ -408,6 +411,7 @@ private: rpl::event_stream<> _likeToggled; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; + rpl::event_stream<> _showScheduledRequests; rpl::variable _recording; rpl::variable _hasSendText; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 0c88e71e6e..18c3dc57a3 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -861,7 +861,9 @@ QSize Message::performCountOptimalSize() { void Message::refreshTopicButton() { const auto item = data(); - if (isAttachedToPrevious() || context() != Context::History) { + if (isAttachedToPrevious() + || (context() != Context::History) + || item->isScheduled()) { _topicButton = nullptr; } else if (const auto topic = item->topic()) { if (!_topicButton) { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 8813a2c05f..7e4f218b24 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_sticker_toast.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_contact_status.h" +#include "history/view/history_view_scheduled_section.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_pinned_tracker.h" #include "history/view/history_view_pinned_section.h" @@ -62,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_shared_media.h" #include "data/data_send_action.h" +#include "data/data_scheduled_messages.h" #include "data/data_premium_limits.h" #include "storage/storage_media_prepare.h" #include "storage/storage_account.h" @@ -221,9 +223,19 @@ RepliesWidget::RepliesWidget( listShowPremiumToast(emoji); }, .mode = ComposeControls::Mode::Normal, - .sendMenuType = SendMenu::Type::SilentOnly, + .sendMenuType = _topic + ? SendMenu::Type::Scheduled + : SendMenu::Type::SilentOnly, .regularWindow = controller, .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), + .scheduledToggleValue = _topic + ? rpl::single(rpl::empty_value()) | rpl::then( + session().data().scheduledMessages().updates( + _topic->owningHistory()) + ) | rpl::map([=] { + return session().data().scheduledMessages().hasFor(_topic); + }) + : rpl::single(false), })) , _translateBar(std::make_unique(this, controller, history)) , _scroll(std::make_unique( @@ -364,6 +376,20 @@ RepliesWidget::RepliesWidget( ) | rpl::start_with_next([=] { _inner->update(); }, lifetime()); + } else { + session().api().sendActions( + ) | rpl::filter([=](const Api::SendAction &action) { + return (action.history == _history) + && (action.replyTo.topicRootId == _topic->topicRootId()); + }) | rpl::start_with_next([=](const Api::SendAction &action) { + if (action.options.scheduled) { + _composeControls->cancelReplyMessage(); + crl::on_main(this, [=, t = _topic] { + controller->showSection( + std::make_shared(t)); + }); + } + }, lifetime()); } setupTopicViewer(); @@ -778,6 +804,14 @@ void RepliesWidget::setupComposeControls() { data.direction == Direction::Next); }, lifetime()); + _composeControls->showScheduledRequests( + ) | rpl::start_with_next([=] { + controller()->showSection( + _topic + ? std::make_shared(_topic) + : std::make_shared(_history)); + }, lifetime()); + _composeControls->setMimeDataHook([=]( not_null data, Ui::InputField::MimeAction action) { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 2b00066bbd..56f106e269 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -33,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/mime_type.h" #include "chat_helpers/tabbed_selector.h" #include "main/main_session.h" +#include "data/data_forum.h" +#include "data/data_forum_topic.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_scheduled_messages.h" @@ -53,6 +55,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { +ScheduledMemento::ScheduledMemento(not_null history) +: _history(history) +, _forumTopic(nullptr) { +} + +ScheduledMemento::ScheduledMemento(not_null forumTopic) +: _history(forumTopic->owningHistory()) +, _forumTopic(forumTopic) { +} + object_ptr ScheduledMemento::createWidget( QWidget *parent, not_null controller, @@ -61,7 +73,11 @@ object_ptr ScheduledMemento::createWidget( if (column == Window::Column::Third) { return nullptr; } - auto result = object_ptr(parent, controller, _history); + auto result = object_ptr( + parent, + controller, + _history, + _forumTopic); result->setInternalState(geometry, this); return result; } @@ -69,9 +85,11 @@ object_ptr ScheduledMemento::createWidget( ScheduledWidget::ScheduledWidget( QWidget *parent, not_null controller, - not_null history) + not_null history, + const Data::ForumTopic *forumTopic) : Window::SectionWidget(parent, controller, history->peer) , _history(history) +, _forumTopic(forumTopic) , _scroll( this, controller->chatStyle()->value(lifetime(), st::historyScroll), @@ -175,30 +193,80 @@ ScheduledWidget::ScheduledWidget( ScheduledWidget::~ScheduledWidget() = default; void ScheduledWidget::setupComposeControls() { - auto writeRestriction = rpl::combine( - session().changes().peerFlagsValue( - _history->peer, - Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer) - ) | rpl::map([=] { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - const auto canSendAnything = Data::CanSendAnyOf( - _history->peer, - allWithoutPolls); - const auto restriction = Data::RestrictionError( - _history->peer, - ChatRestriction::SendOther); - auto text = !canSendAnything - ? (restriction - ? restriction - : tr::lng_group_not_accessible(tr::now)) - : std::optional(); - return text ? Controls::WriteRestriction{ - .text = std::move(*text), - .type = Controls::WriteRestrictionType::Rights, - } : Controls::WriteRestriction(); - }); + auto writeRestriction = _forumTopic + ? [&] { + auto topicWriteRestrictions = rpl::single( + ) | rpl::then(session().changes().topicUpdates( + Data::TopicUpdate::Flag::Closed + ) | rpl::filter([=](const Data::TopicUpdate &update) { + return (update.topic->history() == _history) + && (update.topic->rootId() == _forumTopic->rootId()); + }) | rpl::to_empty) | rpl::map([=] { + return (!_forumTopic + || _forumTopic->canToggleClosed() + || !_forumTopic->closed()) + ? std::optional() + : tr::lng_forum_topic_closed(tr::now); + }); + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer), + std::move(topicWriteRestrictions) + ) | rpl::map([=]( + auto, + auto, + std::optional topicRestriction) { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _forumTopic, + allWithoutPolls); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction + : topicRestriction + ? std::move(topicRestriction) + : tr::lng_group_not_accessible(tr::now)) + : topicRestriction + ? std::move(topicRestriction) + : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }); + }() + : [&] { + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer) + ) | rpl::map([=] { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _history->peer, + allWithoutPolls); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction + : tr::lng_group_not_accessible(tr::now)) + : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }); + }(); _composeControls->setHistory({ .history = _history.get(), .writeRestriction = std::move(writeRestriction), @@ -564,6 +632,12 @@ Api::SendAction ScheduledWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.options.sendAs = _composeControls->sendAsPeer(); + if (_forumTopic) { + result.replyTo.topicRootId = _forumTopic->topicRootId(); + result.replyTo.messageId = FullMsgId( + history()->peer->id, + _forumTopic->topicRootId()); + } return result; } @@ -576,7 +650,7 @@ void ScheduledWidget::send() { const auto error = GetErrorTextForSending( _history->peer, { - .topicRootId = MsgId(), + .topicRootId = _forumTopic ? _forumTopic->topicRootId() : MsgId(), .forward = nullptr, .text = &textWithTags, .ignoreSlowmodeCountdown = true, @@ -943,6 +1017,16 @@ bool ScheduledWidget::returnTabbedSelector() { } std::shared_ptr ScheduledWidget::createMemento() { + if (_forumTopic) { + if (const auto forum = history()->asForum()) { + const auto rootId = _forumTopic->topicRootId(); + if (const auto topic = forum->topicFor(rootId)) { + auto result = std::make_shared(topic); + saveState(result.get()); + return result; + } + } + } auto result = std::make_shared(history()); saveState(result.get()); return result; @@ -1098,7 +1182,9 @@ rpl::producer ScheduledWidget::listSource( return rpl::single(rpl::empty) | rpl::then( data->scheduledMessages().updates(_history) ) | rpl::map([=] { - return data->scheduledMessages().list(_history); + return _forumTopic + ? data->scheduledMessages().list(_forumTopic) + : data->scheduledMessages().list(_history); }) | rpl::after_next([=](const Data::MessagesSlice &slice) { highlightSingleNewMessage(slice); }); @@ -1187,6 +1273,9 @@ void ScheduledWidget::listUpdateDateLink( } bool ScheduledWidget::listElementHideReply(not_null view) { + if (const auto root = view->data()->topicRootId()) { + return root == view->data()->replyTo().messageId.msg; + } return false; } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 857410b4f5..850e56bb79 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -58,7 +58,8 @@ public: ScheduledWidget( QWidget *parent, not_null controller, - not_null history); + not_null history, + const Data::ForumTopic *forumTopic); ~ScheduledWidget(); not_null history() const; @@ -261,6 +262,7 @@ private: Api::SendOptions options); const not_null _history; + const Data::ForumTopic *_forumTopic; std::shared_ptr _theme; object_ptr _scroll; QPointer _inner; @@ -280,9 +282,8 @@ private: class ScheduledMemento : public Window::SectionMemento { public: - ScheduledMemento(not_null history) - : _history(history) { - } + ScheduledMemento(not_null history); + ScheduledMemento(not_null forumTopic); object_ptr createWidget( QWidget *parent, @@ -300,6 +301,7 @@ public: private: const not_null _history; + const Data::ForumTopic *_forumTopic; ListMemento _list; };