data_forum_topic.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "data/data_forum_topic.h"
  8. #include "data/data_channel.h"
  9. #include "data/data_changes.h"
  10. #include "data/data_forum.h"
  11. #include "data/data_histories.h"
  12. #include "data/data_replies_list.h"
  13. #include "data/data_send_action.h"
  14. #include "data/notify/data_notify_settings.h"
  15. #include "data/data_session.h"
  16. #include "data/stickers/data_custom_emoji.h"
  17. #include "dialogs/dialogs_main_list.h"
  18. #include "dialogs/ui/dialogs_layout.h"
  19. #include "core/application.h"
  20. #include "core/core_settings.h"
  21. #include "apiwrap.h"
  22. #include "api/api_unread_things.h"
  23. #include "history/history.h"
  24. #include "history/history_item.h"
  25. #include "history/history_unread_things.h"
  26. #include "history/view/history_view_item_preview.h"
  27. #include "history/view/history_view_replies_section.h"
  28. #include "main/main_session.h"
  29. #include "base/unixtime.h"
  30. #include "ui/painter.h"
  31. #include "ui/color_int_conversion.h"
  32. #include "ui/text/text_custom_emoji.h"
  33. #include "styles/style_dialogs.h"
  34. #include "styles/style_chat_helpers.h"
  35. #include <QtSvg/QSvgRenderer>
  36. namespace Data {
  37. namespace {
  38. using UpdateFlag = TopicUpdate::Flag;
  39. constexpr auto kUserpicLoopsCount = 1;
  40. } // namespace
  41. const base::flat_map<int32, QString> &ForumTopicIcons() {
  42. static const auto Result = base::flat_map<int32, QString>{
  43. { 0x6FB9F0, u"blue"_q },
  44. { 0xFFD67E, u"yellow"_q },
  45. { 0xCB86DB, u"violet"_q },
  46. { 0x8EEE98, u"green"_q },
  47. { 0xFF93B2, u"rose"_q },
  48. { 0xFB6F5F, u"red"_q },
  49. };
  50. return Result;
  51. }
  52. const std::vector<int32> &ForumTopicColorIds() {
  53. static const auto Result = ForumTopicIcons(
  54. ) | ranges::views::transform([](const auto &pair) {
  55. return pair.first;
  56. }) | ranges::to_vector;
  57. return Result;
  58. }
  59. const QString &ForumTopicDefaultIcon() {
  60. static const auto Result = u"gray"_q;
  61. return Result;
  62. }
  63. const QString &ForumTopicIcon(int32 colorId) {
  64. const auto &icons = ForumTopicIcons();
  65. const auto i = icons.find(colorId);
  66. return (i != end(icons)) ? i->second : ForumTopicDefaultIcon();
  67. }
  68. QString ForumTopicIconPath(const QString &name) {
  69. return u":/gui/topic_icons/%1.svg"_q.arg(name);
  70. }
  71. QImage ForumTopicIconBackground(int32 colorId, int size) {
  72. const auto ratio = style::DevicePixelRatio();
  73. auto svg = QSvgRenderer(ForumTopicIconPath(ForumTopicIcon(colorId)));
  74. auto result = QImage(
  75. QSize(size, size) * ratio,
  76. QImage::Format_ARGB32_Premultiplied);
  77. result.setDevicePixelRatio(ratio);
  78. result.fill(Qt::transparent);
  79. auto p = QPainter(&result);
  80. svg.render(&p, QRect(0, 0, size, size));
  81. p.end();
  82. return result;
  83. }
  84. QString ExtractNonEmojiLetter(const QString &title) {
  85. const auto begin = title.data();
  86. const auto end = begin + title.size();
  87. for (auto ch = begin; ch != end;) {
  88. auto length = 0;
  89. if (Ui::Emoji::Find(ch, end, &length)) {
  90. ch += length;
  91. continue;
  92. }
  93. uint ucs4 = ch->unicode();
  94. length = 1;
  95. if (QChar::isHighSurrogate(ucs4) && ch + 1 != end) {
  96. ushort low = ch[1].unicode();
  97. if (QChar::isLowSurrogate(low)) {
  98. ucs4 = QChar::surrogateToUcs4(ucs4, low);
  99. length = 2;
  100. }
  101. }
  102. if (!QChar::isLetterOrNumber(ucs4)) {
  103. ch += length;
  104. continue;
  105. }
  106. return QString(ch, length);
  107. }
  108. return QString();
  109. }
  110. QImage ForumTopicIconFrame(
  111. int32 colorId,
  112. const QString &title,
  113. const style::ForumTopicIcon &st) {
  114. auto background = ForumTopicIconBackground(colorId, st.size);
  115. if (const auto one = ExtractNonEmojiLetter(title); !one.isEmpty()) {
  116. auto p = QPainter(&background);
  117. p.setPen(Qt::white);
  118. p.setFont(st.font);
  119. p.drawText(
  120. QRect(0, st.textTop, st.size, st.font->height * 2),
  121. one,
  122. style::al_top);
  123. }
  124. return background;
  125. }
  126. QImage ForumTopicGeneralIconFrame(int size, const QColor &color) {
  127. const auto ratio = style::DevicePixelRatio();
  128. auto svg = QSvgRenderer(ForumTopicIconPath(u"general"_q));
  129. auto result = QImage(
  130. QSize(size, size) * ratio,
  131. QImage::Format_ARGB32_Premultiplied);
  132. result.setDevicePixelRatio(ratio);
  133. result.fill(Qt::transparent);
  134. const auto use = size * 0.8;
  135. const auto skip = size * 0.1;
  136. auto p = QPainter(&result);
  137. svg.render(&p, QRectF(skip, 0, use, use));
  138. p.end();
  139. return style::colorizeImage(result, color);
  140. }
  141. TextWithEntities ForumTopicIconWithTitle(
  142. MsgId rootId,
  143. DocumentId iconId,
  144. const QString &title) {
  145. const auto wrapped = st::wrap_rtl(title);
  146. return (rootId == ForumTopic::kGeneralId)
  147. ? TextWithEntities{ u"# "_q + wrapped }
  148. : iconId
  149. ? Data::SingleCustomEmoji(iconId).append(' ').append(wrapped)
  150. : TextWithEntities{ wrapped };
  151. }
  152. QString ForumGeneralIconTitle() {
  153. return QChar(0) + u"general"_q;
  154. }
  155. bool IsForumGeneralIconTitle(const QString &title) {
  156. return !title.isEmpty() && !title[0].unicode();
  157. }
  158. int32 ForumGeneralIconColor(const QColor &color) {
  159. return int32(uint32(color.red()) << 16
  160. | uint32(color.green()) << 8
  161. | uint32(color.blue())
  162. | (uint32(color.alpha() == 255 ? 0 : color.alpha()) << 24));
  163. }
  164. QColor ParseForumGeneralIconColor(int32 value) {
  165. const auto alpha = uint32(value) >> 24;
  166. return QColor(
  167. (value >> 16) & 0xFF,
  168. (value >> 8) & 0xFF,
  169. value & 0xFF,
  170. alpha ? alpha : 255);
  171. }
  172. QString TopicIconEmojiEntity(TopicIconDescriptor descriptor) {
  173. return IsForumGeneralIconTitle(descriptor.title)
  174. ? u"topic_general:"_q + QString::number(uint32(descriptor.colorId))
  175. : (u"topic_icon:"_q
  176. + QString::number(uint32(descriptor.colorId))
  177. + ' '
  178. + ExtractNonEmojiLetter(descriptor.title));
  179. }
  180. TopicIconDescriptor ParseTopicIconEmojiEntity(QStringView entity) {
  181. if (!entity.startsWith(u"topic_")) {
  182. return {};
  183. }
  184. const auto general = u"topic_general:"_q;
  185. const auto normal = u"topic_icon:"_q;
  186. if (entity.startsWith(general)) {
  187. return {
  188. .title = ForumGeneralIconTitle(),
  189. .colorId = int32(entity.mid(general.size()).toUInt()),
  190. };
  191. } else if (entity.startsWith(normal)) {
  192. const auto parts = entity.mid(normal.size()).split(' ');
  193. if (parts.size() == 2) {
  194. return {
  195. .title = parts[1].toString(),
  196. .colorId = int32(parts[0].toUInt()),
  197. };
  198. }
  199. }
  200. return {};
  201. }
  202. ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
  203. : Thread(&forum->history()->owner(), Type::ForumTopic)
  204. , _forum(forum)
  205. , _list(_forum->topicsList())
  206. , _replies(std::make_shared<RepliesList>(history(), rootId, this))
  207. , _sendActionPainter(owner().sendActionManager().repliesPainter(
  208. history(),
  209. rootId))
  210. , _rootId(rootId)
  211. , _lastKnownServerMessageId(rootId)
  212. , _creatorId(creating() ? forum->session().userPeerId() : 0)
  213. , _creationDate(creating() ? base::unixtime::now() : 0)
  214. , _flags(creating() ? Flag::My : Flag()) {
  215. Thread::setMuted(owner().notifySettings().isMuted(this));
  216. _sendActionPainter->setTopic(this);
  217. subscribeToUnreadChanges();
  218. if (isGeneral()) {
  219. style::PaletteChanged(
  220. ) | rpl::start_with_next([=] {
  221. _defaultIcon = QImage();
  222. }, _lifetime);
  223. }
  224. }
  225. ForumTopic::~ForumTopic() {
  226. _sendActionPainter->setTopic(nullptr);
  227. session().api().unreadThings().cancelRequests(this);
  228. }
  229. std::shared_ptr<Data::RepliesList> ForumTopic::replies() const {
  230. return _replies;
  231. }
  232. not_null<ChannelData*> ForumTopic::channel() const {
  233. return _forum->channel();
  234. }
  235. not_null<History*> ForumTopic::history() const {
  236. return _forum->history();
  237. }
  238. not_null<Forum*> ForumTopic::forum() const {
  239. return _forum;
  240. }
  241. rpl::producer<> ForumTopic::destroyed() const {
  242. using namespace rpl::mappers;
  243. return rpl::merge(
  244. _forum->destroyed(),
  245. _forum->topicDestroyed() | rpl::filter(_1 == this) | rpl::to_empty);
  246. }
  247. MsgId ForumTopic::rootId() const {
  248. return _rootId;
  249. }
  250. PeerId ForumTopic::creatorId() const {
  251. return _creatorId;
  252. }
  253. TimeId ForumTopic::creationDate() const {
  254. return _creationDate;
  255. }
  256. not_null<HistoryView::ListMemento*> ForumTopic::listMemento() {
  257. if (!_listMemento) {
  258. _listMemento = std::make_unique<HistoryView::ListMemento>();
  259. }
  260. return _listMemento.get();
  261. }
  262. bool ForumTopic::my() const {
  263. return (_flags & Flag::My);
  264. }
  265. bool ForumTopic::canEdit() const {
  266. return my() || channel()->canManageTopics();
  267. }
  268. bool ForumTopic::canDelete() const {
  269. if (creating() || isGeneral()) {
  270. return false;
  271. } else if (channel()->canDeleteMessages()) {
  272. return true;
  273. }
  274. return my() && replies()->canDeleteMyTopic();
  275. }
  276. bool ForumTopic::canToggleClosed() const {
  277. return !creating() && canEdit();
  278. }
  279. bool ForumTopic::canTogglePinned() const {
  280. return !creating() && channel()->canManageTopics();
  281. }
  282. bool ForumTopic::creating() const {
  283. return _forum->creating(_rootId);
  284. }
  285. void ForumTopic::discard() {
  286. Expects(creating());
  287. _forum->discardCreatingId(_rootId);
  288. }
  289. void ForumTopic::setRealRootId(MsgId realId) {
  290. if (_rootId != realId) {
  291. _rootId = realId;
  292. _lastKnownServerMessageId = realId;
  293. _replies = std::make_shared<RepliesList>(history(), _rootId);
  294. if (_sendActionPainter) {
  295. _sendActionPainter->setTopic(nullptr);
  296. }
  297. _sendActionPainter = owner().sendActionManager().repliesPainter(
  298. history(),
  299. _rootId);
  300. _sendActionPainter->setTopic(this);
  301. subscribeToUnreadChanges();
  302. }
  303. }
  304. void ForumTopic::subscribeToUnreadChanges() {
  305. _replies->unreadCountValue(
  306. ) | rpl::map([=](std::optional<int> value) {
  307. return value ? _replies->displayedUnreadCount() : value;
  308. }) | rpl::distinct_until_changed(
  309. ) | rpl::combine_previous(
  310. ) | rpl::filter([=] {
  311. return inChatList();
  312. }) | rpl::start_with_next([=](
  313. std::optional<int> previous,
  314. std::optional<int> now) {
  315. if (previous.value_or(0) != now.value_or(0)) {
  316. _forum->recentTopicsInvalidate(this);
  317. }
  318. notifyUnreadStateChange(unreadStateFor(
  319. previous.value_or(0),
  320. previous.has_value()));
  321. }, _lifetime);
  322. }
  323. void ForumTopic::readTillEnd() {
  324. _replies->readTill(_lastKnownServerMessageId);
  325. }
  326. void ForumTopic::applyTopic(const MTPDforumTopic &data) {
  327. Expects(_rootId == data.vid().v);
  328. const auto min = data.is_short();
  329. applyCreator(peerFromMTP(data.vfrom_id()));
  330. applyCreationDate(data.vdate().v);
  331. applyTitle(qs(data.vtitle()));
  332. if (const auto iconId = data.vicon_emoji_id()) {
  333. applyIconId(iconId->v);
  334. } else {
  335. applyIconId(0);
  336. }
  337. applyColorId(data.vicon_color().v);
  338. applyIsMy(data.is_my());
  339. setClosed(data.is_closed());
  340. if (!min) {
  341. owner().setPinnedFromEntryList(this, data.is_pinned());
  342. owner().notifySettings().apply(this, data.vnotify_settings());
  343. if (const auto draft = data.vdraft()) {
  344. draft->match([&](const MTPDdraftMessage &data) {
  345. Data::ApplyPeerCloudDraft(
  346. &session(),
  347. channel()->id,
  348. _rootId,
  349. data);
  350. }, [](const MTPDdraftMessageEmpty&) {});
  351. }
  352. _replies->setInboxReadTill(
  353. data.vread_inbox_max_id().v,
  354. data.vunread_count().v);
  355. _replies->setOutboxReadTill(data.vread_outbox_max_id().v);
  356. applyTopicTopMessage(data.vtop_message().v);
  357. unreadMentions().setCount(data.vunread_mentions_count().v);
  358. unreadReactions().setCount(data.vunread_reactions_count().v);
  359. }
  360. }
  361. void ForumTopic::applyCreator(PeerId creatorId) {
  362. if (_creatorId != creatorId) {
  363. _creatorId = creatorId;
  364. session().changes().topicUpdated(this, UpdateFlag::Creator);
  365. }
  366. }
  367. void ForumTopic::applyCreationDate(TimeId date) {
  368. _creationDate = date;
  369. }
  370. void ForumTopic::applyIsMy(bool my) {
  371. if (my != this->my()) {
  372. if (my) {
  373. _flags |= Flag::My;
  374. } else {
  375. _flags &= ~Flag::My;
  376. }
  377. }
  378. }
  379. bool ForumTopic::closed() const {
  380. return _flags & Flag::Closed;
  381. }
  382. void ForumTopic::setClosed(bool closed) {
  383. if (this->closed() == closed) {
  384. return;
  385. } else if (closed) {
  386. _flags |= Flag::Closed;
  387. } else {
  388. _flags &= ~Flag::Closed;
  389. }
  390. session().changes().topicUpdated(this, UpdateFlag::Closed);
  391. }
  392. void ForumTopic::setClosedAndSave(bool closed) {
  393. setClosed(closed);
  394. const auto api = &session().api();
  395. const auto weak = base::make_weak(this);
  396. api->request(MTPchannels_EditForumTopic(
  397. MTP_flags(MTPchannels_EditForumTopic::Flag::f_closed),
  398. channel()->inputChannel,
  399. MTP_int(_rootId),
  400. MTPstring(), // title
  401. MTPlong(), // icon_emoji_id
  402. MTP_bool(closed),
  403. MTPBool() // hidden
  404. )).done([=](const MTPUpdates &result) {
  405. api->applyUpdates(result);
  406. }).fail([=](const MTP::Error &error) {
  407. if (error.type() != u"TOPIC_NOT_MODIFIED") {
  408. if (const auto topic = weak.get()) {
  409. topic->forum()->requestTopic(topic->rootId());
  410. }
  411. }
  412. }).send();
  413. }
  414. bool ForumTopic::hidden() const {
  415. return (_flags & Flag::Hidden);
  416. }
  417. void ForumTopic::setHidden(bool hidden) {
  418. if (hidden) {
  419. _flags |= Flag::Hidden;
  420. } else {
  421. _flags &= ~Flag::Hidden;
  422. }
  423. }
  424. void ForumTopic::indexTitleParts() {
  425. _titleWords.clear();
  426. _titleFirstLetters.clear();
  427. auto toIndexList = QStringList();
  428. auto appendToIndex = [&](const QString &value) {
  429. if (!value.isEmpty()) {
  430. toIndexList.push_back(TextUtilities::RemoveAccents(value));
  431. }
  432. };
  433. appendToIndex(_title);
  434. const auto appendTranslit = !toIndexList.isEmpty()
  435. && cRussianLetters().match(toIndexList.front()).hasMatch();
  436. if (appendTranslit) {
  437. appendToIndex(translitRusEng(toIndexList.front()));
  438. }
  439. auto toIndex = toIndexList.join(' ');
  440. toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);
  441. const auto namesList = TextUtilities::PrepareSearchWords(toIndex);
  442. for (const auto &name : namesList) {
  443. _titleWords.insert(name);
  444. _titleFirstLetters.insert(name[0]);
  445. }
  446. }
  447. int ForumTopic::chatListNameVersion() const {
  448. return _titleVersion;
  449. }
  450. void ForumTopic::applyTopicTopMessage(MsgId topMessageId) {
  451. if (topMessageId) {
  452. growLastKnownServerMessageId(topMessageId);
  453. const auto itemId = FullMsgId(channel()->id, topMessageId);
  454. if (const auto item = owner().message(itemId)) {
  455. setLastServerMessage(item);
  456. resolveChatListMessageGroup();
  457. } else {
  458. setLastServerMessage(nullptr);
  459. }
  460. } else {
  461. setLastServerMessage(nullptr);
  462. }
  463. }
  464. void ForumTopic::resolveChatListMessageGroup() {
  465. if (!(_flags & Flag::ResolveChatListMessage)) {
  466. return;
  467. }
  468. // If we set a single album part, request the full album.
  469. const auto item = _lastServerMessage.value_or(nullptr);
  470. if (item && item->groupId() != MessageGroupId()) {
  471. if (owner().groups().isGroupOfOne(item)
  472. && !item->toPreview({
  473. .hideSender = true,
  474. .hideCaption = true }).images.empty()
  475. && _requestedGroups.emplace(item->fullId()).second) {
  476. owner().histories().requestGroupAround(item);
  477. }
  478. }
  479. }
  480. void ForumTopic::growLastKnownServerMessageId(MsgId id) {
  481. _lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id);
  482. }
  483. void ForumTopic::setLastServerMessage(HistoryItem *item) {
  484. if (item) {
  485. growLastKnownServerMessageId(item->id);
  486. }
  487. _lastServerMessage = item;
  488. if (_lastMessage
  489. && *_lastMessage
  490. && !(*_lastMessage)->isRegular()
  491. && (!item
  492. || (*_lastMessage)->date() > item->date()
  493. || (*_lastMessage)->isSending())) {
  494. return;
  495. }
  496. setLastMessage(item);
  497. }
  498. void ForumTopic::setLastMessage(HistoryItem *item) {
  499. if (_lastMessage && *_lastMessage == item) {
  500. return;
  501. }
  502. _lastMessage = item;
  503. if (!item || item->isRegular()) {
  504. _lastServerMessage = item;
  505. if (item) {
  506. growLastKnownServerMessageId(item->id);
  507. }
  508. }
  509. setChatListMessage(item);
  510. }
  511. void ForumTopic::setChatListMessage(HistoryItem *item) {
  512. if (_chatListMessage && *_chatListMessage == item) {
  513. return;
  514. }
  515. const auto was = _chatListMessage.value_or(nullptr);
  516. if (item) {
  517. if (item->isSponsored()) {
  518. return;
  519. }
  520. if (_chatListMessage
  521. && *_chatListMessage
  522. && !(*_chatListMessage)->isRegular()
  523. && (*_chatListMessage)->date() > item->date()) {
  524. return;
  525. }
  526. _chatListMessage = item;
  527. setChatListTimeId(item->date());
  528. } else if (!_chatListMessage || *_chatListMessage) {
  529. _chatListMessage = nullptr;
  530. updateChatListEntry();
  531. }
  532. _forum->listMessageChanged(was, item);
  533. }
  534. void ForumTopic::chatListPreloadData() {
  535. if (_icon) {
  536. [[maybe_unused]] const auto preload = _icon->ready();
  537. }
  538. allowChatListMessageResolve();
  539. }
  540. void ForumTopic::paintUserpic(
  541. Painter &p,
  542. Ui::PeerUserpicView &view,
  543. const Dialogs::Ui::PaintContext &context) const {
  544. const auto &st = context.st;
  545. auto position = QPoint(st->padding.left(), st->padding.top());
  546. if (_icon) {
  547. if (context.narrow) {
  548. const auto ratio = style::DevicePixelRatio();
  549. const auto tag = Data::CustomEmojiManager::SizeTag::Normal;
  550. const auto size = Data::FrameSizeFromTag(tag) / ratio;
  551. position = QPoint(
  552. (context.width - size) / 2,
  553. (st->height - size) / 2);
  554. }
  555. _icon->paint(p, {
  556. .textColor = (context.active
  557. ? st::dialogsNameFgActive
  558. : context.selected
  559. ? st::dialogsNameFgOver
  560. : st::dialogsNameFg)->c,
  561. .now = context.now,
  562. .position = position,
  563. .paused = context.paused,
  564. });
  565. } else {
  566. if (isGeneral()) {
  567. validateGeneralIcon(context);
  568. } else {
  569. validateDefaultIcon();
  570. }
  571. const auto size = st::defaultForumTopicIcon.size;
  572. if (context.narrow) {
  573. position = QPoint(
  574. (context.width - size) / 2,
  575. (st->height - size) / 2);
  576. } else {
  577. const auto esize = st::emojiSize;
  578. const auto shift = (esize - size) / 2;
  579. position += st::forumTopicIconPosition + QPoint(shift, 0);
  580. }
  581. p.drawImage(position, _defaultIcon);
  582. }
  583. }
  584. void ForumTopic::clearUserpicLoops() {
  585. if (_icon) {
  586. _icon->unload();
  587. }
  588. }
  589. void ForumTopic::validateDefaultIcon() const {
  590. if (!_defaultIcon.isNull()) {
  591. return;
  592. }
  593. _defaultIcon = ForumTopicIconFrame(
  594. _colorId,
  595. _title,
  596. st::defaultForumTopicIcon);
  597. }
  598. void ForumTopic::validateGeneralIcon(
  599. const Dialogs::Ui::PaintContext &context) const {
  600. const auto mask = Flag::GeneralIconActive | Flag::GeneralIconSelected;
  601. const auto flags = context.active
  602. ? Flag::GeneralIconActive
  603. : context.selected
  604. ? Flag::GeneralIconSelected
  605. : Flag(0);
  606. if (!_defaultIcon.isNull() && ((_flags & mask) == flags)) {
  607. return;
  608. }
  609. const auto size = st::defaultForumTopicIcon.size;
  610. const auto &color = context.active
  611. ? st::dialogsTextFgActive
  612. : context.selected
  613. ? st::dialogsTextFgOver
  614. : st::dialogsTextFg;
  615. _defaultIcon = ForumTopicGeneralIconFrame(size, color->c);
  616. _flags = (_flags & ~mask) | flags;
  617. }
  618. void ForumTopic::requestChatListMessage() {
  619. if (!chatListMessageKnown() && !forum()->creating(_rootId)) {
  620. forum()->requestTopic(_rootId);
  621. }
  622. }
  623. TimeId ForumTopic::adjustedChatListTimeId() const {
  624. const auto result = chatListTimeId();
  625. if (const auto draft = history()->cloudDraft(_rootId)) {
  626. if (!Data::DraftIsNull(draft) && !session().supportMode()) {
  627. return std::max(result, draft->date);
  628. }
  629. }
  630. return result;
  631. }
  632. int ForumTopic::fixedOnTopIndex() const {
  633. return 0;
  634. }
  635. bool ForumTopic::shouldBeInChatList() const {
  636. return isPinnedDialog(FilterId())
  637. || !lastMessageKnown()
  638. || (lastMessage() != nullptr);
  639. }
  640. HistoryItem *ForumTopic::lastMessage() const {
  641. return _lastMessage.value_or(nullptr);
  642. }
  643. bool ForumTopic::lastMessageKnown() const {
  644. return _lastMessage.has_value();
  645. }
  646. HistoryItem *ForumTopic::lastServerMessage() const {
  647. return _lastServerMessage.value_or(nullptr);
  648. }
  649. bool ForumTopic::lastServerMessageKnown() const {
  650. return _lastServerMessage.has_value();
  651. }
  652. MsgId ForumTopic::lastKnownServerMessageId() const {
  653. return _lastKnownServerMessageId;
  654. }
  655. QString ForumTopic::title() const {
  656. return _title;
  657. }
  658. TextWithEntities ForumTopic::titleWithIcon() const {
  659. return ForumTopicIconWithTitle(_rootId, _iconId, _title);
  660. }
  661. int ForumTopic::titleVersion() const {
  662. return _titleVersion;
  663. }
  664. void ForumTopic::applyTitle(const QString &title) {
  665. if (_title == title) {
  666. return;
  667. }
  668. _title = title;
  669. invalidateTitleWithIcon();
  670. _defaultIcon = QImage();
  671. indexTitleParts();
  672. updateChatListEntry();
  673. session().changes().topicUpdated(this, UpdateFlag::Title);
  674. }
  675. DocumentId ForumTopic::iconId() const {
  676. return _iconId;
  677. }
  678. void ForumTopic::applyIconId(DocumentId iconId) {
  679. if (_iconId == iconId) {
  680. return;
  681. }
  682. _iconId = iconId;
  683. invalidateTitleWithIcon();
  684. _icon = iconId
  685. ? std::make_unique<Ui::Text::LimitedLoopsEmoji>(
  686. owner().customEmojiManager().create(
  687. _iconId,
  688. [=] { updateChatListEntry(); },
  689. Data::CustomEmojiManager::SizeTag::Normal),
  690. kUserpicLoopsCount)
  691. : nullptr;
  692. if (iconId) {
  693. _defaultIcon = QImage();
  694. }
  695. updateChatListEntry();
  696. session().changes().topicUpdated(this, UpdateFlag::IconId);
  697. }
  698. void ForumTopic::invalidateTitleWithIcon() {
  699. ++_titleVersion;
  700. _forum->recentTopicsInvalidate(this);
  701. }
  702. int32 ForumTopic::colorId() const {
  703. return _colorId;
  704. }
  705. void ForumTopic::applyColorId(int32 colorId) {
  706. if (_colorId != colorId) {
  707. _colorId = colorId;
  708. session().changes().topicUpdated(this, UpdateFlag::ColorId);
  709. }
  710. }
  711. void ForumTopic::applyItemAdded(not_null<HistoryItem*> item) {
  712. if (item->isRegular()) {
  713. setLastServerMessage(item);
  714. } else {
  715. setLastMessage(item);
  716. }
  717. }
  718. void ForumTopic::maybeSetLastMessage(not_null<HistoryItem*> item) {
  719. Expects(item->topicRootId() == _rootId);
  720. if (!_lastMessage
  721. || !(*_lastMessage)
  722. || ((*_lastMessage)->date() < item->date())
  723. || ((*_lastMessage)->date() == item->date()
  724. && (*_lastMessage)->id < item->id)) {
  725. setLastMessage(item);
  726. }
  727. }
  728. void ForumTopic::applyItemRemoved(MsgId id) {
  729. if (const auto lastItem = lastMessage()) {
  730. if (lastItem->id == id) {
  731. _lastMessage = std::nullopt;
  732. }
  733. }
  734. if (const auto lastServerItem = lastServerMessage()) {
  735. if (lastServerItem->id == id) {
  736. _lastServerMessage = std::nullopt;
  737. }
  738. }
  739. if (const auto chatListItem = _chatListMessage.value_or(nullptr)) {
  740. if (chatListItem->id == id) {
  741. _chatListMessage = std::nullopt;
  742. requestChatListMessage();
  743. }
  744. }
  745. }
  746. bool ForumTopic::isServerSideUnread(
  747. not_null<const HistoryItem*> item) const {
  748. return _replies->isServerSideUnread(item);
  749. }
  750. void ForumTopic::setMuted(bool muted) {
  751. if (this->muted() == muted) {
  752. return;
  753. }
  754. const auto state = chatListBadgesState();
  755. const auto notify = state.unread || state.reaction;
  756. const auto notifier = unreadStateChangeNotifier(notify);
  757. Thread::setMuted(muted);
  758. session().changes().topicUpdated(this, UpdateFlag::Notifications);
  759. }
  760. not_null<HistoryView::SendActionPainter*> ForumTopic::sendActionPainter() {
  761. return _sendActionPainter.get();
  762. }
  763. Dialogs::UnreadState ForumTopic::chatListUnreadState() const {
  764. return unreadStateFor(
  765. _replies->displayedUnreadCount(),
  766. _replies->unreadCountKnown());
  767. }
  768. Dialogs::BadgesState ForumTopic::chatListBadgesState() const {
  769. auto result = Dialogs::BadgesForUnread(
  770. chatListUnreadState(),
  771. Dialogs::CountInBadge::Messages,
  772. Dialogs::IncludeInBadge::All);
  773. if (!result.unread && _replies->inboxReadTillId() < 2) {
  774. result.unread = channel()->amIn()
  775. && (_lastKnownServerMessageId > history()->inboxReadTillId());
  776. result.unreadMuted = muted();
  777. }
  778. return result;
  779. }
  780. Dialogs::UnreadState ForumTopic::unreadStateFor(
  781. int count,
  782. bool known) const {
  783. auto result = Dialogs::UnreadState();
  784. const auto muted = this->muted();
  785. result.messages = count;
  786. result.chats = count ? 1 : 0;
  787. result.mentions = unreadMentions().has() ? 1 : 0;
  788. result.reactions = unreadReactions().has() ? 1 : 0;
  789. result.messagesMuted = muted ? result.messages : 0;
  790. result.chatsMuted = muted ? result.chats : 0;
  791. result.reactionsMuted = muted ? result.reactions : 0;
  792. result.known = known;
  793. return result;
  794. }
  795. void ForumTopic::allowChatListMessageResolve() {
  796. if (_flags & Flag::ResolveChatListMessage) {
  797. return;
  798. }
  799. _flags |= Flag::ResolveChatListMessage;
  800. resolveChatListMessageGroup();
  801. }
  802. HistoryItem *ForumTopic::chatListMessage() const {
  803. return _lastMessage.value_or(nullptr);
  804. }
  805. bool ForumTopic::chatListMessageKnown() const {
  806. return _lastMessage.has_value();
  807. }
  808. const QString &ForumTopic::chatListName() const {
  809. return _title;
  810. }
  811. const base::flat_set<QString> &ForumTopic::chatListNameWords() const {
  812. return _titleWords;
  813. }
  814. const base::flat_set<QChar> &ForumTopic::chatListFirstLetters() const {
  815. return _titleFirstLetters;
  816. }
  817. void ForumTopic::hasUnreadMentionChanged(bool has) {
  818. auto was = chatListUnreadState();
  819. if (has) {
  820. was.mentions = 0;
  821. } else {
  822. was.mentions = 1;
  823. }
  824. notifyUnreadStateChange(was);
  825. }
  826. void ForumTopic::hasUnreadReactionChanged(bool has) {
  827. auto was = chatListUnreadState();
  828. if (has) {
  829. was.reactions = was.reactionsMuted = 0;
  830. } else {
  831. was.reactions = 1;
  832. was.reactionsMuted = muted() ? was.reactions : 0;
  833. }
  834. notifyUnreadStateChange(was);
  835. }
  836. const QString &ForumTopic::chatListNameSortKey() const {
  837. static const auto empty = QString();
  838. return empty;
  839. }
  840. } // namespace Data