diff --git a/Telegram/Resources/icons/info/info_stories_archive.png b/Telegram/Resources/icons/info/info_stories_archive.png new file mode 100644 index 000000000..9b4b79ce4 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@2x.png b/Telegram/Resources/icons/info/info_stories_archive@2x.png new file mode 100644 index 000000000..831363fa5 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@3x.png b/Telegram/Resources/icons/info/info_stories_archive@3x.png new file mode 100644 index 000000000..e02e85c6c Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent.png b/Telegram/Resources/icons/info/info_stories_recent.png new file mode 100644 index 000000000..341ee2a06 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@2x.png b/Telegram/Resources/icons/info/info_stories_recent@2x.png new file mode 100644 index 000000000..ecb3fc72d Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@3x.png b/Telegram/Resources/icons/info/info_stories_recent@3x.png new file mode 100644 index 000000000..bacf1a3c1 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index fc537b734..328cc231a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1123,8 +1123,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_sure_kick_channel" = "Remove {user} from the channel?"; "lng_profile_sure_remove_admin" = "Remove {user} from admins?"; "lng_profile_loading" = "Loading..."; -"lng_profile_stories#one" = "{count} story"; -"lng_profile_stories#other" = "{count} stories"; +"lng_profile_saved_stories#one" = "{count} saved story"; +"lng_profile_saved_stories#other" = "{count} saved stories"; "lng_profile_photos#one" = "{count} photo"; "lng_profile_photos#other" = "{count} photos"; "lng_profile_gifs#one" = "{count} GIF"; @@ -3812,9 +3812,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_views#other" = "{count} views"; "lng_stories_no_views" = "No views"; "lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; +"lng_stories_cant_reply" = "You can't reply to this story."; -"lng_stories_my_title" = "My Stories"; -"lng_stories_archive_button" = "Archive"; +"lng_stories_my_title" = "Saved Stories"; +"lng_stories_archive_button" = "Stories Archive"; +"lng_stories_recent_button" = "Recent Stories"; "lng_stories_archive_title" = "Stories Archive"; "lng_stories_reply_sent" = "Message Sent"; "lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list."; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 9e6e2e146..834592de6 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -82,6 +82,7 @@ object_ptr PrepareContactsBox( auto stories = object_ptr( box, + st::dialogsStoriesList, Stories::ContentForSession( &sessionController->session(), Data::StorySourcesList::All), diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 260e4d766..f8a51b93e 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -423,6 +423,7 @@ void Stories::apply(const MTPDupdateStory &data) { if (!user->hasStoriesHidden()) { refreshInList(StorySourcesList::NotHidden); } + _sourceChanged.fire_copy(peerId); } void Stories::apply(not_null peer, const MTPUserStories *data) { diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 47a025611..30b36538d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -500,6 +500,12 @@ DialogsStories { nameTop: pixels; nameStyle: TextStyle; } +DialogsStoriesList { + small: DialogsStories; + full: DialogsStories; + bg: color; + readOpacity: double; +} dialogsStories: DialogsStories { left: 4px; @@ -532,3 +538,16 @@ dialogsStoriesFull: DialogsStories { linkFontOver: font(11px); } } + +dialogsStoriesList: DialogsStoriesList { + small: dialogsStories; + full: dialogsStoriesFull; + bg: dialogsBg; + readOpacity: 0.6; +} +dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) { + bg: transparent; +} +dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) { + readOpacity: 1.; +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 22ac09842..a63e159c7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -142,6 +142,7 @@ InnerWidget::InnerWidget( , _controller(controller) , _stories(std::make_unique( this, + st::dialogsStoriesList, Stories::ContentForSession( &controller->session(), Data::StorySourcesList::NotHidden), diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 332dec14a..24a97ae70 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -8,6 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_content.h" #include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -19,7 +24,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Dialogs::Stories { namespace { -class PeerUserpic final : public Userpic { +constexpr auto kShownLastCount = 3; + +class PeerUserpic final : public Thumbnail { public: explicit PeerUserpic(not_null peer); @@ -48,6 +55,70 @@ private: }; +class StoryThumbnail : public Thumbnail { +public: + explicit StoryThumbnail(FullStoryId id); + virtual ~StoryThumbnail() = default; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +protected: + struct Thumb { + Image *image = nullptr; + bool blurred = false; + }; + [[nodiscard]] virtual Main::Session &session() = 0; + [[nodiscard]] virtual Thumb loaded(FullStoryId id) = 0; + virtual void clear() = 0; + +private: + const FullStoryId _id; + QImage _full; + rpl::lifetime _subscription; + QImage _prepared; + bool _blurred = false; + +}; + +class PhotoThumbnail final : public StoryThumbnail { +public: + PhotoThumbnail(not_null photo, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _photo; + std::shared_ptr _media; + +}; + +class VideoThumbnail final : public StoryThumbnail { +public: + VideoThumbnail(not_null video, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _video; + std::shared_ptr _media; + +}; + +class EmptyThumbnail final : public Thumbnail { +public: + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + QImage _cached; + +}; + class State final { public: State(not_null data, Data::StorySourcesList list); @@ -57,7 +128,9 @@ public: private: const not_null _data; const Data::StorySourcesList _list; - base::flat_map, std::shared_ptr> _userpics; + base::flat_map< + not_null, + std::shared_ptr> _userpics; }; @@ -122,6 +195,127 @@ void PeerUserpic::processNewPhoto() { }, _subscribed->downloadLifetime); } +StoryThumbnail::StoryThumbnail(FullStoryId id) +: _id(id) { +} + +QImage StoryThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_prepared.width() != size * ratio) { + if (_full.isNull()) { + _prepared = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _prepared.fill(Qt::black); + } else { + const auto width = _full.width(); + const auto skip = std::max((_full.height() - width) / 2, 0); + _prepared = _full.copy(0, skip, width, width).scaled( + QSize(size, size) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + _prepared = Images::Circle(std::move(_prepared)); + } + return _prepared; +} + +void StoryThumbnail::subscribeToUpdates(Fn callback) { + _subscription.destroy(); + if (!callback) { + clear(); + return; + } else if (!_full.isNull() && !_blurred) { + return; + } + const auto thumbnail = loaded(_id); + if (const auto image = thumbnail.image) { + _full = image->original(); + } + _blurred = thumbnail.blurred; + if (!_blurred) { + _prepared = QImage(); + } else { + _subscription = session().downloaderTaskFinished( + ) | rpl::filter([=] { + const auto thumbnail = loaded(_id); + if (!thumbnail.blurred) { + _full = thumbnail.image->original(); + _prepared = QImage(); + _blurred = false; + return true; + } + return false; + }) | rpl::take(1) | rpl::start_with_next(callback); + } +} + +PhotoThumbnail::PhotoThumbnail(not_null photo, FullStoryId id) +: StoryThumbnail(id) +, _photo(photo) { +} + +Main::Session &PhotoThumbnail::session() { + return _photo->session(); +} + +StoryThumbnail::Thumb PhotoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _photo->createMediaView(); + _media->wanted( + Data::PhotoSize::Small, + Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->image(Data::PhotoSize::Small)) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void PhotoThumbnail::clear() { + _media = nullptr; +} + +VideoThumbnail::VideoThumbnail( + not_null video, + FullStoryId id) +: StoryThumbnail(id) +, _video(video) { +} + +Main::Session &VideoThumbnail::session() { + return _video->session(); +} + +StoryThumbnail::Thumb VideoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _video->createMediaView(); + _media->thumbnailWanted(Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->thumbnail()) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void VideoThumbnail::clear() { + _media = nullptr; +} + +QImage EmptyThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_cached.width() != size * ratio) { + _cached = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _cached.fill(Qt::black); + } + return _cached; +} + +void EmptyThumbnail::subscribeToUpdates(Fn callback) { +} + State::State(not_null data, Data::StorySourcesList list) : _data(data) , _list(list) { @@ -130,12 +324,12 @@ State::State(not_null data, Data::StorySourcesList list) Content State::next() { auto result = Content{ .full = (_list == Data::StorySourcesList::All) }; const auto &sources = _data->sources(_list); - result.users.reserve(sources.size()); + result.elements.reserve(sources.size()); for (const auto &info : sources) { const auto source = _data->source(info.id); Assert(source != nullptr); - auto userpic = std::shared_ptr(); + auto userpic = std::shared_ptr(); const auto user = source->user; if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; @@ -143,15 +337,15 @@ Content State::next() { userpic = std::make_shared(user); _userpics.emplace(user, userpic); } - result.users.push_back({ + result.elements.push_back({ .id = uint64(user->id.value), .name = (user->isSelf() ? tr::lng_stories_my_name(tr::now) : user->shortName()), - .userpic = std::move(userpic), + .thumbnail = std::move(userpic), .unread = info.unread, .hidden = info.hidden, - .self = user->isSelf(), + .skipSmall = user->isSelf(), }); } return result; @@ -177,4 +371,88 @@ rpl::producer ContentForSession( }; } +[[nodiscard]] std::shared_ptr PrepareThumbnail( + not_null story) { + using Result = std::shared_ptr; + const auto id = story->fullId(); + return v::match(story->media().data, [](v::null_t) -> Result { + return std::make_shared(); + }, [&](not_null photo) -> Result { + return std::make_shared(photo, id); + }, [&](not_null video) -> Result { + return std::make_shared(video, id); + }); +} + +rpl::producer LastForPeer(not_null peer) { + using namespace rpl::mappers; + + const auto stories = &peer->owner().stories(); + const auto peerId = peer->id; + + return rpl::single( + peerId + ) | rpl::then( + stories->sourceChanged() | rpl::filter(_1 == peerId) + ) | rpl::map([=] { + auto ids = std::vector(); + auto readTill = StoryId(); + if (const auto source = stories->source(peerId)) { + readTill = source->readTill; + ids = ranges::views::all(source->ids) + | ranges::views::reverse + | ranges::views::take(kShownLastCount) + | ranges::views::transform(&Data::StoryIdDates::id) + | ranges::to_vector; + } + return rpl::make_producer([=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + Fn check; + base::has_weak_ptr guard; + bool pushed = false; + }; + const auto state = lifetime.make_state(); + state->check = [=] { + if (state->pushed) { + return; + } + auto resolving = false; + auto result = Content(); + for (const auto id : ids) { + const auto storyId = FullStoryId{ peerId, id }; + const auto maybe = stories->lookup(storyId); + if (maybe) { + if (!resolving) { + result.elements.reserve(ids.size()); + result.elements.push_back({ + .id = uint64(id), + .thumbnail = PrepareThumbnail(*maybe), + .unread = (id > readTill), + }); + } + } else if (maybe.error() == Data::NoStory::Unknown) { + resolving = true; + stories->resolve( + storyId, + crl::guard(&state->guard, state->check)); + } + } + if (resolving) { + return; + } + state->pushed = true; + consumer.put_next(std::move(result)); + consumer.put_done(); + }; + rpl::single(peerId) | rpl::then( + stories->itemsChanged() | rpl::filter(_1 == peerId) + ) | rpl::start_with_next(state->check, lifetime); + + return lifetime; + }); + }) | rpl::flatten_latest(); +} + } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h index 1feb6a77a..8b25c8313 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -23,4 +23,6 @@ struct Content; not_null session, Data::StorySourcesList list); +[[nodiscard]] rpl::producer LastForPeer(not_null peer); + } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index c53282395..c022e025c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -17,13 +17,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Dialogs::Stories { namespace { -constexpr auto kSmallUserpicsShown = 3; -constexpr auto kSmallReadOpacity = 0.6; +constexpr auto kSmallThumbsShown = 3; constexpr auto kSummaryExpandLeft = 1.5; constexpr auto kPreloadPages = 2; -[[nodiscard]] int AvailableNameWidth() { - const auto &full = st::dialogsStoriesFull; +[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) { + const auto &full = st.full; const auto &font = full.nameStyle.font; const auto skip = font->spacew; return full.photoLeft * 2 + full.photo - 2 * skip; @@ -35,7 +34,7 @@ struct List::Layout { int itemsCount = 0; int shownHeight = 0; float64 ratio = 0.; - float64 userpicLeft = 0.; + float64 thumbnailLeft = 0.; float64 photoLeft = 0.; float64 left = 0.; float64 single = 0.; @@ -52,9 +51,11 @@ struct List::Layout { List::List( not_null parent, + const style::DialogsStoriesList &st, rpl::producer content, Fn shownHeight) : RpWidget(parent) +, _st(st) , _shownHeight(shownHeight) { setCursor(style::cur_default); @@ -64,45 +65,46 @@ List::List( _shownAnimation.stop(); setMouseTracking(true); - resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); + resize(0, _data.empty() ? 0 : st.full.height); } void List::showContent(Content &&content) { if (_content == content) { return; } - if (content.users.empty()) { + if (content.elements.empty()) { _hidingData = base::take(_data); if (!_hidingData.empty()) { toggleAnimated(false); } return; } - const auto hidden = _content.users.empty(); + const auto hidden = _content.elements.empty(); _content = std::move(content); 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; + _data.items.reserve(_content.elements.size()); + for (const auto &element : _content.elements) { + const auto id = element.id; + const auto i = ranges::find(items, id, [](const Item &item) { + return item.element.id; }); if (i != end(items)) { _data.items.push_back(std::move(*i)); auto &item = _data.items.back(); - if (item.user.userpic != user.userpic) { - item.user.userpic = user.userpic; + if (item.element.thumbnail != element.thumbnail) { + item.element.thumbnail = element.thumbnail; item.subscribed = false; } - if (item.user.name != user.name) { - item.user.name = user.name; + if (item.element.name != element.name) { + item.element.name = element.name; item.nameCache = QImage(); } - item.user.unread = user.unread; - item.user.hidden = user.hidden; + item.element.unread = element.unread; + item.element.hidden = element.hidden; } else { - _data.items.emplace_back(Item{ .user = user }); + _data.items.emplace_back(Item{ .element = element }); } } updateScrollMax(); @@ -115,23 +117,25 @@ void List::showContent(Content &&content) { List::Summaries List::ComposeSummaries(Data &data) { const auto total = int(data.items.size()); - const auto skip = (total > 1 && data.items[0].user.self) ? 1 : 0; + const auto skip = (total > 1 && data.items[0].element.skipSmall) + ? 1 + : 0; auto unreadInFirst = 0; auto unreadTotal = 0; for (auto i = skip; i != total; ++i) { - if (data.items[i].user.unread) { + if (data.items[i].element.unread) { ++unreadTotal; - if (i < skip + kSmallUserpicsShown) { + if (i < skip + kSmallThumbsShown) { ++unreadInFirst; } } } - auto result = Summaries{ .skipSelf = (skip > 0) }; + auto result = Summaries{ .skipOne = (skip > 0) }; 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; + to = data.items[index].element.name; } else { to = (last ? tr::lng_stories_row_unread_and_last @@ -140,19 +144,19 @@ List::Summaries List::ComposeSummaries(Data &data) { lt_accumulated, to, lt_user, - data.items[index].user.name); + data.items[index].element.name); } }; if (!total) { return result; - } else if (total <= skip + kSmallUserpicsShown) { + } else if (total <= skip + kSmallThumbsShown) { for (auto i = skip; i != total; ++i) { append(result.allNames.string, i, i == total - 1); } } if (unreadInFirst > 0 && unreadInFirst == unreadTotal) { for (auto i = skip; i != total; ++i) { - if (data.items[i].user.unread) { + if (data.items[i].element.unread) { append(result.unreadNames.string, i, !--unreadTotal); } } @@ -166,20 +170,22 @@ bool List::StringsEqual(const Summaries &a, const Summaries &b) { && (a.unreadNames.string == b.unreadNames.string); } -void List::Populate(Summary &summary) { +void List::Populate( + const style::DialogsStories &st, + Summary &summary) { if (summary.empty()) { return; } summary.cache = QImage(); - summary.text = Ui::Text::String( - st::dialogsStories.nameStyle, - summary.string); + summary.text = Ui::Text::String(st.nameStyle, summary.string); } -void List::Populate(Summaries &summaries) { - Populate(summaries.total); - Populate(summaries.allNames); - Populate(summaries.unreadNames); +void List::Populate( + const style::DialogsStories &st, + Summaries &summaries) { + Populate(st, summaries.total); + Populate(st, summaries.allNames); + Populate(st, summaries.unreadNames); } void List::updateSummary(Data &data) { @@ -188,7 +194,7 @@ void List::updateSummary(Data &data) { return; } data.summaries = std::move(summaries); - Populate(data.summaries); + Populate(_st.small, data.summaries); } void List::toggleAnimated(bool shown) { @@ -203,14 +209,14 @@ void List::updateHeight() { const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.); resize( width(), - anim::interpolate(0, st::dialogsStoriesFull.height, shown)); + anim::interpolate(0, _st.full.height, shown)); if (_data.empty() && shown == 0.) { _hidingData = {}; } } void List::updateScrollMax() { - const auto &full = st::dialogsStoriesFull; + const auto &full = _st.full; const auto singleFull = full.photoLeft * 2 + full.photo; const auto widthFull = full.left + int(_data.items.size()) * singleFull; _scrollLeftMax = std::max(widthFull - width(), 0); @@ -252,8 +258,8 @@ void List::resizeEvent(QResizeEvent *e) { } List::Layout List::computeLayout() const { - const auto &st = st::dialogsStories; - const auto &full = st::dialogsStoriesFull; + const auto &st = _st.small; + const auto &full = _st.full; const auto shownHeight = std::max(_shownHeight(), st.height); const auto ratio = float64(shownHeight - st.height) / (full.height - st.height); @@ -267,11 +273,12 @@ List::Layout List::computeLayout() const { + st::defaultDialogRow.photoSize + st::defaultDialogRow.padding.left(); const auto narrow = (width() <= narrowWidth); - const auto smallSkip = (itemsCount > 1 && rendering.items[0].user.self) + const auto smallSkip = (itemsCount > 1 + && rendering.items[0].element.skipSmall) ? 1 : 0; const auto smallCount = std::min( - kSmallUserpicsShown, + kSmallThumbsShown, itemsCount - smallSkip); const auto smallWidth = st.photo + (smallCount - 1) * st.shift; const auto leftSmall = (narrow @@ -288,17 +295,17 @@ List::Layout List::computeLayout() const { const auto startIndexSmall = std::min(startIndexFull, smallSkip); const auto endIndexSmall = smallSkip + smallCount; const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift); - const auto userpicLeftFull = cellLeftFull + full.photoLeft; - const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; - const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); + const auto thumbnailLeftFull = cellLeftFull + full.photoLeft; + const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft; + const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull); const auto photoLeft = lerp(st.photoLeft, full.photoLeft); return Layout{ .itemsCount = itemsCount, .shownHeight = shownHeight, .ratio = ratio, - .userpicLeft = userpicLeft, + .thumbnailLeft = thumbnailLeft, .photoLeft = photoLeft, - .left = userpicLeft - photoLeft, + .left = thumbnailLeft - photoLeft, .single = lerp(st.shift, singleFull), .smallSkip = smallSkip, .leftFull = leftFull, @@ -313,8 +320,8 @@ List::Layout List::computeLayout() const { } void List::paintEvent(QPaintEvent *e) { - const auto &st = st::dialogsStories; - const auto &full = st::dialogsStoriesFull; + const auto &st = _st.small; + const auto &full = _st.full; const auto layout = computeLayout(); const auto ratio = layout.ratio; const auto lerp = [&](float64 a, float64 b) { @@ -331,14 +338,14 @@ void List::paintEvent(QPaintEvent *e) { + (photoTop + (photo / 2.)); const auto nameScale = layout.shownHeight / float64(full.height); const auto nameTop = nameScale * full.nameTop; - const auto nameWidth = nameScale * AvailableNameWidth(); + const auto nameWidth = nameScale * AvailableNameWidth(_st); const auto nameHeight = nameScale * full.nameStyle.font->height; const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; - const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); - const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); + const auto readUserpicOpacity = lerp(_st.readOpacity, 1.); + const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.); auto p = QPainter(this); - p.fillRect(e->rect(), st::dialogsBg); + p.fillRect(e->rect(), _st.bg); p.translate(0, height() - layout.shownHeight); const auto drawSmall = (ratio < 1.); @@ -375,8 +382,8 @@ void List::paintEvent(QPaintEvent *e) { return Single{ x, indexSmall, small, indexFull, full }; }; const auto hasUnread = [&](const Single &single) { - return (single.itemSmall && single.itemSmall->user.unread) - || (single.itemFull && single.itemFull->user.unread); + return (single.itemSmall && single.itemSmall->element.unread) + || (single.itemFull && single.itemFull->element.unread); }; const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { auto nextGradientPainted = false; @@ -398,7 +405,9 @@ void List::paintEvent(QPaintEvent *e) { } if (i > first && hasUnread(current) && next) { if (current.itemSmall || !next.itemSmall) { - if (i - 1 == first && first > 0 && !skippedPainted) { + if (i - 1 == first + && first > 0 + && !skippedPainted) { if (const auto skipped = lookup(i - 2)) { skippedPainted = true; paintGradient(skipped); @@ -425,11 +434,15 @@ void List::paintEvent(QPaintEvent *e) { // Unread gradient. const auto x = single.x; - const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); + const auto userpic = QRectF( + x + layout.photoLeft, + photoTop, + photo, + photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; - const auto smallUnread = small && small->user.unread; - const auto fullUnread = itemFull && itemFull->user.unread; + const auto smallUnread = small && small->element.unread; + const auto fullUnread = itemFull && itemFull->element.unread; const auto unreadOpacity = (smallUnread && fullUnread) ? 1. : smallUnread @@ -458,11 +471,15 @@ void List::paintEvent(QPaintEvent *e) { Expects(single.itemSmall || single.itemFull); const auto x = single.x; - const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); + const auto userpic = QRectF( + x + layout.photoLeft, + photoTop, + photo, + photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; - const auto smallUnread = small && small->user.unread; - const auto fullUnread = itemFull && itemFull->user.unread; + const auto smallUnread = small && small->element.unread; + const auto fullUnread = itemFull && itemFull->element.unread; // White circle with possible read gray line. const auto hasReadLine = (itemFull && !fullUnread); @@ -483,25 +500,27 @@ void List::paintEvent(QPaintEvent *e) { // Userpic. if (itemFull == small) { p.setOpacity(smallUnread ? 1. : readUserpicOpacity); - validateUserpic(itemFull); + validateThumbnail(itemFull); const auto size = full.photo; - p.drawImage(userpic, itemFull->user.userpic->image(size)); + p.drawImage(userpic, itemFull->element.thumbnail->image(size)); } else { if (small) { p.setOpacity(smallUnread ? (itemFull ? 1. : (1. - ratio)) : (itemFull - ? kSmallReadOpacity + ? _st.readOpacity : readUserpicAppearingOpacity)); - validateUserpic(small); + validateThumbnail(small); const auto size = (ratio > 0.) ? full.photo : st.photo; - p.drawImage(userpic, small->user.userpic->image(size)); + p.drawImage(userpic, small->element.thumbnail->image(size)); } if (itemFull) { p.setOpacity(ratio); - validateUserpic(itemFull); + validateThumbnail(itemFull); const auto size = full.photo; - p.drawImage(userpic, itemFull->user.userpic->image(size)); + p.drawImage( + userpic, + itemFull->element.thumbnail->image(size)); } } p.setOpacity(1.); @@ -510,11 +529,11 @@ void List::paintEvent(QPaintEvent *e) { paintSummary(p, rendering, summaryTop, ratio); } -void List::validateUserpic(not_null item) { +void List::validateThumbnail(not_null item) { if (!item->subscribed) { item->subscribed = true; - //const auto id = item.user.id; - item->user.userpic->subscribeToUpdates([=] { + //const auto id = item.element.id; + item->element.thumbnail->subscribeToUpdates([=] { update(); }); } @@ -525,10 +544,10 @@ void List::validateName(not_null item) { if (!item->nameCache.isNull() && item->nameCacheColor == color->c) { return; } - const auto &full = st::dialogsStoriesFull; + const auto &full = _st.full; const auto &font = full.nameStyle.font; - const auto available = AvailableNameWidth(); - const auto text = Ui::Text::String(full.nameStyle, item->user.name); + const auto available = AvailableNameWidth(_st); + const auto text = Ui::Text::String(full.nameStyle, item->element.name); const auto ratio = style::DevicePixelRatio(); item->nameCacheColor = color->c; item->nameCache = QImage( @@ -542,13 +561,13 @@ void List::validateName(not_null item) { } List::Summary &List::ChooseSummary( + const style::DialogsStories &st, Summaries &summaries, int totalItems, int fullWidth) { - const auto &st = st::dialogsStories; const auto used = std::min( - totalItems - (summaries.skipSelf ? 1 : 0), - kSmallUserpicsShown); + totalItems - (summaries.skipOne ? 1 : 0), + kSmallThumbsShown); const auto taken = st.left + st.photoLeft + st.photo @@ -572,13 +591,14 @@ List::Summary &List::ChooseSummary( return summaries.total; } -void List::PrerenderSummary(Summary &summary) { +void List::PrerenderSummary( + const style::DialogsStories &st, + 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( @@ -597,16 +617,20 @@ void List::paintSummary( float64 summaryTop, float64 hidden) { const auto total = int(data.items.size()); - auto &summary = ChooseSummary(data.summaries, total, width()); - PrerenderSummary(summary); + auto &summary = ChooseSummary( + _st.small, + data.summaries, + total, + width()); + PrerenderSummary(_st.small, 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 &st = _st.small; + const auto &full = _st.full; const auto used = std::min( - total - (data.summaries.skipSelf ? 1 : 0), - kSmallUserpicsShown); + total - (data.summaries.skipOne ? 1 : 0), + kSmallThumbsShown); const auto fullLeft = st.left + st.photoLeft + st.photo @@ -671,7 +695,7 @@ void List::mouseMoveEvent(QMouseEvent *e) { if (!_dragging && _mouseDownPosition) { if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() >= QApplication::startDragDistance()) { - if (_shownHeight() < st::dialogsStoriesFull.height) { + if (_shownHeight() < _st.full.height) { _expandRequests.fire({}); } _dragging = true; @@ -718,7 +742,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) { if (_selected < 0) { _expandRequests.fire({}); } else if (_selected < _data.items.size()) { - _clicks.fire_copy(_data.items[_selected].user.id); + _clicks.fire_copy(_data.items[_selected].element.id); } } } @@ -737,8 +761,8 @@ void List::contextMenuEvent(QContextMenuEvent *e) { auto &item = _data.items[_selected]; _menu = base::make_unique_q(this); - const auto id = item.user.id; - const auto hidden = item.user.hidden; + const auto id = item.element.id; + const auto hidden = item.element.hidden; _menu->addAction(tr::lng_context_view_profile(tr::now), [=] { _showProfileRequests.fire_copy(id); }); @@ -781,8 +805,8 @@ void List::updateSelected() { if (_pressed >= 0) { return; } - const auto &st = st::dialogsStories; - const auto &full = st::dialogsStoriesFull; + const auto &st = _st.small; + const auto &full = _st.full; const auto p = mapFromGlobal(_lastMousePosition); const auto layout = computeLayout(); const auto firstRightFull = layout.leftFull diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 72fd67a9a..f81f90c05 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -13,31 +13,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class QPainter; +namespace style { +struct DialogsStories; +struct DialogsStoriesList; +} // namespace style + namespace Ui { class PopupMenu; } // namespace Ui namespace Dialogs::Stories { -class Userpic { +class Thumbnail { public: [[nodiscard]] virtual QImage image(int size) = 0; virtual void subscribeToUpdates(Fn callback) = 0; }; -struct User { +struct Element { uint64 id = 0; QString name; - std::shared_ptr userpic; + std::shared_ptr thumbnail; bool unread = false; bool hidden = false; - bool self = false; + bool skipSmall = false; - friend inline bool operator==(const User &a, const User &b) = default; + friend inline bool operator==( + const Element &a, + const Element &b) = default; }; struct Content { - std::vector users; + std::vector elements; bool full = false; friend inline bool operator==( @@ -54,6 +61,7 @@ class List final : public Ui::RpWidget { public: List( not_null parent, + const style::DialogsStoriesList &st, rpl::producer content, Fn shownHeight); @@ -67,7 +75,7 @@ public: private: struct Layout; struct Item { - User user; + Element element; QImage nameCache; QColor nameCacheColor; bool subscribed = false; @@ -88,7 +96,7 @@ private: Summary total; Summary allNames; Summary unreadNames; - bool skipSelf = false; + bool skipOne = false; }; struct Data { std::vector items; @@ -103,13 +111,20 @@ private: [[nodiscard]] static bool StringsEqual( const Summaries &a, const Summaries &b); - static void Populate(Summary &summary); - static void Populate(Summaries &summaries); + static void Populate( + const style::DialogsStories &st, + Summary &summary); + static void Populate( + const style::DialogsStories &st, + Summaries &summaries); [[nodiscard]] static Summary &ChooseSummary( + const style::DialogsStories &st, Summaries &summaries, int totalItems, int fullWidth); - static void PrerenderSummary(Summary &summary); + static void PrerenderSummary( + const style::DialogsStories &st, + Summary &summary); void showContent(Content &&content); void enterEventHook(QEnterEvent *e) override; @@ -121,7 +136,7 @@ private: void mouseReleaseEvent(QMouseEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; - void validateUserpic(not_null item); + void validateThumbnail(not_null item); void validateName(not_null item); void updateScrollMax(); void updateSummary(Data &data); @@ -140,6 +155,7 @@ private: [[nodiscard]] Layout computeLayout() const; + const style::DialogsStoriesList &_st; Content _content; Data _data; Data _hidingData; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 3fb9ab309..e2bee8c06 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1979,6 +1979,8 @@ bool HistoryItem::forbidsSaving() const { bool HistoryItem::canDelete() const { if (isSponsored()) { return false; + } else if (IsStoryMsgId(id)) { + return false && _history->peer->isSelf(); // #TODO stories } else if (isService() && !isRegular()) { return false; } else if (topicRootId() == id) { diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 62febc455..f2a9482cb 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -368,6 +368,8 @@ infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }}; infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }}; infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }}; infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }}; +infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; +infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }}; infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }}; diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 37dfe5529..a9e114515 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -534,6 +534,8 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const { case Type::MusicFile: return tr::lng_media_selected_song; case Type::Link: return tr::lng_media_selected_link; case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + // #TODO stories + case Type::PhotoVideo: return tr::lng_media_selected_photo; } Unexpected("Type in TopBar::generateSelectedText()"); }(); diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index af1facc58..9d3693280 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -142,7 +142,7 @@ inline auto AddStoriesButton( parent, std::move(count), [](int count) { - return tr::lng_profile_stories(tr::now, lt_count, count); + return tr::lng_profile_saved_stories(tr::now, lt_count, count); }, tracker)->entity(); result->addClickHandler([=] { diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp index 970b63d02..9fabb28ad 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -347,7 +347,7 @@ void ListSection::resizeToWidth(int newWidth) { _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) - st::infoMediaSkip; for (auto &item : _items) { - item->resizeGetHeight(_itemWidth); + _itemHeight = item->resizeGetHeight(_itemWidth); } } break; @@ -378,7 +378,7 @@ int ListSection::recountHeight() { case Type::Video: case Type::PhotoVideo: // #TODO stories case Type::RoundFile: { - auto itemHeight = _itemWidth + st::infoMediaSkip; + auto itemHeight = _itemHeight + st::infoMediaSkip; auto index = 0; result += _itemsTop; for (auto &item : _items) { diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.h b/Telegram/SourceFiles/info/media/info_media_list_section.h index 77666a31a..358712a75 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.h +++ b/Telegram/SourceFiles/info/media/info_media_list_section.h @@ -82,6 +82,7 @@ private: int _itemsLeft = 0; int _itemsTop = 0; int _itemWidth = 0; + int _itemHeight = 0; int _itemsInRow = 1; mutable int _rowsCount = 0; int _top = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index f2d8dcde7..3e75975fa 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -421,19 +421,21 @@ std::unique_ptr Provider::createLayout( } return nullptr; }; - const auto spoiler = [&] { - if (const auto media = item->media()) { - return media->hasSpoiler(); - } - return false; - }; const auto &songSt = st::overviewFileLayout; using namespace Overview::Layout; + const auto options = [&] { + const auto media = item->media(); + return MediaOptions{ .spoiler = media && media->hasSpoiler() }; + }; switch (type) { case Type::Photo: if (const auto photo = getPhoto()) { - return std::make_unique(delegate, item, photo, spoiler()); + return std::make_unique( + delegate, + item, + photo, + options()); } return nullptr; case Type::GIF: @@ -443,7 +445,7 @@ std::unique_ptr Provider::createLayout( return nullptr; case Type::Video: if (const auto file = getFile()) { - return std::make_unique