Added initial ability to send and receive scheduled messages in forums.

This commit is contained in:
23rd 2024-03-12 15:36:36 +03:00
parent cd59f1d576
commit 672ad64e53
9 changed files with 246 additions and 37 deletions

View File

@ -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*> history) const {
return (i != end(_data)) ? i->second.items.size() : 0;
}
bool ScheduledMessages::hasFor(not_null<Data::ForumTopic*> 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<HistoryItem*> local) {
@ -374,7 +385,8 @@ rpl::producer<> ScheduledMessages::updates(not_null<History*> history) {
}) | rpl::to_empty;
}
Data::MessagesSlice ScheduledMessages::list(not_null<History*> history) {
Data::MessagesSlice ScheduledMessages::list(
not_null<History*> 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*> history) {
return result;
}
Data::MessagesSlice ScheduledMessages::list(
not_null<const Data::ForumTopic*> 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*> history) {
const auto peer = history->peer;
if (peer->isBroadcast() && !Data::CanSendAnything(peer)) {

View File

@ -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*> history) const;
[[nodiscard]] bool hasFor(not_null<Data::ForumTopic*> topic) const;
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
void checkEntitiesAndUpdate(const MTPDmessage &data);
@ -51,7 +52,9 @@ public:
not_null<HistoryItem*> local);
[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history) const;
[[nodiscard]] Data::MessagesSlice list(
not_null<const Data::ForumTopic*> topic) const;
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;

View File

@ -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::empty_value>();
}) | rpl::flatten_latest(

View File

@ -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<Ui::IconButton>(
_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() {

View File

@ -112,6 +112,7 @@ struct ComposeControlsDescriptor {
QString voiceCustomCancelText;
bool voiceLockFromBottom = false;
ChatHelpers::ComposeFeatures features;
rpl::producer<bool> scheduledToggleValue;
};
class ComposeControls final {
@ -172,6 +173,7 @@ public:
[[nodiscard]] auto replyNextRequests() const
-> rpl::producer<ReplyNextRequest>;
[[nodiscard]] rpl::producer<> focusRequests() const;
[[nodiscard]] rpl::producer<> showScheduledRequests() const;
using MimeDataHook = Fn<bool(
not_null<const QMimeData*> data,
@ -382,6 +384,7 @@ private:
std::unique_ptr<Ui::SilentToggle> _silent;
std::unique_ptr<Controls::TTLButton> _ttlInfo;
base::unique_qptr<Controls::CharactersLimitLabel> _charsLimitation;
base::unique_qptr<Ui::IconButton> _scheduled;
std::unique_ptr<InlineBots::Layout::Widget> _inlineResults;
std::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;
@ -408,6 +411,7 @@ private:
rpl::event_stream<> _likeToggled;
rpl::event_stream<ReplyNextRequest> _replyNextRequests;
rpl::event_stream<> _focusRequests;
rpl::event_stream<> _showScheduledRequests;
rpl::variable<bool> _recording;
rpl::variable<bool> _hasSendText;

View File

@ -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) {

View File

@ -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<TranslateBar>(this, controller, history))
, _scroll(std::make_unique<Ui::ScrollArea>(
@ -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<HistoryView::ScheduledMemento>(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<HistoryView::ScheduledMemento>(_topic)
: std::make_shared<HistoryView::ScheduledMemento>(_history));
}, lifetime());
_composeControls->setMimeDataHook([=](
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) {

View File

@ -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(history)
, _forumTopic(nullptr) {
}
ScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic)
: _history(forumTopic->owningHistory())
, _forumTopic(forumTopic) {
}
object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
@ -61,7 +73,11 @@ object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
if (column == Window::Column::Third) {
return nullptr;
}
auto result = object_ptr<ScheduledWidget>(parent, controller, _history);
auto result = object_ptr<ScheduledWidget>(
parent,
controller,
_history,
_forumTopic);
result->setInternalState(geometry, this);
return result;
}
@ -69,9 +85,11 @@ object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
ScheduledWidget::ScheduledWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<History*> history)
not_null<History*> 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<QString>();
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<QString>()
: 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<QString> 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<QString>();
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<QString>();
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<Window::SectionMemento> 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<ScheduledMemento>(topic);
saveState(result.get());
return result;
}
}
}
auto result = std::make_shared<ScheduledMemento>(history());
saveState(result.get());
return result;
@ -1098,7 +1182,9 @@ rpl::producer<Data::MessagesSlice> 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<const Element*> view) {
if (const auto root = view->data()->topicRootId()) {
return root == view->data()->replyTo().messageId.msg;
}
return false;
}

View File

@ -58,7 +58,8 @@ public:
ScheduledWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<History*> history);
not_null<History*> history,
const Data::ForumTopic *forumTopic);
~ScheduledWidget();
not_null<History*> history() const;
@ -261,6 +262,7 @@ private:
Api::SendOptions options);
const not_null<History*> _history;
const Data::ForumTopic *_forumTopic;
std::shared_ptr<Ui::ChatTheme> _theme;
object_ptr<Ui::ScrollArea> _scroll;
QPointer<ListWidget> _inner;
@ -280,9 +282,8 @@ private:
class ScheduledMemento : public Window::SectionMemento {
public:
ScheduledMemento(not_null<History*> history)
: _history(history) {
}
ScheduledMemento(not_null<History*> history);
ScheduledMemento(not_null<Data::ForumTopic*> forumTopic);
object_ptr<Window::SectionWidget> createWidget(
QWidget *parent,
@ -300,6 +301,7 @@ public:
private:
const not_null<History*> _history;
const Data::ForumTopic *_forumTopic;
ListMemento _list;
};