From d57ada8a6418d435bfd0bccffdd2d2cd762319b7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 May 2023 22:54:13 +0400 Subject: [PATCH] Show stories summary status in chats list. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/dialogs/dialogs.style | 8 +- .../dialogs/ui/dialogs_stories_list.cpp | 198 ++++++++++++++++-- .../dialogs/ui/dialogs_stories_list.h | 47 ++++- 4 files changed, 240 insertions(+), 18 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8bfb90dff..54b768de8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3778,6 +3778,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_userpic_builder_color_subtitle" = "Choose background"; "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; +"lng_stories_row_count#one" = "{count} Story"; +"lng_stories_row_count#other" = "{count} Stories"; +"lng_stories_row_unread_and_one" = "{accumulated}, {user}"; +"lng_stories_row_unread_and_last" = "{accumulated} and {user}"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 52d80079a..47a025611 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -495,6 +495,8 @@ DialogsStories { shift: pixels; lineTwice: pixels; lineReadTwice: pixels; + nameLeft: pixels; + nameRight: pixels; nameTop: pixels; nameStyle: TextStyle; } @@ -507,7 +509,9 @@ dialogsStories: DialogsStories { shift: 16px; lineTwice: 3px; lineReadTwice: 0px; - nameTop: 9px; + nameLeft: 11px; + nameRight: 10px; + nameTop: 3px; nameStyle: semiboldTextStyle; } @@ -519,6 +523,8 @@ dialogsStoriesFull: DialogsStories { photoTop: 9px; lineTwice: 4px; lineReadTwice: 2px; + nameLeft: 0px; + nameRight: 0px; nameTop: 56px; nameStyle: TextStyle(defaultTextStyle) { font: font(11px); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index c16fe7015..d5d1f3d3c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_stories_list.h" +#include "lang/lang_keys.h" #include "ui/painter.h" #include "styles/style_dialogs.h" @@ -17,6 +18,7 @@ namespace { constexpr auto kSmallUserpicsShown = 3; constexpr auto kSmallReadOpacity = 0.6; +constexpr auto kSummaryExpandLeft = 1.5; [[nodiscard]] int AvailableNameWidth() { const auto &full = st::dialogsStoriesFull; @@ -40,7 +42,7 @@ List::List( }, lifetime()); _shownAnimation.stop(); - resize(0, _items.empty() ? 0 : st::dialogsStoriesFull.height); + resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); } void List::showContent(Content &&content) { @@ -48,24 +50,25 @@ void List::showContent(Content &&content) { return; } if (content.users.empty()) { - _hidingItems = base::take(_items); - if (!_hidingItems.empty()) { + _hidingData = base::take(_data); + if (!_hidingData.empty()) { toggleAnimated(false); } return; } const auto hidden = _content.users.empty(); _content = std::move(content); - auto items = base::take(_items.empty() ? _hidingItems : _items); - _hidingItems.clear(); - _items.reserve(_content.users.size()); + auto items = base::take( + _data.items.empty() ? _hidingData.items : _data.items); + _hidingData = {}; + _data.items.reserve(_content.users.size()); for (const auto &user : _content.users) { const auto i = ranges::find(items, user.id, [](const Item &item) { return item.user.id; }); if (i != end(items)) { - _items.push_back(std::move(*i)); - auto &item = _items.back(); + _data.items.push_back(std::move(*i)); + auto &item = _data.items.back(); if (item.user.userpic != user.userpic) { item.user.userpic = user.userpic; item.subscribed = false; @@ -76,16 +79,94 @@ void List::showContent(Content &&content) { } item.user.unread = user.unread; } else { - _items.emplace_back(Item{ .user = user }); + _data.items.emplace_back(Item{ .user = user }); } } updateScrollMax(); + updateSummary(_data); update(); if (hidden) { toggleAnimated(true); } } +List::Summaries List::ComposeSummaries(Data &data) { + const auto total = int(data.items.size()); + auto unreadInFirst = 0; + auto unreadTotal = 0; + for (auto i = 0; i != total; ++i) { + if (data.items[i].user.unread) { + ++unreadTotal; + if (i < kSmallUserpicsShown) { + ++unreadInFirst; + } + } + } + auto result = Summaries(); + result.total.string + = tr::lng_stories_row_count(tr::now, lt_count, total); + const auto append = [&](QString &to, int index, bool last) { + if (to.isEmpty()) { + to = data.items[index].user.name; + } else { + to = (last + ? tr::lng_stories_row_unread_and_last + : tr::lng_stories_row_unread_and_one)( + tr::now, + lt_accumulated, + to, + lt_user, + data.items[index].user.name); + } + }; + if (!total) { + return result; + } else if (total <= kSmallUserpicsShown) { + for (auto i = 0; i != total; ++i) { + append(result.allNames.string, i, i == total - 1); + } + } + if (unreadInFirst > 0 && unreadInFirst == unreadTotal) { + for (auto i = 0; i != total; ++i) { + if (data.items[i].user.unread) { + append(result.unreadNames.string, i, !--unreadTotal); + } + } + } + return result; +} + +bool List::StringsEqual(const Summaries &a, const Summaries &b) { + return (a.total.string == b.total.string) + && (a.allNames.string == b.allNames.string) + && (a.unreadNames.string == b.unreadNames.string); +} + +void List::Populate(Summary &summary) { + if (summary.empty()) { + return; + } + summary.cache = QImage(); + summary.text = Ui::Text::String( + st::dialogsStories.nameStyle, + summary.string); +} + +void List::Populate(Summaries &summaries) { + Populate(summaries.total); + Populate(summaries.allNames); + Populate(summaries.unreadNames); +} + +void List::updateSummary(Data &data) { + auto summaries = ComposeSummaries(data); + if (StringsEqual(summaries, data.summaries)) { + return; + } + data.summaries = std::move(summaries); + Populate(data.summaries); +} + void List::toggleAnimated(bool shown) { _shownAnimation.start( [=] { updateHeight(); }, @@ -95,16 +176,19 @@ void List::toggleAnimated(bool shown) { } void List::updateHeight() { - const auto shown = _shownAnimation.value(_items.empty() ? 0. : 1.); + const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.); resize( width(), anim::interpolate(0, st::dialogsStoriesFull.height, shown)); + if (_data.empty() && shown == 0.) { + _hidingData = {}; + } } void List::updateScrollMax() { const auto &full = st::dialogsStoriesFull; const auto singleFull = full.photoLeft * 2 + full.photo; - const auto widthFull = full.left + int(_items.size()) * singleFull; + const auto widthFull = full.left + int(_data.items.size()) * singleFull; _scrollLeftMax = std::max(widthFull - width(), 0); _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); update(); @@ -139,18 +223,19 @@ void List::paintEvent(QPaintEvent *e) { const auto lerp = [=](float64 a, float64 b) { return a + (b - a) * ratio; }; + auto &rendering = _data.empty() ? _hidingData : _data; const auto photo = lerp(st.photo, full.photo); const auto photoTopSmall = (st.height - st.photo) / 2.; const auto photoTop = lerp(photoTopSmall, full.photoTop); const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto infoTop = st.nameTop + const auto summaryTop = st.nameTop - (st.photoTop + (st.photo / 2.)) + (photoTop + (photo / 2.)); const auto singleSmall = st.shift; const auto singleFull = full.photoLeft * 2 + full.photo; const auto single = lerp(singleSmall, singleFull); - const auto itemsCount = int(_items.size()); + const auto itemsCount = int(rendering.items.size()); const auto leftSmall = st.left; const auto leftFull = full.left - _scrollLeft; const auto startIndexFull = std::max(-leftFull, 0) / singleFull; @@ -182,6 +267,8 @@ void List::paintEvent(QPaintEvent *e) { const auto drawFull = (ratio > 0.); auto hq = PainterHighQualityEnabler(p); + paintSummary(p, rendering, summaryTop, ratio); + const auto count = std::max( endIndexFull - startIndexFull, endIndexSmall - startIndexSmall); @@ -201,10 +288,10 @@ void List::paintEvent(QPaintEvent *e) { const auto indexSmall = startIndexSmall + index; const auto indexFull = startIndexFull + index; const auto small = (drawSmall && indexSmall < endIndexSmall) - ? &_items[indexSmall] + ? &rendering.items[indexSmall] : nullptr; const auto full = (drawFull && indexFull < endIndexFull) - ? &_items[indexFull] + ? &rendering.items[indexFull] : nullptr; const auto x = left + single * index; return Single{ x, indexSmall, small, indexFull, full }; @@ -361,6 +448,87 @@ void List::validateName(not_null item) { text.drawElided(p, 0, 0, available, 1, style::al_top); } +List::Summary &List::ChooseSummary( + Summaries &summaries, + int totalItems, + int fullWidth) { + const auto &st = st::dialogsStories; + const auto used = std::min(totalItems, kSmallUserpicsShown); + const auto taken = st.left + + st.photoLeft + + st.photo + + (used - 1) * st.shift + + st.nameLeft + + st.nameRight; + const auto available = fullWidth - taken; + const auto prepare = [&](Summary &summary) { + if (!summary.empty() && (summary.text.maxWidth() <= available)) { + summary.available = available; + return true; + } + return false; + }; + if (prepare(summaries.unreadNames)) { + return summaries.unreadNames; + } else if (prepare(summaries.allNames)) { + return summaries.allNames; + } + prepare(summaries.total); + return summaries.total; +} + +void List::PrerenderSummary(Summary &summary) { + if (!summary.cache.isNull() + && summary.cacheForWidth == summary.available + && summary.cacheColor == st::dialogsNameFg->c) { + return; + } + const auto &st = st::dialogsStories; + const auto use = std::min(summary.text.maxWidth(), summary.available); + const auto ratio = style::DevicePixelRatio(); + summary.cache = QImage( + QSize(use, st.nameStyle.font->height) * ratio, + QImage::Format_ARGB32_Premultiplied); + summary.cache.setDevicePixelRatio(ratio); + summary.cache.fill(Qt::transparent); + auto p = Painter(&summary.cache); + p.setPen(st::dialogsNameFg); + summary.text.drawElided(p, 0, 0, summary.available); +} + +void List::paintSummary( + QPainter &p, + Data &data, + float64 summaryTop, + float64 hidden) { + const auto total = int(data.items.size()); + auto &summary = ChooseSummary(data.summaries, total, width()); + PrerenderSummary(summary); + const auto lerp = [&](float64 from, float64 to) { + return from + (to - from) * hidden; + }; + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto used = std::min(total, kSmallUserpicsShown); + const auto fullLeft = st.left + + st.photoLeft + + st.photo + + (used - 1) * st.shift + + st.nameLeft; + const auto leftFinal = std::min( + full.left + (full.photoLeft * 2 + full.photo) * total, + width()) * kSummaryExpandLeft; + const auto left = lerp(fullLeft, leftFinal); + const auto ratio = summary.cache.devicePixelRatio(); + const auto summaryWidth = lerp(summary.cache.width() / ratio, 0.); + const auto summaryHeight = lerp(summary.cache.height() / ratio, 0.); + summaryTop += ((summary.cache.height() / ratio) - summaryHeight) / 2.; + p.setOpacity(1. - hidden); + p.drawImage( + QRectF(left, summaryTop, summaryWidth, summaryHeight), + summary.cache); +} + void List::wheelEvent(QWheelEvent *e) { const auto horizontal = (e->angleDelta().x() != 0); if (!horizontal) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index cbc635482..636f855f7 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -55,6 +55,43 @@ private: QColor nameCacheColor; bool subscribed = false; }; + struct Summary { + QString string; + Ui::Text::String text; + int available = 0; + QImage cache; + QColor cacheColor; + int cacheForWidth = 0; + + [[nodiscard]] bool empty() const { + return string.isEmpty(); + } + }; + struct Summaries { + Summary total; + Summary allNames; + Summary unreadNames; + }; + struct Data { + std::vector items; + Summaries summaries; + + [[nodiscard]] bool empty() const { + return items.empty(); + } + }; + + [[nodiscard]] static Summaries ComposeSummaries(Data &data); + [[nodiscard]] static bool StringsEqual( + const Summaries &a, + const Summaries &b); + static void Populate(Summary &summary); + static void Populate(Summaries &summaries); + [[nodiscard]] static Summary &ChooseSummary( + Summaries &summaries, + int totalItems, + int fullWidth); + static void PrerenderSummary(Summary &summary); void showContent(Content &&content); void enterEventHook(QEnterEvent *e) override; @@ -68,15 +105,21 @@ private: void validateUserpic(not_null item); void validateName(not_null item); void updateScrollMax(); + void updateSummary(Data &data); void checkDragging(); bool finishDragging(); void updateHeight(); void toggleAnimated(bool shown); + void paintSummary( + QPainter &p, + Data &data, + float64 summaryTop, + float64 hidden); Content _content; - std::vector _items; - std::vector _hidingItems; + Data _data; + Data _hidingData; Fn _shownHeight = 0; rpl::event_stream _clicks; rpl::event_stream<> _expandRequests;