init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
604
Telegram/SourceFiles/data/data_forum.cpp
Normal file
604
Telegram/SourceFiles/data/data_forum.cpp
Normal file
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
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_forum.h"
|
||||
|
||||
#include "data/components/recent_peers.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_forum_icons.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_replies_list.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_unread_things.h"
|
||||
#include "main/main_session.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/application.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTopicsFirstLoad = 20;
|
||||
constexpr auto kLoadedTopicsMinCount = 20;
|
||||
constexpr auto kTopicsPerPage = 500;
|
||||
constexpr auto kStalePerRequest = 100;
|
||||
constexpr auto kShowTopicNamesCount = 8;
|
||||
// constexpr auto kGeneralColorId = 0xA9A9A9;
|
||||
|
||||
} // namespace
|
||||
|
||||
Forum::Forum(not_null<History*> history)
|
||||
: _history(history)
|
||||
, _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) {
|
||||
Expects(_history->peer->isChannel()
|
||||
|| _history->peer->isBot());
|
||||
|
||||
if (_history->inChatList()) {
|
||||
preloadTopics();
|
||||
}
|
||||
if (peer()->canCreateTopics()) {
|
||||
owner().forumIcons().requestDefaultIfUnknown();
|
||||
}
|
||||
}
|
||||
|
||||
Forum::~Forum() {
|
||||
for (const auto &request : _topicRequests) {
|
||||
if (request.second.id != _staleRequestId) {
|
||||
owner().histories().cancelRequest(request.second.id);
|
||||
}
|
||||
}
|
||||
if (_staleRequestId) {
|
||||
session().api().request(_staleRequestId).cancel();
|
||||
}
|
||||
if (_requestId) {
|
||||
session().api().request(_requestId).cancel();
|
||||
}
|
||||
auto &storage = session().storage();
|
||||
auto &changes = session().changes();
|
||||
const auto peerId = _history->peer->id;
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
storage.unload(Storage::SharedMediaUnloadThread(
|
||||
peerId,
|
||||
rootId,
|
||||
PeerId()));
|
||||
_history->setForwardDraft(rootId, PeerId(), {});
|
||||
|
||||
const auto raw = topic.get();
|
||||
changes.topicRemoved(raw);
|
||||
changes.entryRemoved(raw);
|
||||
}
|
||||
}
|
||||
|
||||
Session &Forum::owner() const {
|
||||
return _history->owner();
|
||||
}
|
||||
|
||||
Main::Session &Forum::session() const {
|
||||
return _history->session();
|
||||
}
|
||||
|
||||
not_null<History*> Forum::history() const {
|
||||
return _history;
|
||||
}
|
||||
|
||||
not_null<PeerData*> Forum::peer() const {
|
||||
return _history->peer;
|
||||
}
|
||||
|
||||
UserData *Forum::bot() const {
|
||||
return _history->peer->asBot();
|
||||
}
|
||||
|
||||
ChannelData *Forum::channel() const {
|
||||
return _history->peer->asChannel();
|
||||
}
|
||||
|
||||
not_null<Dialogs::MainList*> Forum::topicsList() {
|
||||
return &_topicsList;
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::destroyed() const {
|
||||
if (const auto bot = this->bot()) {
|
||||
return bot->flagsValue(
|
||||
) | rpl::filter([=](const UserData::Flags::Change &update) {
|
||||
using Flag = UserData::Flag;
|
||||
return (update.diff & Flag::Forum)
|
||||
&& !(update.value & Flag::Forum);
|
||||
}) | rpl::take(1) | rpl::to_empty;
|
||||
}
|
||||
return channel()->flagsValue(
|
||||
) | rpl::filter([=](const ChannelData::Flags::Change &update) {
|
||||
using Flag = ChannelData::Flag;
|
||||
return (update.diff & Flag::Forum) && !(update.value & Flag::Forum);
|
||||
}) | rpl::take(1) | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<not_null<ForumTopic*>> Forum::topicDestroyed() const {
|
||||
return _topicDestroyed.events();
|
||||
}
|
||||
|
||||
void Forum::preloadTopics() {
|
||||
if (topicsList()->indexed()->size() < kLoadedTopicsMinCount) {
|
||||
requestTopics();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::reloadTopics() {
|
||||
_topicsList.setLoaded(false);
|
||||
session().api().request(base::take(_requestId)).cancel();
|
||||
_offset = {};
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
if (!topic->creating()) {
|
||||
_staleRootIds.emplace(topic->rootId());
|
||||
}
|
||||
}
|
||||
requestTopics();
|
||||
}
|
||||
|
||||
void Forum::requestTopics() {
|
||||
if (_topicsList.loaded() || _requestId) {
|
||||
return;
|
||||
}
|
||||
const auto firstLoad = !_offset.date;
|
||||
const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage;
|
||||
_requestId = session().api().request(MTPmessages_GetForumTopics(
|
||||
MTP_flags(0),
|
||||
peer()->input(),
|
||||
MTPstring(), // q
|
||||
MTP_int(_offset.date),
|
||||
MTP_int(_offset.id),
|
||||
MTP_int(_offset.topicId),
|
||||
MTP_int(loadCount)
|
||||
)).done([=](const MTPmessages_ForumTopics &result) {
|
||||
const auto previousOffset = _offset;
|
||||
applyReceivedTopics(result, _offset);
|
||||
const auto &list = result.data().vtopics().v;
|
||||
if (list.isEmpty()
|
||||
|| list.size() == result.data().vcount().v
|
||||
|| (_offset == previousOffset)) {
|
||||
_topicsList.setLoaded();
|
||||
}
|
||||
_requestId = 0;
|
||||
_chatsListChanges.fire({});
|
||||
if (_topicsList.loaded()) {
|
||||
_chatsListLoadedEvents.fire({});
|
||||
}
|
||||
reorderLastTopics();
|
||||
requestSomeStale();
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
_topicsList.setLoaded();
|
||||
if (error.type() == u"CHANNEL_FORUM_MISSING"_q && channel()) {
|
||||
const auto flags = channel()->flags() & ~ChannelDataFlag::Forum;
|
||||
channel()->setFlags(flags);
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Forum::applyTopicDeleted(MsgId rootId) {
|
||||
_topicsDeleted.emplace(rootId);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i == end(_topics)) {
|
||||
return;
|
||||
}
|
||||
const auto raw = i->second.get();
|
||||
Core::App().notifications().clearFromTopic(raw);
|
||||
owner().removeChatListEntry(raw);
|
||||
|
||||
if (ranges::contains(_lastTopics, not_null(raw))) {
|
||||
reorderLastTopics();
|
||||
}
|
||||
|
||||
if (_activeSubsectionTopic == raw) {
|
||||
_activeSubsectionTopic = nullptr;
|
||||
}
|
||||
_topicDestroyed.fire(raw);
|
||||
_history->session().recentPeers().chatOpenRemove(raw);
|
||||
session().changes().topicUpdated(
|
||||
raw,
|
||||
Data::TopicUpdate::Flag::Destroyed);
|
||||
session().changes().entryUpdated(
|
||||
raw,
|
||||
Data::EntryUpdate::Flag::Destroyed);
|
||||
_topics.erase(i);
|
||||
|
||||
_history->destroyMessagesByTopic(rootId);
|
||||
session().storage().unload(Storage::SharedMediaUnloadThread(
|
||||
_history->peer->id,
|
||||
rootId,
|
||||
PeerId()));
|
||||
_history->setForwardDraft(rootId, PeerId(), {});
|
||||
}
|
||||
|
||||
void Forum::reorderLastTopics() {
|
||||
// We want first kShowTopicNamesCount histories, by last message date.
|
||||
const auto pred = [](not_null<ForumTopic*> a, not_null<ForumTopic*> b) {
|
||||
const auto aItem = a->chatListMessage();
|
||||
const auto bItem = b->chatListMessage();
|
||||
const auto aDate = aItem ? aItem->date() : TimeId(0);
|
||||
const auto bDate = bItem ? bItem->date() : TimeId(0);
|
||||
return aDate > bDate;
|
||||
};
|
||||
_lastTopics.clear();
|
||||
_lastTopics.reserve(kShowTopicNamesCount + 1);
|
||||
auto &&topics = ranges::views::all(
|
||||
*_topicsList.indexed()
|
||||
) | ranges::views::transform([](not_null<Dialogs::Row*> row) {
|
||||
return row->topic();
|
||||
});
|
||||
auto nonPinnedChecked = 0;
|
||||
for (const auto topic : topics) {
|
||||
const auto i = ranges::upper_bound(
|
||||
_lastTopics,
|
||||
not_null(topic),
|
||||
pred);
|
||||
if (size(_lastTopics) < kShowTopicNamesCount
|
||||
|| i != end(_lastTopics)) {
|
||||
_lastTopics.insert(i, topic);
|
||||
}
|
||||
if (size(_lastTopics) > kShowTopicNamesCount) {
|
||||
_lastTopics.pop_back();
|
||||
}
|
||||
if (!topic->isPinnedDialog(FilterId())
|
||||
&& ++nonPinnedChecked >= kShowTopicNamesCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
++_lastTopicsVersion;
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
|
||||
int Forum::recentTopicsListVersion() const {
|
||||
return _lastTopicsVersion;
|
||||
}
|
||||
|
||||
void Forum::recentTopicsInvalidate(not_null<ForumTopic*> topic) {
|
||||
if (ranges::contains(_lastTopics, topic)) {
|
||||
++_lastTopicsVersion;
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<not_null<ForumTopic*>> &Forum::recentTopics() const {
|
||||
return _lastTopics;
|
||||
}
|
||||
|
||||
void Forum::saveActiveSubsectionThread(not_null<Thread*> thread) {
|
||||
if (const auto topic = thread->asTopic()) {
|
||||
Assert(topic->forum() == this);
|
||||
_activeSubsectionTopic = topic->creating() ? nullptr : topic;
|
||||
} else {
|
||||
Assert(thread == history());
|
||||
_activeSubsectionTopic = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Thread *Forum::activeSubsectionThread() const {
|
||||
return _activeSubsectionTopic;
|
||||
}
|
||||
|
||||
void Forum::markUnreadCountsUnknown(MsgId readTillId) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
const auto replies = topic->replies();
|
||||
if (replies->unreadCountCurrent() > 0) {
|
||||
replies->setInboxReadTill(readTillId, std::nullopt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::updateUnreadCounts(
|
||||
MsgId readTillId,
|
||||
const base::flat_map<not_null<ForumTopic*>, int> &counts) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
const auto raw = topic.get();
|
||||
const auto replies = raw->replies();
|
||||
const auto i = counts.find(raw);
|
||||
const auto count = (i != end(counts)) ? i->second : 0;
|
||||
replies->setInboxReadTill(readTillId, count);
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) {
|
||||
if (from || to) {
|
||||
reorderLastTopics();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
ForumOffsets &updateOffsets) {
|
||||
applyReceivedTopics(topics, [&](not_null<ForumTopic*> topic) {
|
||||
if (const auto last = topic->lastServerMessage()) {
|
||||
updateOffsets.date = last->date();
|
||||
updateOffsets.id = last->id;
|
||||
}
|
||||
updateOffsets.topicId = topic->rootId();
|
||||
});
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPmessages_ForumTopics &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback) {
|
||||
const auto &data = topics.data();
|
||||
owner().processUsers(data.vusers());
|
||||
owner().processChats(data.vchats());
|
||||
owner().processMessages(data.vmessages(), NewMessageType::Existing);
|
||||
if (const auto channel = this->channel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
}
|
||||
applyReceivedTopics(data.vtopics(), std::move(callback));
|
||||
if (!_staleRootIds.empty()) {
|
||||
requestSomeStale();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::applyReceivedTopics(
|
||||
const MTPVector<MTPForumTopic> &topics,
|
||||
Fn<void(not_null<ForumTopic*>)> callback) {
|
||||
const auto &list = topics.v;
|
||||
for (const auto &topic : list) {
|
||||
const auto rootId = topic.match([&](const auto &data) {
|
||||
return data.vid().v;
|
||||
});
|
||||
_staleRootIds.remove(rootId);
|
||||
topic.match([&](const MTPDforumTopicDeleted &data) {
|
||||
applyTopicDeleted(rootId);
|
||||
}, [&](const MTPDforumTopic &data) {
|
||||
_topicsDeleted.remove(rootId);
|
||||
const auto i = _topics.find(rootId);
|
||||
const auto creating = (i == end(_topics));
|
||||
const auto raw = creating
|
||||
? _topics.emplace(
|
||||
rootId,
|
||||
std::make_unique<ForumTopic>(this, rootId)
|
||||
).first->second.get()
|
||||
: i->second.get();
|
||||
raw->applyTopic(data);
|
||||
if (creating) {
|
||||
if (const auto last = _history->chatListMessage()
|
||||
; last && last->topicRootId() == rootId) {
|
||||
_history->lastItemDialogsView().itemInvalidated(last);
|
||||
_history->updateChatListEntry();
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::requestSomeStale() {
|
||||
if (_staleRequestId
|
||||
|| (!_offset.id && _requestId)
|
||||
|| _staleRootIds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto type = Histories::RequestType::History;
|
||||
auto rootIds = QVector<MTPint>();
|
||||
rootIds.reserve(std::min(int(_staleRootIds.size()), kStalePerRequest));
|
||||
for (auto i = begin(_staleRootIds); i != end(_staleRootIds);) {
|
||||
const auto rootId = *i;
|
||||
i = _staleRootIds.erase(i);
|
||||
|
||||
rootIds.push_back(MTP_int(rootId));
|
||||
if (rootIds.size() == kStalePerRequest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rootIds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto call = [=] {
|
||||
for (const auto &id : rootIds) {
|
||||
finishTopicRequest(id.v);
|
||||
}
|
||||
};
|
||||
auto &histories = owner().histories();
|
||||
_staleRequestId = histories.sendRequest(_history, type, [=](
|
||||
Fn<void()> finish) {
|
||||
return session().api().request(
|
||||
MTPmessages_GetForumTopicsByID(
|
||||
peer()->input(),
|
||||
MTP_vector<MTPint>(rootIds))
|
||||
).done([=](const MTPmessages_ForumTopics &result) {
|
||||
_staleRequestId = 0;
|
||||
applyReceivedTopics(result);
|
||||
call();
|
||||
finish();
|
||||
}).fail([=] {
|
||||
_staleRequestId = 0;
|
||||
call();
|
||||
finish();
|
||||
}).send();
|
||||
});
|
||||
for (const auto &id : rootIds) {
|
||||
_topicRequests[id.v].id = _staleRequestId;
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::finishTopicRequest(MsgId rootId) {
|
||||
if (const auto request = _topicRequests.take(rootId)) {
|
||||
for (const auto &callback : request->callbacks) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::requestTopic(MsgId rootId, Fn<void()> done) {
|
||||
auto &request = _topicRequests[rootId];
|
||||
if (done) {
|
||||
request.callbacks.push_back(std::move(done));
|
||||
}
|
||||
if (!request.id
|
||||
&& _staleRootIds.emplace(rootId).second
|
||||
&& (_staleRootIds.size() == 1)) {
|
||||
crl::on_main(&session(), [peer = peer()] {
|
||||
if (const auto forum = peer->forum()) {
|
||||
forum->requestSomeStale();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ForumTopic *Forum::applyTopicAdded(
|
||||
MsgId rootId,
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId,
|
||||
PeerId creatorId,
|
||||
TimeId date,
|
||||
bool my) {
|
||||
Expects(rootId != 0);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
const auto raw = (i != end(_topics))
|
||||
? i->second.get()
|
||||
: _topics.emplace(
|
||||
rootId,
|
||||
std::make_unique<ForumTopic>(this, rootId)
|
||||
).first->second.get();
|
||||
raw->applyTitle(title);
|
||||
raw->applyColorId(colorId);
|
||||
raw->applyIconId(iconId);
|
||||
raw->applyCreator(creatorId);
|
||||
raw->applyCreationDate(date);
|
||||
raw->applyIsMy(my);
|
||||
if (!creating(rootId)) {
|
||||
raw->addToChatList(FilterId(), topicsList());
|
||||
_chatsListChanges.fire({});
|
||||
reorderLastTopics();
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
MsgId Forum::reserveCreatingId(
|
||||
const QString &title,
|
||||
int32 colorId,
|
||||
DocumentId iconId) {
|
||||
const auto result = owner().nextLocalMessageId();
|
||||
_creatingRootIds.emplace(result);
|
||||
applyTopicAdded(
|
||||
result,
|
||||
title,
|
||||
colorId,
|
||||
iconId,
|
||||
session().userPeerId(),
|
||||
base::unixtime::now(),
|
||||
true);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Forum::discardCreatingId(MsgId rootId) {
|
||||
Expects(creating(rootId));
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i != end(_topics)) {
|
||||
Assert(!i->second->inChatList());
|
||||
_topics.erase(i);
|
||||
}
|
||||
_creatingRootIds.remove(rootId);
|
||||
}
|
||||
|
||||
bool Forum::creating(MsgId rootId) const {
|
||||
return _creatingRootIds.contains(rootId);
|
||||
}
|
||||
|
||||
void Forum::created(MsgId rootId, MsgId realId) {
|
||||
if (rootId == realId) {
|
||||
return;
|
||||
}
|
||||
_creatingRootIds.remove(rootId);
|
||||
const auto i = _topics.find(rootId);
|
||||
Assert(i != end(_topics));
|
||||
auto topic = std::move(i->second);
|
||||
_topics.erase(i);
|
||||
const auto id = FullMsgId(_history->peer->id, realId);
|
||||
if (!_topics.contains(realId)) {
|
||||
_topics.emplace(
|
||||
realId,
|
||||
std::move(topic)
|
||||
).first->second->setRealRootId(realId);
|
||||
|
||||
reorderLastTopics();
|
||||
}
|
||||
owner().notifyItemIdChange({ id, rootId });
|
||||
}
|
||||
|
||||
void Forum::clearAllUnreadMentions() {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
topic->unreadMentions().clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::clearAllUnreadReactions() {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
topic->unreadReactions().clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Forum::enumerateTopics(Fn<void(not_null<ForumTopic*>)> action) const {
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
action(topic.get());
|
||||
}
|
||||
}
|
||||
|
||||
ForumTopic *Forum::topicFor(MsgId rootId) {
|
||||
if (!rootId) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto i = _topics.find(rootId);
|
||||
return (i != end(_topics)) ? i->second.get() : nullptr;
|
||||
}
|
||||
|
||||
ForumTopic *Forum::enforceTopicFor(MsgId rootId) {
|
||||
Expects(rootId != 0);
|
||||
|
||||
const auto i = _topics.find(rootId);
|
||||
if (i != end(_topics)) {
|
||||
return i->second.get();
|
||||
}
|
||||
requestTopic(rootId);
|
||||
return applyTopicAdded(rootId, {}, {}, {}, {}, {}, {});
|
||||
}
|
||||
|
||||
bool Forum::topicDeleted(MsgId rootId) const {
|
||||
return _topicsDeleted.contains(rootId)
|
||||
|| (rootId == ForumTopic::kGeneralId && peer()->isBot());
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::chatsListChanges() const {
|
||||
return _chatsListChanges.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::chatsListLoadedEvents() const {
|
||||
return _chatsListLoadedEvents.events();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
Reference in New Issue
Block a user