2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-31 14:38:15 +00:00

Allow enabling forum, creating topics.

This commit is contained in:
John Preston
2022-09-20 22:12:30 +04:00
parent c88140e256
commit 388fe6adfb
21 changed files with 781 additions and 49 deletions

View File

@@ -60,11 +60,11 @@ Data::ChatBotCommands::Changed MegagroupInfo::setBotCommands(
return _botCommands.update(list);
}
void MegagroupInfo::setIsForum(bool is) {
void MegagroupInfo::setIsForum(not_null<ChannelData*> that, bool is) {
if (is == (_forum != nullptr)) {
return;
} else if (is) {
_forum = std::make_unique<Data::Forum>();
_forum = std::make_unique<Data::Forum>(that->owner().history(that));
} else {
_forum = nullptr;
}
@@ -97,10 +97,6 @@ ChannelData::ChannelData(not_null<Data::Session*> owner, PeerId id)
mgInfo = nullptr;
}
}
if (change.diff & Flag::Forum) {
Assert(mgInfo != nullptr);
mgInfo->setIsForum(change.value & Flag::Forum);
}
if (change.diff & Flag::CallNotEmpty) {
if (const auto history = this->owner().historyLoaded(this)) {
history->updateChatListEntry();

View File

@@ -100,7 +100,7 @@ public:
return _botCommands;
}
void setIsForum(bool is);
void setIsForum(not_null<ChannelData*> that, bool is);
[[nodiscard]] Data::Forum *forum() const;
std::deque<not_null<UserData*>> lastParticipants;

View File

@@ -41,8 +41,7 @@ Folder::Folder(not_null<Data::Session*> owner, FolderId id)
&owner->session(),
FilterId(),
owner->maxPinnedChatsLimitValue(this, FilterId()))
, _name(tr::lng_archived_name(tr::now))
, _chatListNameSortKey(owner->nameSortKey(_name)) {
, _name(tr::lng_archived_name(tr::now)) {
indexNameParts();
session().changes().peerUpdates(
@@ -374,7 +373,8 @@ const base::flat_set<QChar> &Folder::chatListFirstLetters() const {
}
const QString &Folder::chatListNameSortKey() const {
return _chatListNameSortKey;
static const auto empty = QString();
return empty;
}
} // namespace Data

View File

@@ -21,7 +21,6 @@ class Session;
namespace Data {
class Session;
class Folder;
class Folder final : public Dialogs::Entry, public base::has_weak_ptr {
public:
@@ -31,12 +30,12 @@ public:
Folder(const Folder &) = delete;
Folder &operator=(const Folder &) = delete;
FolderId id() const;
[[nodiscard]] FolderId id() const;
void registerOne(not_null<History*> history);
void unregisterOne(not_null<History*> history);
void oneListMessageChanged(HistoryItem *from, HistoryItem *to);
not_null<Dialogs::MainList*> chatsList();
[[nodiscard]] not_null<Dialogs::MainList*> chatsList();
void applyDialog(const MTPDdialogFolder &data);
void applyPinnedUpdate(const MTPDupdateDialogPinned &data);
@@ -94,13 +93,12 @@ private:
const style::color *overrideBg,
const style::color *overrideFg) const;
FolderId _id = 0;
const FolderId _id = 0;
Dialogs::MainList _chatsList;
QString _name;
base::flat_set<QString> _nameWords;
base::flat_set<QChar> _nameFirstLetters;
QString _chatListNameSortKey;
std::vector<not_null<History*>> _lastHistories;
HistoryItem *_chatListMessage = nullptr;

View File

@@ -7,10 +7,172 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_forum.h"
namespace Data {
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_forum_topic.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "base/random.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/input_fields.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
Forum::Forum() = default;
namespace Data {
namespace {
constexpr auto kTopicsFirstLoad = 20;
constexpr auto kTopicsPerPage = 500;
} // namespace
Forum::Forum(not_null<History*> forum)
: _forum(forum)
, _topicsList(&forum->session(), FilterId(0), rpl::single(1)) {
}
Forum::~Forum() = default;
not_null<Dialogs::MainList*> Forum::topicsList() {
return &_topicsList;
}
void Forum::requestTopics() {
if (_allLoaded || _requestId) {
return;
}
const auto forum = _forum;
const auto firstLoad = !_offsetDate;
const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage;
const auto api = &forum->session().api();
_requestId = api->request(MTPchannels_GetForumTopics(
MTP_flags(0),
forum->peer->asChannel()->inputChannel,
MTPstring(), // q
MTP_int(_offsetDate),
MTP_int(_offsetId),
MTP_int(_offsetTopicId),
MTP_int(loadCount)
)).done([=](const MTPmessages_ForumTopics &result) {
if (!forum->peer->isForum()) {
return;
}
const auto &data = result.data();
const auto owner = &forum->owner();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
owner->processMessages(data.vmessages(), NewMessageType::Existing);
forum->peer->asChannel()->ptsReceived(data.vpts().v);
const auto &list = data.vtopics().v;
for (const auto &topic : list) {
const auto rootId = MsgId(topic.data().vid().v);
if (const auto i = _topics.find(rootId); i != end(_topics)) {
i->second->applyTopic(topic);
} else {
const auto raw = _topics.emplace(
rootId,
std::make_unique<ForumTopic>(forum, rootId)
).first->second.get();
raw->applyTopic(topic);
raw->addToChatList(FilterId(), topicsList());
}
}
if (list.isEmpty() || list.size() == data.vcount().v) {
_allLoaded = true;
}
if (const auto date = data.vnext_date()) {
_offsetDate = date->v;
}
_requestId = 0;
_chatsListChanges.fire({});
if (_allLoaded) {
_chatsListLoadedEvents.fire({});
}
}).fail([=](const MTP::Error &error) {
_allLoaded = true;
_requestId = 0;
}).send();
}
void Forum::topicAdded(not_null<HistoryItem*> root) {
const auto rootId = root->id;
if (const auto i = _topics.find(rootId); i != end(_topics)) {
//i->second->applyTopic(topic);
} else {
const auto raw = _topics.emplace(
rootId,
std::make_unique<ForumTopic>(_forum, rootId)
).first->second.get();
//raw->applyTopic(topic);
raw->addToChatList(FilterId(), topicsList());
_chatsListChanges.fire({});
}
}
rpl::producer<> Forum::chatsListChanges() const {
return _chatsListChanges.events();
}
rpl::producer<> Forum::chatsListLoadedEvents() const {
return _chatsListLoadedEvents.events();
}
void ShowAddForumTopic(
not_null<Window::SessionController*> controller,
not_null<ChannelData*> forum) {
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(rpl::single(u"New Topic"_q));
const auto title = box->addRow(
object_ptr<Ui::InputField>(
box,
st::defaultInputField,
rpl::single(u"Topic Title"_q))); // #TODO forum
const auto message = box->addRow(
object_ptr<Ui::InputField>(
box,
st::newGroupDescription,
Ui::InputField::Mode::MultiLine,
rpl::single(u"Message"_q))); // #TODO forum
box->setFocusCallback([=] {
title->setFocusFast();
});
box->addButton(tr::lng_create_group_create(), [=] {
if (!forum->isForum()) {
box->closeBox();
return;
} else if (title->getLastText().trimmed().isEmpty()) {
title->setFocus();
return;
} else if (message->getLastText().trimmed().isEmpty()) {
message->setFocus();
return;
}
const auto randomId = base::RandomValue<uint64>();
const auto api = &forum->session().api();
api->request(MTPchannels_CreateForumTopic(
MTP_flags(0),
forum->inputChannel,
MTP_string(title->getLastText().trimmed()),
MTPInputMedia(),
MTP_string(message->getLastText().trimmed()),
MTP_long(randomId),
MTPVector<MTPMessageEntity>(),
MTPInputPeer() // send_as
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result, randomId);
box->closeBox();
}).fail([=](const MTP::Error &error) {
api->sendMessageFail(error, forum, randomId);
}).send();
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}), Ui::LayerOption::KeepOther);
}
} // namespace Data

View File

@@ -7,15 +7,49 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "dialogs/dialogs_main_list.h"
class History;
class ChannelData;
namespace Window {
class SessionController;
} // namespace Window;
namespace Data {
class Forum final {
public:
Forum();
explicit Forum(not_null<History*> forum);
~Forum();
[[nodiscard]] not_null<Dialogs::MainList*> topicsList();
void requestTopics();
[[nodiscard]] rpl::producer<> chatsListChanges() const;
[[nodiscard]] rpl::producer<> chatsListLoadedEvents() const;
void topicAdded(not_null<HistoryItem*> root);
private:
const not_null<History*> _forum;
base::flat_map<MsgId, std::unique_ptr<ForumTopic>> _topics;
Dialogs::MainList _topicsList;
mtpRequestId _requestId = 0;
TimeId _offsetDate = 0;
MsgId _offsetId = 0;
MsgId _offsetTopicId = 0;
bool _allLoaded = false;
rpl::event_stream<> _chatsListChanges;
rpl::event_stream<> _chatsListLoadedEvents;
};
void ShowAddForumTopic(
not_null<Window::SessionController*> controller,
not_null<ChannelData*> forum);
} // namespace Data

View File

@@ -0,0 +1,340 @@
/*
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_topic.h"
#include "data/data_channel.h"
#include "data/data_forum.h"
#include "data/data_session.h"
#include "dialogs/dialogs_main_list.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "history/history.h"
#include "history/history_item.h"
namespace Data {
ForumTopic::ForumTopic(not_null<History*> forum, MsgId rootId)
: Entry(&forum->owner(), Type::ForumTopic)
, _forum(forum)
, _list(forum->peer->asChannel()->forum()->topicsList())
, _rootId(rootId) {
}
not_null<History*> ForumTopic::forum() const {
return _forum;
}
MsgId ForumTopic::rootId() const {
return _rootId;
}
void ForumTopic::applyTopic(const MTPForumTopic &topic) {
Expects(_rootId == topic.data().vid().v);
const auto &data = topic.data();
const auto title = qs(data.vtitle());
if (_title != title) {
_title = title;
++_titleVersion;
indexTitleParts();
updateChatListEntry();
}
const auto pinned = _list->pinned();
if (data.is_pinned()) {
pinned->addPinned(Dialogs::Key(this));
} else {
pinned->setPinned(Dialogs::Key(this), false);
}
applyTopicFields(
data.vunread_count().v,
data.vread_inbox_max_id().v,
data.vread_outbox_max_id().v);
applyTopicTopMessage(data.vtop_message().v);
//setUnreadMark(data.is_unread_mark());
}
void ForumTopic::indexTitleParts() {
_titleWords.clear();
_titleFirstLetters.clear();
auto toIndexList = QStringList();
auto appendToIndex = [&](const QString &value) {
if (!value.isEmpty()) {
toIndexList.push_back(TextUtilities::RemoveAccents(value));
}
};
appendToIndex(_title);
const auto appendTranslit = !toIndexList.isEmpty()
&& cRussianLetters().match(toIndexList.front()).hasMatch();
if (appendTranslit) {
appendToIndex(translitRusEng(toIndexList.front()));
}
auto toIndex = toIndexList.join(' ');
toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);
const auto namesList = TextUtilities::PrepareSearchWords(toIndex);
for (const auto &name : namesList) {
_titleWords.insert(name);
_titleFirstLetters.insert(name[0]);
}
}
int ForumTopic::chatListNameVersion() const {
return _titleVersion;
}
void ForumTopic::applyTopicFields(
int unreadCount,
MsgId maxInboxRead,
MsgId maxOutboxRead) {
if (maxInboxRead + 1 >= _inboxReadBefore.value_or(1)) {
setUnreadCount(unreadCount);
setInboxReadTill(maxInboxRead);
}
setOutboxReadTill(maxOutboxRead);
}
void ForumTopic::applyTopicTopMessage(MsgId topMessageId) {
if (topMessageId) {
const auto itemId = FullMsgId(_forum->peer->id, topMessageId);
if (const auto item = owner().message(itemId)) {
setLastServerMessage(item);
} else {
setLastServerMessage(nullptr);
}
} else {
setLastServerMessage(nullptr);
}
}
void ForumTopic::setLastServerMessage(HistoryItem *item) {
_lastServerMessage = item;
if (_lastMessage
&& *_lastMessage
&& !(*_lastMessage)->isRegular()
&& (!item || (*_lastMessage)->date() > item->date())) {
return;
}
setLastMessage(item);
}
void ForumTopic::setLastMessage(HistoryItem *item) {
if (_lastMessage && *_lastMessage == item) {
return;
}
_lastMessage = item;
if (!item || item->isRegular()) {
_lastServerMessage = item;
}
setChatListMessage(item);
}
void ForumTopic::setChatListMessage(HistoryItem *item) {
if (_chatListMessage && *_chatListMessage == item) {
return;
}
const auto was = _chatListMessage.value_or(nullptr);
if (item) {
if (item->isSponsored()) {
return;
}
if (_chatListMessage
&& *_chatListMessage
&& !(*_chatListMessage)->isRegular()
&& (*_chatListMessage)->date() > item->date()) {
return;
}
_chatListMessage = item;
setChatListTimeId(item->date());
#if 0 // #TODO forum
// If we have a single message from a group, request the full album.
if (hasOrphanMediaGroupPart()
&& !item->toPreview({
.hideSender = true,
.hideCaption = true }).images.empty()) {
owner().histories().requestGroupAround(item);
}
#endif
} else if (!_chatListMessage || *_chatListMessage) {
_chatListMessage = nullptr;
updateChatListEntry();
}
}
void ForumTopic::setInboxReadTill(MsgId upTo) {
if (_inboxReadBefore) {
accumulate_max(*_inboxReadBefore, upTo + 1);
} else {
_inboxReadBefore = upTo + 1;
}
}
void ForumTopic::setOutboxReadTill(MsgId upTo) {
if (_outboxReadBefore) {
accumulate_max(*_outboxReadBefore, upTo + 1);
} else {
_outboxReadBefore = upTo + 1;
}
}
void ForumTopic::loadUserpic() {
}
void ForumTopic::paintUserpic(
Painter &p,
std::shared_ptr<Data::CloudImageView> &view,
int x,
int y,
int size) const {
// #TODO forum
}
void ForumTopic::requestChatListMessage() {
if (!chatListMessageKnown()) {
// #TODO forum
}
}
TimeId ForumTopic::adjustedChatListTimeId() const {
const auto result = chatListTimeId();
#if 0 // #TODO forum
if (const auto draft = cloudDraft()) {
if (!Data::draftIsNull(draft) && !session().supportMode()) {
return std::max(result, draft->date);
}
}
#endif
return result;
}
int ForumTopic::fixedOnTopIndex() const {
return kArchiveFixOnTopIndex;
}
bool ForumTopic::shouldBeInChatList() const {
return isPinnedDialog(FilterId())
|| !lastMessageKnown()
|| (lastMessage() != nullptr);
}
HistoryItem *ForumTopic::lastMessage() const {
return _lastMessage.value_or(nullptr);
}
bool ForumTopic::lastMessageKnown() const {
return _lastMessage.has_value();
}
HistoryItem *ForumTopic::lastServerMessage() const {
return _lastServerMessage.value_or(nullptr);
}
bool ForumTopic::lastServerMessageKnown() const {
return _lastServerMessage.has_value();
}
int ForumTopic::unreadCount() const {
return _unreadCount ? *_unreadCount : 0;
}
int ForumTopic::unreadCountForBadge() const {
const auto result = unreadCount();
return (!result && unreadMark()) ? 1 : result;
}
bool ForumTopic::unreadCountKnown() const {
return _unreadCount.has_value();
}
void ForumTopic::setUnreadCount(int newUnreadCount) {
if (_unreadCount == newUnreadCount) {
return;
}
const auto wasForBadge = (unreadCountForBadge() > 0);
const auto notifier = unreadStateChangeNotifier(true);
_unreadCount = newUnreadCount;
}
void ForumTopic::setUnreadMark(bool unread) {
if (_unreadMark == unread) {
return;
}
const auto noUnreadMessages = !unreadCount();
const auto refresher = gsl::finally([&] {
if (inChatList() && noUnreadMessages) {
updateChatListEntry();
}
});
const auto notifier = unreadStateChangeNotifier(noUnreadMessages);
_unreadMark = unread;
}
bool ForumTopic::unreadMark() const {
return _unreadMark;
}
int ForumTopic::chatListUnreadCount() const {
const auto state = chatListUnreadState();
return state.marks
+ (Core::App().settings().countUnreadMessages()
? state.messages
: state.chats);
}
Dialogs::UnreadState ForumTopic::chatListUnreadState() const {
auto result = Dialogs::UnreadState();
const auto count = _unreadCount.value_or(0);
const auto mark = !count && _unreadMark;
const auto muted = _forum->mute();
result.messages = count;
result.messagesMuted = muted ? count : 0;
result.chats = count ? 1 : 0;
result.chatsMuted = (count && muted) ? 1 : 0;
result.marks = mark ? 1 : 0;
result.marksMuted = (mark && muted) ? 1 : 0;
result.known = _unreadCount.has_value();
return result;
}
bool ForumTopic::chatListUnreadMark() const {
return false;
}
bool ForumTopic::chatListMutedBadge() const {
return true;
}
HistoryItem *ForumTopic::chatListMessage() const {
return _lastMessage.value_or(nullptr);
}
bool ForumTopic::chatListMessageKnown() const {
return _lastMessage.has_value();
}
const QString &ForumTopic::chatListName() const {
return _title;
}
const base::flat_set<QString> &ForumTopic::chatListNameWords() const {
return _titleWords;
}
const base::flat_set<QChar> &ForumTopic::chatListFirstLetters() const {
return _titleFirstLetters;
}
const QString &ForumTopic::chatListNameSortKey() const {
static const auto empty = QString();
return empty;
}
} // namespace Data

View File

@@ -0,0 +1,115 @@
/*
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 "dialogs/dialogs_entry.h"
class ChannelData;
namespace Dialogs {
class MainList;
} // namespace Dialogs
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Session;
class ForumTopic final : public Dialogs::Entry {
public:
ForumTopic(not_null<History*> forum, MsgId rootId);
ForumTopic(const ForumTopic &) = delete;
ForumTopic &operator=(const ForumTopic &) = delete;
[[nodiscard]] not_null<History*> forum() const;
[[nodiscard]] MsgId rootId() const;
void applyTopic(const MTPForumTopic &topic);
TimeId adjustedChatListTimeId() const override;
int fixedOnTopIndex() const override;
bool shouldBeInChatList() const override;
int chatListUnreadCount() const override;
bool chatListUnreadMark() const override;
bool chatListMutedBadge() const override;
Dialogs::UnreadState chatListUnreadState() const override;
HistoryItem *chatListMessage() const override;
bool chatListMessageKnown() const override;
void requestChatListMessage() override;
const QString &chatListName() const override;
const QString &chatListNameSortKey() const override;
const base::flat_set<QString> &chatListNameWords() const override;
const base::flat_set<QChar> &chatListFirstLetters() const override;
[[nodiscard]] HistoryItem *lastMessage() const;
[[nodiscard]] HistoryItem *lastServerMessage() const;
[[nodiscard]] bool lastMessageKnown() const;
[[nodiscard]] bool lastServerMessageKnown() const;
void loadUserpic() override;
void paintUserpic(
Painter &p,
std::shared_ptr<Data::CloudImageView> &view,
int x,
int y,
int size) const override;
[[nodiscard]] int unreadCount() const;
[[nodiscard]] bool unreadCountKnown() const;
[[nodiscard]] int unreadCountForBadge() const; // unreadCount || unreadMark ? 1 : 0.
void setUnreadCount(int newUnreadCount);
void setUnreadMark(bool unread);
[[nodiscard]] bool unreadMark() const;
private:
void indexTitleParts();
void applyTopicTopMessage(MsgId topMessageId);
void applyTopicFields(
int unreadCount,
MsgId maxInboxRead,
MsgId maxOutboxRead);
void applyChatListMessage(HistoryItem *item);
void setLastMessage(HistoryItem *item);
void setLastServerMessage(HistoryItem *item);
void setChatListMessage(HistoryItem *item);
void setInboxReadTill(MsgId upTo);
void setOutboxReadTill(MsgId upTo);
int chatListNameVersion() const override;
const not_null<History*> _forum;
const not_null<Dialogs::MainList*> _list;
const MsgId _rootId = 0;
QString _title;
base::flat_set<QString> _titleWords;
base::flat_set<QChar> _titleFirstLetters;
int _titleVersion = 0;
std::optional<MsgId> _inboxReadBefore;
std::optional<MsgId> _outboxReadBefore;
std::optional<int> _unreadCount;
std::optional<HistoryItem*> _lastMessage;
std::optional<HistoryItem*> _lastServerMessage;
std::optional<HistoryItem*> _chatListMessage;
bool _unreadMark = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View File

@@ -805,7 +805,12 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| ((data.is_forum() && data.is_megagroup())
? Flag::Forum
: Flag());
const auto wasForum = channel->isForum();
channel->setFlags((channel->flags() & ~flagsMask) | flagsSet);
if (const auto nowForum = channel->isForum(); nowForum != wasForum) {
Assert(channel->mgInfo != nullptr);
channel->mgInfo->setIsForum(channel, nowForum);
}
channel->setName(
qs(data.vtitle()),

View File

@@ -239,52 +239,53 @@ enum class MessageFlag : uint64 {
MentionsMe = (1ULL << 15),
IsOrWasScheduled = (1ULL << 16),
NoForwards = (1ULL << 17),
TopicStart = (1ULL << 18),
// Needs to return back to inline mode.
HasSwitchInlineButton = (1ULL << 18),
HasSwitchInlineButton = (1ULL << 19),
// For "shared links" indexing.
HasTextLinks = (1ULL << 19),
HasTextLinks = (1ULL << 20),
// Group / channel create or migrate service message.
IsGroupEssential = (1ULL << 20),
IsGroupEssential = (1ULL << 21),
// Edited media is generated on the client
// and should not update media from server.
IsLocalUpdateMedia = (1ULL << 21),
IsLocalUpdateMedia = (1ULL << 22),
// Sent from inline bot, need to re-set media when sent.
FromInlineBot = (1ULL << 22),
FromInlineBot = (1ULL << 23),
// Generated on the client side and should be unread.
ClientSideUnread = (1ULL << 23),
ClientSideUnread = (1ULL << 24),
// In a supergroup.
HasAdminBadge = (1ULL << 24),
HasAdminBadge = (1ULL << 25),
// Outgoing message that is being sent.
BeingSent = (1ULL << 25),
BeingSent = (1ULL << 26),
// Outgoing message and failed to be sent.
SendingFailed = (1ULL << 26),
SendingFailed = (1ULL << 27),
// No media and only a several emoji or an only custom emoji text.
SpecialOnlyEmoji = (1ULL << 27),
SpecialOnlyEmoji = (1ULL << 28),
// Message existing in the message history.
HistoryEntry = (1ULL << 28),
HistoryEntry = (1ULL << 29),
// Local message, not existing on the server.
Local = (1ULL << 29),
Local = (1ULL << 30),
// Fake message for some UI element.
FakeHistoryItem = (1ULL << 30),
FakeHistoryItem = (1ULL << 31),
// Contact sign-up message, notification should be skipped for Silent.
IsContactSignUp = (1ULL << 31),
IsContactSignUp = (1ULL << 32),
// Optimization for item text custom emoji repainting.
CustomEmojiRepainting = (1ULL << 32),
CustomEmojiRepainting = (1ULL << 33),
};
inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>;