data_cloud_themes.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  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_cloud_themes.h"
  8. #include "window/themes/window_theme.h"
  9. #include "window/themes/window_theme_preview.h"
  10. #include "window/themes/window_theme_editor_box.h"
  11. #include "window/window_controller.h"
  12. #include "data/data_session.h"
  13. #include "data/data_document.h"
  14. #include "data/data_file_origin.h"
  15. #include "data/data_document_media.h"
  16. #include "main/main_session.h"
  17. #include "ui/boxes/confirm_box.h"
  18. #include "media/view/media_view_open_common.h"
  19. #include "lang/lang_keys.h"
  20. #include "apiwrap.h"
  21. namespace Data {
  22. namespace {
  23. constexpr auto kFirstReloadTimeout = 10 * crl::time(1000);
  24. constexpr auto kReloadTimeout = 3600 * crl::time(1000);
  25. bool IsTestingColors/* = false*/;
  26. } // namespace
  27. CloudTheme CloudTheme::Parse(
  28. not_null<Main::Session*> session,
  29. const MTPDtheme &data,
  30. bool parseSettings) {
  31. const auto document = data.vdocument();
  32. const auto paper = [&](const MTPThemeSettings &settings) {
  33. return settings.match([&](const MTPDthemeSettings &data) {
  34. return data.vwallpaper()
  35. ? WallPaper::Create(session, *data.vwallpaper())
  36. : std::nullopt;
  37. });
  38. };
  39. const auto outgoingMessagesColors = [&](
  40. const MTPThemeSettings &settings) {
  41. auto result = std::vector<QColor>();
  42. settings.match([&](const MTPDthemeSettings &data) {
  43. if (const auto colors = data.vmessage_colors()) {
  44. for (const auto &color : colors->v) {
  45. result.push_back(Ui::ColorFromSerialized(color));
  46. }
  47. }
  48. });
  49. return result;
  50. };
  51. const auto accentColor = [&](const MTPThemeSettings &settings) {
  52. return settings.match([&](const MTPDthemeSettings &data) {
  53. return Ui::ColorFromSerialized(data.vaccent_color());
  54. });
  55. };
  56. const auto outgoingAccentColor = [&](const MTPThemeSettings &settings) {
  57. return settings.match([&](const MTPDthemeSettings &data) {
  58. return Ui::MaybeColorFromSerialized(data.voutbox_accent_color());
  59. });
  60. };
  61. const auto basedOnDark = [&](const MTPThemeSettings &settings) {
  62. return settings.match([&](const MTPDthemeSettings &data) {
  63. return data.vbase_theme().match([](
  64. const MTPDbaseThemeNight &) {
  65. return true;
  66. }, [](const MTPDbaseThemeTinted &) {
  67. return true;
  68. }, [](const auto &) {
  69. return false;
  70. });
  71. });
  72. };
  73. const auto settings = [&] {
  74. auto result = base::flat_map<Type, Settings>();
  75. const auto settings = data.vsettings();
  76. if (!settings) {
  77. return result;
  78. }
  79. for (const auto &fields : settings->v) {
  80. const auto type = basedOnDark(fields) ? Type::Dark : Type::Light;
  81. result.emplace(type, Settings{
  82. .paper = paper(fields),
  83. .accentColor = accentColor(fields),
  84. .outgoingAccentColor = outgoingAccentColor(fields),
  85. .outgoingMessagesColors = outgoingMessagesColors(fields),
  86. });
  87. }
  88. return result;
  89. };
  90. return {
  91. .id = data.vid().v,
  92. .accessHash = data.vaccess_hash().v,
  93. .slug = qs(data.vslug()),
  94. .title = qs(data.vtitle()),
  95. .documentId = (document
  96. ? session->data().processDocument(*document)->id
  97. : DocumentId(0)),
  98. .createdBy = data.is_creator() ? session->userId() : UserId(0),
  99. .usersCount = data.vinstalls_count().value_or_empty(),
  100. .emoticon = qs(data.vemoticon().value_or_empty()),
  101. .settings = (parseSettings
  102. ? settings()
  103. : base::flat_map<Type, Settings>()),
  104. };
  105. }
  106. CloudTheme CloudTheme::Parse(
  107. not_null<Main::Session*> session,
  108. const MTPTheme &data,
  109. bool parseSettings) {
  110. return data.match([&](const MTPDtheme &data) {
  111. return CloudTheme::Parse(session, data, parseSettings);
  112. });
  113. }
  114. QString CloudThemes::Format() {
  115. static const auto kResult = QString::fromLatin1("tdesktop");
  116. return kResult;
  117. }
  118. CloudThemes::CloudThemes(not_null<Main::Session*> session)
  119. : _session(session)
  120. , _reloadCurrentTimer([=] { reloadCurrent(); }) {
  121. setupReload();
  122. }
  123. void CloudThemes::setupReload() {
  124. using namespace Window::Theme;
  125. if (needReload()) {
  126. _reloadCurrentTimer.callOnce(kFirstReloadTimeout);
  127. }
  128. Background()->updates(
  129. ) | rpl::filter([](const BackgroundUpdate &update) {
  130. return (update.type == BackgroundUpdate::Type::ApplyingTheme);
  131. }) | rpl::map([=] {
  132. return needReload();
  133. }) | rpl::start_with_next([=](bool need) {
  134. install();
  135. if (need) {
  136. scheduleReload();
  137. } else {
  138. _reloadCurrentTimer.cancel();
  139. }
  140. }, _lifetime);
  141. }
  142. bool CloudThemes::needReload() const {
  143. const auto &fields = Window::Theme::Background()->themeObject().cloud;
  144. return fields.id && fields.documentId;
  145. }
  146. void CloudThemes::install() {
  147. using namespace Window::Theme;
  148. const auto &fields = Background()->themeObject().cloud;
  149. auto &themeId = IsNightMode()
  150. ? _installedNightThemeId
  151. : _installedDayThemeId;
  152. const auto cloudId = fields.documentId ? fields.id : uint64(0);
  153. if (themeId == cloudId) {
  154. return;
  155. }
  156. themeId = cloudId;
  157. using Flag = MTPaccount_InstallTheme::Flag;
  158. const auto flags = (IsNightMode() ? Flag::f_dark : Flag(0))
  159. | Flag::f_format
  160. | (themeId ? Flag::f_theme : Flag(0));
  161. _session->api().request(MTPaccount_InstallTheme(
  162. MTP_flags(flags),
  163. MTP_inputTheme(MTP_long(cloudId), MTP_long(fields.accessHash)),
  164. MTP_string(Format()),
  165. MTPBaseTheme()
  166. )).send();
  167. }
  168. void CloudThemes::reloadCurrent() {
  169. if (!needReload()) {
  170. return;
  171. }
  172. const auto &fields = Window::Theme::Background()->themeObject().cloud;
  173. _session->api().request(MTPaccount_GetTheme(
  174. MTP_string(Format()),
  175. MTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash))
  176. )).done([=](const MTPTheme &result) {
  177. applyUpdate(result);
  178. }).fail([=] {
  179. _reloadCurrentTimer.callOnce(kReloadTimeout);
  180. }).send();
  181. }
  182. void CloudThemes::applyUpdate(const MTPTheme &theme) {
  183. theme.match([&](const MTPDtheme &data) {
  184. const auto cloud = CloudTheme::Parse(_session, data);
  185. const auto &object = Window::Theme::Background()->themeObject();
  186. if ((cloud.id != object.cloud.id)
  187. || (cloud.documentId == object.cloud.documentId)
  188. || !cloud.documentId) {
  189. return;
  190. }
  191. applyFromDocument(cloud);
  192. });
  193. scheduleReload();
  194. }
  195. void CloudThemes::resolve(
  196. not_null<Window::Controller*> controller,
  197. const QString &slug,
  198. const FullMsgId &clickFromMessageId) {
  199. _session->api().request(_resolveRequestId).cancel();
  200. _resolveRequestId = _session->api().request(MTPaccount_GetTheme(
  201. MTP_string(Format()),
  202. MTP_inputThemeSlug(MTP_string(slug))
  203. )).done([=](const MTPTheme &result) {
  204. showPreview(controller, result);
  205. }).fail([=](const MTP::Error &error) {
  206. if (error.type() == u"THEME_FORMAT_INVALID"_q) {
  207. controller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));
  208. }
  209. }).send();
  210. }
  211. void CloudThemes::showPreview(
  212. not_null<Window::Controller*> controller,
  213. const MTPTheme &data) {
  214. data.match([&](const MTPDtheme &data) {
  215. showPreview(controller, CloudTheme::Parse(_session, data));
  216. });
  217. }
  218. void CloudThemes::showPreview(
  219. not_null<Window::Controller*> controller,
  220. const CloudTheme &cloud) {
  221. if (const auto documentId = cloud.documentId) {
  222. previewFromDocument(controller, cloud);
  223. } else if (cloud.createdBy == _session->userId()) {
  224. controller->show(Box(
  225. Window::Theme::CreateForExistingBox,
  226. controller,
  227. cloud));
  228. } else {
  229. controller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));
  230. }
  231. }
  232. void CloudThemes::applyFromDocument(const CloudTheme &cloud) {
  233. const auto document = _session->data().document(cloud.documentId);
  234. loadDocumentAndInvoke(_updatingFrom, cloud, document, [=](
  235. std::shared_ptr<Data::DocumentMedia> media) {
  236. const auto document = media->owner();
  237. auto preview = Window::Theme::PreviewFromFile(
  238. media->bytes(),
  239. document->location().name(),
  240. cloud);
  241. if (preview) {
  242. Window::Theme::Apply(std::move(preview));
  243. Window::Theme::KeepApplied();
  244. }
  245. });
  246. }
  247. void CloudThemes::previewFromDocument(
  248. not_null<Window::Controller*> controller,
  249. const CloudTheme &cloud) {
  250. const auto sessionController = controller->sessionController();
  251. if (!sessionController) {
  252. return;
  253. }
  254. const auto document = _session->data().document(cloud.documentId);
  255. loadDocumentAndInvoke(_previewFrom, cloud, document, [=](
  256. std::shared_ptr<Data::DocumentMedia> media) {
  257. const auto document = media->owner();
  258. using Open = Media::View::OpenRequest;
  259. controller->openInMediaView(Open(sessionController, document, cloud));
  260. });
  261. }
  262. void CloudThemes::loadDocumentAndInvoke(
  263. LoadingDocument &value,
  264. const CloudTheme &cloud,
  265. not_null<DocumentData*> document,
  266. Fn<void(std::shared_ptr<Data::DocumentMedia>)> callback) {
  267. const auto alreadyWaiting = (value.document != nullptr);
  268. if (alreadyWaiting) {
  269. value.document->cancel();
  270. }
  271. value.document = document;
  272. value.documentMedia = document->createMediaView();
  273. value.document->save(
  274. Data::FileOriginTheme(cloud.id, cloud.accessHash),
  275. QString());
  276. value.callback = std::move(callback);
  277. if (value.documentMedia->loaded()) {
  278. invokeForLoaded(value);
  279. return;
  280. }
  281. if (!alreadyWaiting) {
  282. _session->downloaderTaskFinished(
  283. ) | rpl::filter([=, &value] {
  284. return value.documentMedia->loaded();
  285. }) | rpl::start_with_next([=, &value] {
  286. invokeForLoaded(value);
  287. }, value.subscription);
  288. }
  289. }
  290. void CloudThemes::invokeForLoaded(LoadingDocument &value) {
  291. const auto onstack = std::move(value.callback);
  292. auto media = std::move(value.documentMedia);
  293. value = LoadingDocument();
  294. onstack(std::move(media));
  295. }
  296. void CloudThemes::scheduleReload() {
  297. if (needReload()) {
  298. _reloadCurrentTimer.callOnce(kReloadTimeout);
  299. } else {
  300. _reloadCurrentTimer.cancel();
  301. }
  302. }
  303. void CloudThemes::refresh() {
  304. if (_refreshRequestId) {
  305. return;
  306. }
  307. _refreshRequestId = _session->api().request(MTPaccount_GetThemes(
  308. MTP_string(Format()),
  309. MTP_long(_hash)
  310. )).done([=](const MTPaccount_Themes &result) {
  311. _refreshRequestId = 0;
  312. result.match([&](const MTPDaccount_themes &data) {
  313. _hash = data.vhash().v;
  314. parseThemes(data.vthemes().v);
  315. _updates.fire({});
  316. }, [](const MTPDaccount_themesNotModified &) {
  317. });
  318. }).fail([=] {
  319. _refreshRequestId = 0;
  320. }).send();
  321. }
  322. void CloudThemes::parseThemes(const QVector<MTPTheme> &list) {
  323. _list.clear();
  324. _list.reserve(list.size());
  325. for (const auto &theme : list) {
  326. _list.push_back(CloudTheme::Parse(_session, theme));
  327. }
  328. checkCurrentTheme();
  329. }
  330. void CloudThemes::refreshChatThemes() {
  331. if (_chatThemesRequestId) {
  332. return;
  333. }
  334. _chatThemesRequestId = _session->api().request(MTPaccount_GetChatThemes(
  335. MTP_long(_chatThemesHash)
  336. )).done([=](const MTPaccount_Themes &result) {
  337. _chatThemesRequestId = 0;
  338. result.match([&](const MTPDaccount_themes &data) {
  339. _chatThemesHash = data.vhash().v;
  340. parseChatThemes(data.vthemes().v);
  341. _chatThemesUpdates.fire({});
  342. }, [](const MTPDaccount_themesNotModified &) {
  343. });
  344. }).fail([=] {
  345. _chatThemesRequestId = 0;
  346. }).send();
  347. }
  348. const std::vector<CloudTheme> &CloudThemes::chatThemes() const {
  349. return _chatThemes;
  350. }
  351. rpl::producer<> CloudThemes::chatThemesUpdated() const {
  352. return _chatThemesUpdates.events();
  353. }
  354. std::optional<CloudTheme> CloudThemes::themeForEmoji(
  355. const QString &emoticon) const {
  356. const auto emoji = Ui::Emoji::Find(emoticon);
  357. if (!emoji) {
  358. return {};
  359. }
  360. const auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {
  361. return Ui::Emoji::Find(v.emoticon);
  362. });
  363. return (i != end(_chatThemes)) ? std::make_optional(*i) : std::nullopt;
  364. }
  365. rpl::producer<std::optional<CloudTheme>> CloudThemes::themeForEmojiValue(
  366. const QString &emoticon) {
  367. const auto testing = TestingColors();
  368. if (!Ui::Emoji::Find(emoticon)) {
  369. return rpl::single<std::optional<CloudTheme>>(std::nullopt);
  370. } else if (auto result = themeForEmoji(emoticon)) {
  371. if (testing) {
  372. return rpl::single(
  373. std::move(result)
  374. ) | rpl::then(chatThemesUpdated(
  375. ) | rpl::map([=] {
  376. return themeForEmoji(emoticon);
  377. }) | rpl::filter([](const std::optional<CloudTheme> &theme) {
  378. return theme.has_value();
  379. }));
  380. }
  381. return rpl::single(std::move(result));
  382. }
  383. refreshChatThemes();
  384. const auto limit = testing ? (1 << 20) : 1;
  385. return rpl::single<std::optional<CloudTheme>>(
  386. std::nullopt
  387. ) | rpl::then(chatThemesUpdated(
  388. ) | rpl::map([=] {
  389. return themeForEmoji(emoticon);
  390. }) | rpl::filter([](const std::optional<CloudTheme> &theme) {
  391. return theme.has_value();
  392. }) | rpl::take(limit));
  393. }
  394. bool CloudThemes::TestingColors() {
  395. return IsTestingColors;
  396. }
  397. void CloudThemes::SetTestingColors(bool testing) {
  398. IsTestingColors = testing;
  399. }
  400. QString CloudThemes::prepareTestingLink(const CloudTheme &theme) const {
  401. const auto hex = [](int value) {
  402. return QChar((value < 10) ? ('0' + value) : ('a' + (value - 10)));
  403. };
  404. const auto hex2 = [&](int value) {
  405. return QString() + hex(value / 16) + hex(value % 16);
  406. };
  407. const auto color = [&](const QColor &color) {
  408. return hex2(color.red()) + hex2(color.green()) + hex2(color.blue());
  409. };
  410. const auto colors = [&](const std::vector<QColor> &colors) {
  411. auto list = QStringList();
  412. for (const auto &c : colors) {
  413. list.push_back(color(c));
  414. }
  415. return list.join(",");
  416. };
  417. auto arguments = QStringList();
  418. for (const auto &[type, settings] : theme.settings) {
  419. const auto add = [&, type = type](const QString &value) {
  420. const auto prefix = (type == CloudTheme::Type::Dark)
  421. ? u"dark_"_q
  422. : u""_q;
  423. arguments.push_back(prefix + value);
  424. };
  425. add("accent=" + color(settings.accentColor));
  426. if (settings.paper && !settings.paper->backgroundColors().empty()) {
  427. add("bg=" + colors(settings.paper->backgroundColors()));
  428. }
  429. if (settings.paper/* && settings.paper->hasShareUrl()*/) {
  430. add("intensity="
  431. + QString::number(settings.paper->patternIntensity()));
  432. //const auto url = settings.paper->shareUrl(_session);
  433. //const auto from = url.indexOf("bg/");
  434. //const auto till = url.indexOf("?");
  435. //if (from > 0 && till > from) {
  436. // add("slug=" + url.mid(from + 3, till - from - 3));
  437. //}
  438. }
  439. if (settings.outgoingAccentColor) {
  440. add("out_accent" + color(*settings.outgoingAccentColor));
  441. }
  442. if (!settings.outgoingMessagesColors.empty()) {
  443. add("out_bg=" + colors(settings.outgoingMessagesColors));
  444. }
  445. }
  446. return arguments.isEmpty()
  447. ? QString()
  448. : ("tg://test_chat_theme?" + arguments.join("&"));
  449. }
  450. std::optional<CloudTheme> CloudThemes::updateThemeFromLink(
  451. const QString &emoticon,
  452. const QMap<QString, QString> &params) {
  453. const auto emoji = Ui::Emoji::Find(emoticon);
  454. if (!TestingColors() || !emoji) {
  455. return std::nullopt;
  456. }
  457. const auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {
  458. return Ui::Emoji::Find(v.emoticon);
  459. });
  460. if (i == end(_chatThemes)) {
  461. return std::nullopt;
  462. }
  463. const auto hex = [](const QString &value) {
  464. return (value.size() != 1)
  465. ? std::nullopt
  466. : (value[0] >= 'a' && value[0] <= 'f')
  467. ? std::make_optional(10 + int(value[0].unicode() - 'a'))
  468. : (value[0] >= 'A' && value[0] <= 'F')
  469. ? std::make_optional(10 + int(value[0].unicode() - 'A'))
  470. : (value[0] >= '0' && value[0] <= '9')
  471. ? std::make_optional(int(value[0].unicode() - '0'))
  472. : std::nullopt;
  473. };
  474. const auto hex2 = [&](const QString &value) {
  475. const auto first = hex(value.mid(0, 1));
  476. const auto second = hex(value.mid(1, 1));
  477. return (first && second)
  478. ? std::make_optional((*first) * 16 + (*second))
  479. : std::nullopt;
  480. };
  481. const auto color = [&](const QString &value) {
  482. const auto red = hex2(value.mid(0, 2));
  483. const auto green = hex2(value.mid(2, 2));
  484. const auto blue = hex2(value.mid(4, 2));
  485. return (red && green && blue)
  486. ? std::make_optional(QColor(*red, *green, *blue))
  487. : std::nullopt;
  488. };
  489. const auto colors = [&](const QString &value) {
  490. auto list = value.split(",");
  491. auto result = std::vector<QColor>();
  492. for (const auto &single : list) {
  493. if (const auto c = color(single)) {
  494. result.push_back(*c);
  495. } else {
  496. return std::vector<QColor>();
  497. }
  498. }
  499. return (result.size() > 4) ? std::vector<QColor>() : result;
  500. };
  501. const auto parse = [&](CloudThemeType type, const QString &prefix = {}) {
  502. const auto accent = color(params["accent"]);
  503. if (!accent) {
  504. return;
  505. }
  506. auto &settings = i->settings[type];
  507. settings.accentColor = *accent;
  508. const auto bg = colors(params["bg"]);
  509. settings.paper = (settings.paper && !bg.empty())
  510. ? std::make_optional(settings.paper->withBackgroundColors(bg))
  511. : settings.paper;
  512. settings.paper = (settings.paper && params["intensity"].toInt())
  513. ? std::make_optional(
  514. settings.paper->withPatternIntensity(
  515. params["intensity"].toInt()))
  516. : settings.paper;
  517. settings.outgoingAccentColor = color(params["out_accent"]);
  518. settings.outgoingMessagesColors = colors(params["out_bg"]);
  519. };
  520. if (params.contains("dark_accent")) {
  521. parse(CloudThemeType::Dark, "dark_");
  522. }
  523. if (params.contains("accent")) {
  524. parse(params["dark"].isEmpty()
  525. ? CloudThemeType::Light
  526. : CloudThemeType::Dark);
  527. }
  528. _chatThemesUpdates.fire({});
  529. return *i;
  530. }
  531. void CloudThemes::parseChatThemes(const QVector<MTPTheme> &list) {
  532. _chatThemes.clear();
  533. _chatThemes.reserve(list.size());
  534. for (const auto &theme : list) {
  535. _chatThemes.push_back(CloudTheme::Parse(_session, theme, true));
  536. }
  537. }
  538. void CloudThemes::checkCurrentTheme() {
  539. const auto &object = Window::Theme::Background()->themeObject();
  540. if (!object.cloud.id || !object.cloud.documentId) {
  541. return;
  542. }
  543. const auto i = ranges::find(_list, object.cloud.id, &CloudTheme::id);
  544. if (i == end(_list)) {
  545. install();
  546. }
  547. }
  548. rpl::producer<> CloudThemes::updated() const {
  549. return _updates.events();
  550. }
  551. const std::vector<CloudTheme> &CloudThemes::list() const {
  552. return _list;
  553. }
  554. void CloudThemes::savedFromEditor(const CloudTheme &theme) {
  555. const auto i = ranges::find(_list, theme.id, &CloudTheme::id);
  556. if (i != end(_list)) {
  557. *i = theme;
  558. _updates.fire({});
  559. } else {
  560. _list.insert(begin(_list), theme);
  561. _updates.fire({});
  562. }
  563. }
  564. void CloudThemes::remove(uint64 cloudThemeId) {
  565. const auto i = ranges::find(_list, cloudThemeId, &CloudTheme::id);
  566. if (i == end(_list)) {
  567. return;
  568. }
  569. _session->api().request(MTPaccount_SaveTheme(
  570. MTP_inputTheme(
  571. MTP_long(i->id),
  572. MTP_long(i->accessHash)),
  573. MTP_bool(true)
  574. )).send();
  575. _list.erase(i);
  576. _updates.fire({});
  577. }
  578. } // namespace Data