Improve saved / archive stories design.

This commit is contained in:
John Preston 2023-06-19 21:00:34 +04:00
parent 119ee6044a
commit e98770d418
29 changed files with 660 additions and 202 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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_kick_channel" = "Remove {user} from the channel?";
"lng_profile_sure_remove_admin" = "Remove {user} from admins?"; "lng_profile_sure_remove_admin" = "Remove {user} from admins?";
"lng_profile_loading" = "Loading..."; "lng_profile_loading" = "Loading...";
"lng_profile_stories#one" = "{count} story"; "lng_profile_saved_stories#one" = "{count} saved story";
"lng_profile_stories#other" = "{count} stories"; "lng_profile_saved_stories#other" = "{count} saved stories";
"lng_profile_photos#one" = "{count} photo"; "lng_profile_photos#one" = "{count} photo";
"lng_profile_photos#other" = "{count} photos"; "lng_profile_photos#other" = "{count} photos";
"lng_profile_gifs#one" = "{count} GIF"; "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_views#other" = "{count} views";
"lng_stories_no_views" = "No views"; "lng_stories_no_views" = "No views";
"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; "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_my_title" = "Saved Stories";
"lng_stories_archive_button" = "Archive"; "lng_stories_archive_button" = "Stories Archive";
"lng_stories_recent_button" = "Recent Stories";
"lng_stories_archive_title" = "Stories Archive"; "lng_stories_archive_title" = "Stories Archive";
"lng_stories_reply_sent" = "Message Sent"; "lng_stories_reply_sent" = "Message Sent";
"lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list."; "lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";

View File

@ -82,6 +82,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
auto stories = object_ptr<Stories::List>( auto stories = object_ptr<Stories::List>(
box, box,
st::dialogsStoriesList,
Stories::ContentForSession( Stories::ContentForSession(
&sessionController->session(), &sessionController->session(),
Data::StorySourcesList::All), Data::StorySourcesList::All),

View File

@ -423,6 +423,7 @@ void Stories::apply(const MTPDupdateStory &data) {
if (!user->hasStoriesHidden()) { if (!user->hasStoriesHidden()) {
refreshInList(StorySourcesList::NotHidden); refreshInList(StorySourcesList::NotHidden);
} }
_sourceChanged.fire_copy(peerId);
} }
void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) { void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {

View File

@ -500,6 +500,12 @@ DialogsStories {
nameTop: pixels; nameTop: pixels;
nameStyle: TextStyle; nameStyle: TextStyle;
} }
DialogsStoriesList {
small: DialogsStories;
full: DialogsStories;
bg: color;
readOpacity: double;
}
dialogsStories: DialogsStories { dialogsStories: DialogsStories {
left: 4px; left: 4px;
@ -532,3 +538,16 @@ dialogsStoriesFull: DialogsStories {
linkFontOver: font(11px); linkFontOver: font(11px);
} }
} }
dialogsStoriesList: DialogsStoriesList {
small: dialogsStories;
full: dialogsStoriesFull;
bg: dialogsBg;
readOpacity: 0.6;
}
dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) {
bg: transparent;
}
dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) {
readOpacity: 1.;
}

View File

@ -142,6 +142,7 @@ InnerWidget::InnerWidget(
, _controller(controller) , _controller(controller)
, _stories(std::make_unique<Stories::List>( , _stories(std::make_unique<Stories::List>(
this, this,
st::dialogsStoriesList,
Stories::ContentForSession( Stories::ContentForSession(
&controller->session(), &controller->session(),
Data::StorySourcesList::NotHidden), Data::StorySourcesList::NotHidden),

View File

@ -8,6 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_content.h"
#include "data/data_changes.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_session.h"
#include "data/data_stories.h" #include "data/data_stories.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -19,7 +24,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Dialogs::Stories { namespace Dialogs::Stories {
namespace { namespace {
class PeerUserpic final : public Userpic { constexpr auto kShownLastCount = 3;
class PeerUserpic final : public Thumbnail {
public: public:
explicit PeerUserpic(not_null<PeerData*> peer); explicit PeerUserpic(not_null<PeerData*> 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<void()> 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<PhotoData*> photo, FullStoryId id);
private:
Main::Session &session() override;
Thumb loaded(FullStoryId id) override;
void clear() override;
const not_null<PhotoData*> _photo;
std::shared_ptr<Data::PhotoMedia> _media;
};
class VideoThumbnail final : public StoryThumbnail {
public:
VideoThumbnail(not_null<DocumentData*> video, FullStoryId id);
private:
Main::Session &session() override;
Thumb loaded(FullStoryId id) override;
void clear() override;
const not_null<DocumentData*> _video;
std::shared_ptr<Data::DocumentMedia> _media;
};
class EmptyThumbnail final : public Thumbnail {
public:
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
QImage _cached;
};
class State final { class State final {
public: public:
State(not_null<Data::Stories*> data, Data::StorySourcesList list); State(not_null<Data::Stories*> data, Data::StorySourcesList list);
@ -57,7 +128,9 @@ public:
private: private:
const not_null<Data::Stories*> _data; const not_null<Data::Stories*> _data;
const Data::StorySourcesList _list; const Data::StorySourcesList _list;
base::flat_map<not_null<UserData*>, std::shared_ptr<Userpic>> _userpics; base::flat_map<
not_null<UserData*>,
std::shared_ptr<Thumbnail>> _userpics;
}; };
@ -122,6 +195,127 @@ void PeerUserpic::processNewPhoto() {
}, _subscribed->downloadLifetime); }, _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<void()> 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<PhotoData*> 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<DocumentData*> 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<void()> callback) {
}
State::State(not_null<Data::Stories*> data, Data::StorySourcesList list) State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
: _data(data) : _data(data)
, _list(list) { , _list(list) {
@ -130,12 +324,12 @@ State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
Content State::next() { Content State::next() {
auto result = Content{ .full = (_list == Data::StorySourcesList::All) }; auto result = Content{ .full = (_list == Data::StorySourcesList::All) };
const auto &sources = _data->sources(_list); const auto &sources = _data->sources(_list);
result.users.reserve(sources.size()); result.elements.reserve(sources.size());
for (const auto &info : sources) { for (const auto &info : sources) {
const auto source = _data->source(info.id); const auto source = _data->source(info.id);
Assert(source != nullptr); Assert(source != nullptr);
auto userpic = std::shared_ptr<Userpic>(); auto userpic = std::shared_ptr<Thumbnail>();
const auto user = source->user; const auto user = source->user;
if (const auto i = _userpics.find(user); i != end(_userpics)) { if (const auto i = _userpics.find(user); i != end(_userpics)) {
userpic = i->second; userpic = i->second;
@ -143,15 +337,15 @@ Content State::next() {
userpic = std::make_shared<PeerUserpic>(user); userpic = std::make_shared<PeerUserpic>(user);
_userpics.emplace(user, userpic); _userpics.emplace(user, userpic);
} }
result.users.push_back({ result.elements.push_back({
.id = uint64(user->id.value), .id = uint64(user->id.value),
.name = (user->isSelf() .name = (user->isSelf()
? tr::lng_stories_my_name(tr::now) ? tr::lng_stories_my_name(tr::now)
: user->shortName()), : user->shortName()),
.userpic = std::move(userpic), .thumbnail = std::move(userpic),
.unread = info.unread, .unread = info.unread,
.hidden = info.hidden, .hidden = info.hidden,
.self = user->isSelf(), .skipSmall = user->isSelf(),
}); });
} }
return result; return result;
@ -177,4 +371,88 @@ rpl::producer<Content> ContentForSession(
}; };
} }
[[nodiscard]] std::shared_ptr<Thumbnail> PrepareThumbnail(
not_null<Data::Story*> story) {
using Result = std::shared_ptr<Thumbnail>;
const auto id = story->fullId();
return v::match(story->media().data, [](v::null_t) -> Result {
return std::make_shared<EmptyThumbnail>();
}, [&](not_null<PhotoData*> photo) -> Result {
return std::make_shared<PhotoThumbnail>(photo, id);
}, [&](not_null<DocumentData*> video) -> Result {
return std::make_shared<VideoThumbnail>(video, id);
});
}
rpl::producer<Content> LastForPeer(not_null<PeerData*> 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<StoryId>();
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<Content>([=](auto consumer) {
auto lifetime = rpl::lifetime();
struct State {
Fn<void()> check;
base::has_weak_ptr guard;
bool pushed = false;
};
const auto state = lifetime.make_state<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 } // namespace Dialogs::Stories

View File

@ -23,4 +23,6 @@ struct Content;
not_null<Main::Session*> session, not_null<Main::Session*> session,
Data::StorySourcesList list); Data::StorySourcesList list);
[[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);
} // namespace Dialogs::Stories } // namespace Dialogs::Stories

View File

@ -17,13 +17,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Dialogs::Stories { namespace Dialogs::Stories {
namespace { namespace {
constexpr auto kSmallUserpicsShown = 3; constexpr auto kSmallThumbsShown = 3;
constexpr auto kSmallReadOpacity = 0.6;
constexpr auto kSummaryExpandLeft = 1.5; constexpr auto kSummaryExpandLeft = 1.5;
constexpr auto kPreloadPages = 2; constexpr auto kPreloadPages = 2;
[[nodiscard]] int AvailableNameWidth() { [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
const auto &full = st::dialogsStoriesFull; const auto &full = st.full;
const auto &font = full.nameStyle.font; const auto &font = full.nameStyle.font;
const auto skip = font->spacew; const auto skip = font->spacew;
return full.photoLeft * 2 + full.photo - 2 * skip; return full.photoLeft * 2 + full.photo - 2 * skip;
@ -35,7 +34,7 @@ struct List::Layout {
int itemsCount = 0; int itemsCount = 0;
int shownHeight = 0; int shownHeight = 0;
float64 ratio = 0.; float64 ratio = 0.;
float64 userpicLeft = 0.; float64 thumbnailLeft = 0.;
float64 photoLeft = 0.; float64 photoLeft = 0.;
float64 left = 0.; float64 left = 0.;
float64 single = 0.; float64 single = 0.;
@ -52,9 +51,11 @@ struct List::Layout {
List::List( List::List(
not_null<QWidget*> parent, not_null<QWidget*> parent,
const style::DialogsStoriesList &st,
rpl::producer<Content> content, rpl::producer<Content> content,
Fn<int()> shownHeight) Fn<int()> shownHeight)
: RpWidget(parent) : RpWidget(parent)
, _st(st)
, _shownHeight(shownHeight) { , _shownHeight(shownHeight) {
setCursor(style::cur_default); setCursor(style::cur_default);
@ -64,45 +65,46 @@ List::List(
_shownAnimation.stop(); _shownAnimation.stop();
setMouseTracking(true); setMouseTracking(true);
resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); resize(0, _data.empty() ? 0 : st.full.height);
} }
void List::showContent(Content &&content) { void List::showContent(Content &&content) {
if (_content == content) { if (_content == content) {
return; return;
} }
if (content.users.empty()) { if (content.elements.empty()) {
_hidingData = base::take(_data); _hidingData = base::take(_data);
if (!_hidingData.empty()) { if (!_hidingData.empty()) {
toggleAnimated(false); toggleAnimated(false);
} }
return; return;
} }
const auto hidden = _content.users.empty(); const auto hidden = _content.elements.empty();
_content = std::move(content); _content = std::move(content);
auto items = base::take( auto items = base::take(
_data.items.empty() ? _hidingData.items : _data.items); _data.items.empty() ? _hidingData.items : _data.items);
_hidingData = {}; _hidingData = {};
_data.items.reserve(_content.users.size()); _data.items.reserve(_content.elements.size());
for (const auto &user : _content.users) { for (const auto &element : _content.elements) {
const auto i = ranges::find(items, user.id, [](const Item &item) { const auto id = element.id;
return item.user.id; const auto i = ranges::find(items, id, [](const Item &item) {
return item.element.id;
}); });
if (i != end(items)) { if (i != end(items)) {
_data.items.push_back(std::move(*i)); _data.items.push_back(std::move(*i));
auto &item = _data.items.back(); auto &item = _data.items.back();
if (item.user.userpic != user.userpic) { if (item.element.thumbnail != element.thumbnail) {
item.user.userpic = user.userpic; item.element.thumbnail = element.thumbnail;
item.subscribed = false; item.subscribed = false;
} }
if (item.user.name != user.name) { if (item.element.name != element.name) {
item.user.name = user.name; item.element.name = element.name;
item.nameCache = QImage(); item.nameCache = QImage();
} }
item.user.unread = user.unread; item.element.unread = element.unread;
item.user.hidden = user.hidden; item.element.hidden = element.hidden;
} else { } else {
_data.items.emplace_back(Item{ .user = user }); _data.items.emplace_back(Item{ .element = element });
} }
} }
updateScrollMax(); updateScrollMax();
@ -115,23 +117,25 @@ void List::showContent(Content &&content) {
List::Summaries List::ComposeSummaries(Data &data) { List::Summaries List::ComposeSummaries(Data &data) {
const auto total = int(data.items.size()); 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 unreadInFirst = 0;
auto unreadTotal = 0; auto unreadTotal = 0;
for (auto i = skip; i != total; ++i) { for (auto i = skip; i != total; ++i) {
if (data.items[i].user.unread) { if (data.items[i].element.unread) {
++unreadTotal; ++unreadTotal;
if (i < skip + kSmallUserpicsShown) { if (i < skip + kSmallThumbsShown) {
++unreadInFirst; ++unreadInFirst;
} }
} }
} }
auto result = Summaries{ .skipSelf = (skip > 0) }; auto result = Summaries{ .skipOne = (skip > 0) };
result.total.string result.total.string
= tr::lng_stories_row_count(tr::now, lt_count, total); = tr::lng_stories_row_count(tr::now, lt_count, total);
const auto append = [&](QString &to, int index, bool last) { const auto append = [&](QString &to, int index, bool last) {
if (to.isEmpty()) { if (to.isEmpty()) {
to = data.items[index].user.name; to = data.items[index].element.name;
} else { } else {
to = (last to = (last
? tr::lng_stories_row_unread_and_last ? tr::lng_stories_row_unread_and_last
@ -140,19 +144,19 @@ List::Summaries List::ComposeSummaries(Data &data) {
lt_accumulated, lt_accumulated,
to, to,
lt_user, lt_user,
data.items[index].user.name); data.items[index].element.name);
} }
}; };
if (!total) { if (!total) {
return result; return result;
} else if (total <= skip + kSmallUserpicsShown) { } else if (total <= skip + kSmallThumbsShown) {
for (auto i = skip; i != total; ++i) { for (auto i = skip; i != total; ++i) {
append(result.allNames.string, i, i == total - 1); append(result.allNames.string, i, i == total - 1);
} }
} }
if (unreadInFirst > 0 && unreadInFirst == unreadTotal) { if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
for (auto i = skip; i != total; ++i) { 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); 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); && (a.unreadNames.string == b.unreadNames.string);
} }
void List::Populate(Summary &summary) { void List::Populate(
const style::DialogsStories &st,
Summary &summary) {
if (summary.empty()) { if (summary.empty()) {
return; return;
} }
summary.cache = QImage(); summary.cache = QImage();
summary.text = Ui::Text::String( summary.text = Ui::Text::String(st.nameStyle, summary.string);
st::dialogsStories.nameStyle,
summary.string);
} }
void List::Populate(Summaries &summaries) { void List::Populate(
Populate(summaries.total); const style::DialogsStories &st,
Populate(summaries.allNames); Summaries &summaries) {
Populate(summaries.unreadNames); Populate(st, summaries.total);
Populate(st, summaries.allNames);
Populate(st, summaries.unreadNames);
} }
void List::updateSummary(Data &data) { void List::updateSummary(Data &data) {
@ -188,7 +194,7 @@ void List::updateSummary(Data &data) {
return; return;
} }
data.summaries = std::move(summaries); data.summaries = std::move(summaries);
Populate(data.summaries); Populate(_st.small, data.summaries);
} }
void List::toggleAnimated(bool shown) { void List::toggleAnimated(bool shown) {
@ -203,14 +209,14 @@ void List::updateHeight() {
const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.); const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.);
resize( resize(
width(), width(),
anim::interpolate(0, st::dialogsStoriesFull.height, shown)); anim::interpolate(0, _st.full.height, shown));
if (_data.empty() && shown == 0.) { if (_data.empty() && shown == 0.) {
_hidingData = {}; _hidingData = {};
} }
} }
void List::updateScrollMax() { void List::updateScrollMax() {
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto singleFull = full.photoLeft * 2 + full.photo; const auto singleFull = full.photoLeft * 2 + full.photo;
const auto widthFull = full.left + int(_data.items.size()) * singleFull; const auto widthFull = full.left + int(_data.items.size()) * singleFull;
_scrollLeftMax = std::max(widthFull - width(), 0); _scrollLeftMax = std::max(widthFull - width(), 0);
@ -252,8 +258,8 @@ void List::resizeEvent(QResizeEvent *e) {
} }
List::Layout List::computeLayout() const { List::Layout List::computeLayout() const {
const auto &st = st::dialogsStories; const auto &st = _st.small;
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto shownHeight = std::max(_shownHeight(), st.height); const auto shownHeight = std::max(_shownHeight(), st.height);
const auto ratio = float64(shownHeight - st.height) const auto ratio = float64(shownHeight - st.height)
/ (full.height - st.height); / (full.height - st.height);
@ -267,11 +273,12 @@ List::Layout List::computeLayout() const {
+ st::defaultDialogRow.photoSize + st::defaultDialogRow.photoSize
+ st::defaultDialogRow.padding.left(); + st::defaultDialogRow.padding.left();
const auto narrow = (width() <= narrowWidth); 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 ? 1
: 0; : 0;
const auto smallCount = std::min( const auto smallCount = std::min(
kSmallUserpicsShown, kSmallThumbsShown,
itemsCount - smallSkip); itemsCount - smallSkip);
const auto smallWidth = st.photo + (smallCount - 1) * st.shift; const auto smallWidth = st.photo + (smallCount - 1) * st.shift;
const auto leftSmall = (narrow const auto leftSmall = (narrow
@ -288,17 +295,17 @@ List::Layout List::computeLayout() const {
const auto startIndexSmall = std::min(startIndexFull, smallSkip); const auto startIndexSmall = std::min(startIndexFull, smallSkip);
const auto endIndexSmall = smallSkip + smallCount; const auto endIndexSmall = smallSkip + smallCount;
const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift); const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift);
const auto userpicLeftFull = cellLeftFull + full.photoLeft; const auto thumbnailLeftFull = cellLeftFull + full.photoLeft;
const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft;
const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull);
const auto photoLeft = lerp(st.photoLeft, full.photoLeft); const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
return Layout{ return Layout{
.itemsCount = itemsCount, .itemsCount = itemsCount,
.shownHeight = shownHeight, .shownHeight = shownHeight,
.ratio = ratio, .ratio = ratio,
.userpicLeft = userpicLeft, .thumbnailLeft = thumbnailLeft,
.photoLeft = photoLeft, .photoLeft = photoLeft,
.left = userpicLeft - photoLeft, .left = thumbnailLeft - photoLeft,
.single = lerp(st.shift, singleFull), .single = lerp(st.shift, singleFull),
.smallSkip = smallSkip, .smallSkip = smallSkip,
.leftFull = leftFull, .leftFull = leftFull,
@ -313,8 +320,8 @@ List::Layout List::computeLayout() const {
} }
void List::paintEvent(QPaintEvent *e) { void List::paintEvent(QPaintEvent *e) {
const auto &st = st::dialogsStories; const auto &st = _st.small;
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto layout = computeLayout(); const auto layout = computeLayout();
const auto ratio = layout.ratio; const auto ratio = layout.ratio;
const auto lerp = [&](float64 a, float64 b) { const auto lerp = [&](float64 a, float64 b) {
@ -331,14 +338,14 @@ void List::paintEvent(QPaintEvent *e) {
+ (photoTop + (photo / 2.)); + (photoTop + (photo / 2.));
const auto nameScale = layout.shownHeight / float64(full.height); const auto nameScale = layout.shownHeight / float64(full.height);
const auto nameTop = nameScale * full.nameTop; 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 nameHeight = nameScale * full.nameStyle.font->height;
const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); const auto readUserpicOpacity = lerp(_st.readOpacity, 1.);
const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.);
auto p = QPainter(this); auto p = QPainter(this);
p.fillRect(e->rect(), st::dialogsBg); p.fillRect(e->rect(), _st.bg);
p.translate(0, height() - layout.shownHeight); p.translate(0, height() - layout.shownHeight);
const auto drawSmall = (ratio < 1.); const auto drawSmall = (ratio < 1.);
@ -375,8 +382,8 @@ void List::paintEvent(QPaintEvent *e) {
return Single{ x, indexSmall, small, indexFull, full }; return Single{ x, indexSmall, small, indexFull, full };
}; };
const auto hasUnread = [&](const Single &single) { const auto hasUnread = [&](const Single &single) {
return (single.itemSmall && single.itemSmall->user.unread) return (single.itemSmall && single.itemSmall->element.unread)
|| (single.itemFull && single.itemFull->user.unread); || (single.itemFull && single.itemFull->element.unread);
}; };
const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
auto nextGradientPainted = false; auto nextGradientPainted = false;
@ -398,7 +405,9 @@ void List::paintEvent(QPaintEvent *e) {
} }
if (i > first && hasUnread(current) && next) { if (i > first && hasUnread(current) && next) {
if (current.itemSmall || !next.itemSmall) { 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)) { if (const auto skipped = lookup(i - 2)) {
skippedPainted = true; skippedPainted = true;
paintGradient(skipped); paintGradient(skipped);
@ -425,11 +434,15 @@ void List::paintEvent(QPaintEvent *e) {
// Unread gradient. // Unread gradient.
const auto x = single.x; 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 small = single.itemSmall;
const auto itemFull = single.itemFull; const auto itemFull = single.itemFull;
const auto smallUnread = small && small->user.unread; const auto smallUnread = small && small->element.unread;
const auto fullUnread = itemFull && itemFull->user.unread; const auto fullUnread = itemFull && itemFull->element.unread;
const auto unreadOpacity = (smallUnread && fullUnread) const auto unreadOpacity = (smallUnread && fullUnread)
? 1. ? 1.
: smallUnread : smallUnread
@ -458,11 +471,15 @@ void List::paintEvent(QPaintEvent *e) {
Expects(single.itemSmall || single.itemFull); Expects(single.itemSmall || single.itemFull);
const auto x = single.x; 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 small = single.itemSmall;
const auto itemFull = single.itemFull; const auto itemFull = single.itemFull;
const auto smallUnread = small && small->user.unread; const auto smallUnread = small && small->element.unread;
const auto fullUnread = itemFull && itemFull->user.unread; const auto fullUnread = itemFull && itemFull->element.unread;
// White circle with possible read gray line. // White circle with possible read gray line.
const auto hasReadLine = (itemFull && !fullUnread); const auto hasReadLine = (itemFull && !fullUnread);
@ -483,25 +500,27 @@ void List::paintEvent(QPaintEvent *e) {
// Userpic. // Userpic.
if (itemFull == small) { if (itemFull == small) {
p.setOpacity(smallUnread ? 1. : readUserpicOpacity); p.setOpacity(smallUnread ? 1. : readUserpicOpacity);
validateUserpic(itemFull); validateThumbnail(itemFull);
const auto size = full.photo; const auto size = full.photo;
p.drawImage(userpic, itemFull->user.userpic->image(size)); p.drawImage(userpic, itemFull->element.thumbnail->image(size));
} else { } else {
if (small) { if (small) {
p.setOpacity(smallUnread p.setOpacity(smallUnread
? (itemFull ? 1. : (1. - ratio)) ? (itemFull ? 1. : (1. - ratio))
: (itemFull : (itemFull
? kSmallReadOpacity ? _st.readOpacity
: readUserpicAppearingOpacity)); : readUserpicAppearingOpacity));
validateUserpic(small); validateThumbnail(small);
const auto size = (ratio > 0.) ? full.photo : st.photo; 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) { if (itemFull) {
p.setOpacity(ratio); p.setOpacity(ratio);
validateUserpic(itemFull); validateThumbnail(itemFull);
const auto size = full.photo; const auto size = full.photo;
p.drawImage(userpic, itemFull->user.userpic->image(size)); p.drawImage(
userpic,
itemFull->element.thumbnail->image(size));
} }
} }
p.setOpacity(1.); p.setOpacity(1.);
@ -510,11 +529,11 @@ void List::paintEvent(QPaintEvent *e) {
paintSummary(p, rendering, summaryTop, ratio); paintSummary(p, rendering, summaryTop, ratio);
} }
void List::validateUserpic(not_null<Item*> item) { void List::validateThumbnail(not_null<Item*> item) {
if (!item->subscribed) { if (!item->subscribed) {
item->subscribed = true; item->subscribed = true;
//const auto id = item.user.id; //const auto id = item.element.id;
item->user.userpic->subscribeToUpdates([=] { item->element.thumbnail->subscribeToUpdates([=] {
update(); update();
}); });
} }
@ -525,10 +544,10 @@ void List::validateName(not_null<Item*> item) {
if (!item->nameCache.isNull() && item->nameCacheColor == color->c) { if (!item->nameCache.isNull() && item->nameCacheColor == color->c) {
return; return;
} }
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto &font = full.nameStyle.font; const auto &font = full.nameStyle.font;
const auto available = AvailableNameWidth(); const auto available = AvailableNameWidth(_st);
const auto text = Ui::Text::String(full.nameStyle, item->user.name); const auto text = Ui::Text::String(full.nameStyle, item->element.name);
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
item->nameCacheColor = color->c; item->nameCacheColor = color->c;
item->nameCache = QImage( item->nameCache = QImage(
@ -542,13 +561,13 @@ void List::validateName(not_null<Item*> item) {
} }
List::Summary &List::ChooseSummary( List::Summary &List::ChooseSummary(
const style::DialogsStories &st,
Summaries &summaries, Summaries &summaries,
int totalItems, int totalItems,
int fullWidth) { int fullWidth) {
const auto &st = st::dialogsStories;
const auto used = std::min( const auto used = std::min(
totalItems - (summaries.skipSelf ? 1 : 0), totalItems - (summaries.skipOne ? 1 : 0),
kSmallUserpicsShown); kSmallThumbsShown);
const auto taken = st.left const auto taken = st.left
+ st.photoLeft + st.photoLeft
+ st.photo + st.photo
@ -572,13 +591,14 @@ List::Summary &List::ChooseSummary(
return summaries.total; return summaries.total;
} }
void List::PrerenderSummary(Summary &summary) { void List::PrerenderSummary(
const style::DialogsStories &st,
Summary &summary) {
if (!summary.cache.isNull() if (!summary.cache.isNull()
&& summary.cacheForWidth == summary.available && summary.cacheForWidth == summary.available
&& summary.cacheColor == st::dialogsNameFg->c) { && summary.cacheColor == st::dialogsNameFg->c) {
return; return;
} }
const auto &st = st::dialogsStories;
const auto use = std::min(summary.text.maxWidth(), summary.available); const auto use = std::min(summary.text.maxWidth(), summary.available);
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
summary.cache = QImage( summary.cache = QImage(
@ -597,16 +617,20 @@ void List::paintSummary(
float64 summaryTop, float64 summaryTop,
float64 hidden) { float64 hidden) {
const auto total = int(data.items.size()); const auto total = int(data.items.size());
auto &summary = ChooseSummary(data.summaries, total, width()); auto &summary = ChooseSummary(
PrerenderSummary(summary); _st.small,
data.summaries,
total,
width());
PrerenderSummary(_st.small, summary);
const auto lerp = [&](float64 from, float64 to) { const auto lerp = [&](float64 from, float64 to) {
return from + (to - from) * hidden; return from + (to - from) * hidden;
}; };
const auto &st = st::dialogsStories; const auto &st = _st.small;
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto used = std::min( const auto used = std::min(
total - (data.summaries.skipSelf ? 1 : 0), total - (data.summaries.skipOne ? 1 : 0),
kSmallUserpicsShown); kSmallThumbsShown);
const auto fullLeft = st.left const auto fullLeft = st.left
+ st.photoLeft + st.photoLeft
+ st.photo + st.photo
@ -671,7 +695,7 @@ void List::mouseMoveEvent(QMouseEvent *e) {
if (!_dragging && _mouseDownPosition) { if (!_dragging && _mouseDownPosition) {
if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
>= QApplication::startDragDistance()) { >= QApplication::startDragDistance()) {
if (_shownHeight() < st::dialogsStoriesFull.height) { if (_shownHeight() < _st.full.height) {
_expandRequests.fire({}); _expandRequests.fire({});
} }
_dragging = true; _dragging = true;
@ -718,7 +742,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
if (_selected < 0) { if (_selected < 0) {
_expandRequests.fire({}); _expandRequests.fire({});
} else if (_selected < _data.items.size()) { } 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]; auto &item = _data.items[_selected];
_menu = base::make_unique_q<Ui::PopupMenu>(this); _menu = base::make_unique_q<Ui::PopupMenu>(this);
const auto id = item.user.id; const auto id = item.element.id;
const auto hidden = item.user.hidden; const auto hidden = item.element.hidden;
_menu->addAction(tr::lng_context_view_profile(tr::now), [=] { _menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
_showProfileRequests.fire_copy(id); _showProfileRequests.fire_copy(id);
}); });
@ -781,8 +805,8 @@ void List::updateSelected() {
if (_pressed >= 0) { if (_pressed >= 0) {
return; return;
} }
const auto &st = st::dialogsStories; const auto &st = _st.small;
const auto &full = st::dialogsStoriesFull; const auto &full = _st.full;
const auto p = mapFromGlobal(_lastMousePosition); const auto p = mapFromGlobal(_lastMousePosition);
const auto layout = computeLayout(); const auto layout = computeLayout();
const auto firstRightFull = layout.leftFull const auto firstRightFull = layout.leftFull

View File

@ -13,31 +13,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class QPainter; class QPainter;
namespace style {
struct DialogsStories;
struct DialogsStoriesList;
} // namespace style
namespace Ui { namespace Ui {
class PopupMenu; class PopupMenu;
} // namespace Ui } // namespace Ui
namespace Dialogs::Stories { namespace Dialogs::Stories {
class Userpic { class Thumbnail {
public: public:
[[nodiscard]] virtual QImage image(int size) = 0; [[nodiscard]] virtual QImage image(int size) = 0;
virtual void subscribeToUpdates(Fn<void()> callback) = 0; virtual void subscribeToUpdates(Fn<void()> callback) = 0;
}; };
struct User { struct Element {
uint64 id = 0; uint64 id = 0;
QString name; QString name;
std::shared_ptr<Userpic> userpic; std::shared_ptr<Thumbnail> thumbnail;
bool unread = false; bool unread = false;
bool hidden = 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 { struct Content {
std::vector<User> users; std::vector<Element> elements;
bool full = false; bool full = false;
friend inline bool operator==( friend inline bool operator==(
@ -54,6 +61,7 @@ class List final : public Ui::RpWidget {
public: public:
List( List(
not_null<QWidget*> parent, not_null<QWidget*> parent,
const style::DialogsStoriesList &st,
rpl::producer<Content> content, rpl::producer<Content> content,
Fn<int()> shownHeight); Fn<int()> shownHeight);
@ -67,7 +75,7 @@ public:
private: private:
struct Layout; struct Layout;
struct Item { struct Item {
User user; Element element;
QImage nameCache; QImage nameCache;
QColor nameCacheColor; QColor nameCacheColor;
bool subscribed = false; bool subscribed = false;
@ -88,7 +96,7 @@ private:
Summary total; Summary total;
Summary allNames; Summary allNames;
Summary unreadNames; Summary unreadNames;
bool skipSelf = false; bool skipOne = false;
}; };
struct Data { struct Data {
std::vector<Item> items; std::vector<Item> items;
@ -103,13 +111,20 @@ private:
[[nodiscard]] static bool StringsEqual( [[nodiscard]] static bool StringsEqual(
const Summaries &a, const Summaries &a,
const Summaries &b); const Summaries &b);
static void Populate(Summary &summary); static void Populate(
static void Populate(Summaries &summaries); const style::DialogsStories &st,
Summary &summary);
static void Populate(
const style::DialogsStories &st,
Summaries &summaries);
[[nodiscard]] static Summary &ChooseSummary( [[nodiscard]] static Summary &ChooseSummary(
const style::DialogsStories &st,
Summaries &summaries, Summaries &summaries,
int totalItems, int totalItems,
int fullWidth); int fullWidth);
static void PrerenderSummary(Summary &summary); static void PrerenderSummary(
const style::DialogsStories &st,
Summary &summary);
void showContent(Content &&content); void showContent(Content &&content);
void enterEventHook(QEnterEvent *e) override; void enterEventHook(QEnterEvent *e) override;
@ -121,7 +136,7 @@ private:
void mouseReleaseEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override;
void validateUserpic(not_null<Item*> item); void validateThumbnail(not_null<Item*> item);
void validateName(not_null<Item*> item); void validateName(not_null<Item*> item);
void updateScrollMax(); void updateScrollMax();
void updateSummary(Data &data); void updateSummary(Data &data);
@ -140,6 +155,7 @@ private:
[[nodiscard]] Layout computeLayout() const; [[nodiscard]] Layout computeLayout() const;
const style::DialogsStoriesList &_st;
Content _content; Content _content;
Data _data; Data _data;
Data _hidingData; Data _hidingData;

View File

@ -1979,6 +1979,8 @@ bool HistoryItem::forbidsSaving() const {
bool HistoryItem::canDelete() const { bool HistoryItem::canDelete() const {
if (isSponsored()) { if (isSponsored()) {
return false; return false;
} else if (IsStoryMsgId(id)) {
return false && _history->peer->isSelf(); // #TODO stories
} else if (isService() && !isRegular()) { } else if (isService() && !isRegular()) {
return false; return false;
} else if (topicRootId() == id) { } else if (topicRootId() == id) {

View File

@ -368,6 +368,8 @@ infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }};
infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }}; infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }};
infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }}; infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }};
infoIconMediaStories: icon {{ "info/info_media_stories", 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 }}; infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }};
infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }}; infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }};

View File

@ -534,6 +534,8 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const {
case Type::MusicFile: return tr::lng_media_selected_song; case Type::MusicFile: return tr::lng_media_selected_song;
case Type::Link: return tr::lng_media_selected_link; case Type::Link: return tr::lng_media_selected_link;
case Type::RoundVoiceFile: return tr::lng_media_selected_audio; 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()"); Unexpected("Type in TopBar::generateSelectedText()");
}(); }();

View File

@ -142,7 +142,7 @@ inline auto AddStoriesButton(
parent, parent,
std::move(count), std::move(count),
[](int 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(); tracker)->entity();
result->addClickHandler([=] { result->addClickHandler([=] {

View File

@ -347,7 +347,7 @@ void ListSection::resizeToWidth(int newWidth) {
_itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
- st::infoMediaSkip; - st::infoMediaSkip;
for (auto &item : _items) { for (auto &item : _items) {
item->resizeGetHeight(_itemWidth); _itemHeight = item->resizeGetHeight(_itemWidth);
} }
} break; } break;
@ -378,7 +378,7 @@ int ListSection::recountHeight() {
case Type::Video: case Type::Video:
case Type::PhotoVideo: // #TODO stories case Type::PhotoVideo: // #TODO stories
case Type::RoundFile: { case Type::RoundFile: {
auto itemHeight = _itemWidth + st::infoMediaSkip; auto itemHeight = _itemHeight + st::infoMediaSkip;
auto index = 0; auto index = 0;
result += _itemsTop; result += _itemsTop;
for (auto &item : _items) { for (auto &item : _items) {

View File

@ -82,6 +82,7 @@ private:
int _itemsLeft = 0; int _itemsLeft = 0;
int _itemsTop = 0; int _itemsTop = 0;
int _itemWidth = 0; int _itemWidth = 0;
int _itemHeight = 0;
int _itemsInRow = 1; int _itemsInRow = 1;
mutable int _rowsCount = 0; mutable int _rowsCount = 0;
int _top = 0; int _top = 0;

View File

@ -421,19 +421,21 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
} }
return nullptr; return nullptr;
}; };
const auto spoiler = [&] {
if (const auto media = item->media()) {
return media->hasSpoiler();
}
return false;
};
const auto &songSt = st::overviewFileLayout; const auto &songSt = st::overviewFileLayout;
using namespace Overview::Layout; using namespace Overview::Layout;
const auto options = [&] {
const auto media = item->media();
return MediaOptions{ .spoiler = media && media->hasSpoiler() };
};
switch (type) { switch (type) {
case Type::Photo: case Type::Photo:
if (const auto photo = getPhoto()) { if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(delegate, item, photo, spoiler()); return std::make_unique<Photo>(
delegate,
item,
photo,
options());
} }
return nullptr; return nullptr;
case Type::GIF: case Type::GIF:
@ -443,7 +445,7 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
return nullptr; return nullptr;
case Type::Video: case Type::Video:
if (const auto file = getFile()) { if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, spoiler()); return std::make_unique<Video>(delegate, item, file, options());
} }
return nullptr; return nullptr;
case Type::File: case Type::File:

View File

@ -8,17 +8,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/stories/info_stories_inner_widget.h" #include "info/stories/info_stories_inner_widget.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "info/media/info_media_list_widget.h" #include "info/media/info_media_list_widget.h"
#include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_icon.h"
#include "info/stories/info_stories_widget.h" #include "info/stories/info_stories_widget.h"
#include "info/info_controller.h" #include "info/info_controller.h"
#include "info/info_memento.h" #include "info/info_memento.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_common.h" #include "settings/settings_common.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "styles/style_dialogs.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_settings.h"
namespace Info::Stories { namespace Info::Stories {
@ -99,38 +108,112 @@ void InnerWidget::setupArchive() {
&& _isStackBottom) { && _isStackBottom) {
createArchiveButton(); createArchiveButton();
} else { } else {
_archive.destroy(); _buttons.destroy();
refreshHeight(); refreshHeight();
} }
} }
void InnerWidget::createArchiveButton() { void InnerWidget::createArchiveButton() {
_archive.create(this); _buttons.create(this);
_archive->show(); _buttons->show();
const auto button = ::Settings::AddButton( const auto stories = &_controller->session().data().stories();
_archive, const auto self = _controller->session().user();
const auto archive = ::Settings::AddButton(
_buttons,
tr::lng_stories_archive_button(), tr::lng_stories_archive_button(),
st::infoSharedMediaButton); st::infoSharedMediaButton);
button->addClickHandler([=] { archive->addClickHandler([=] {
_controller->showSection(Info::Stories::Make( _controller->showSection(Info::Stories::Make(
_controller->key().storiesPeer(), _controller->key().storiesPeer(),
Stories::Tab::Archive)); Stories::Tab::Archive));
}); });
auto count = rpl::single(
rpl::empty
) | rpl::then(stories->archiveChanged()) | rpl::map([=] {
const auto value = stories->archiveCount();
return (value > 0) ? QString::number(value) : QString();
});
::Settings::CreateRightLabel(
archive,
std::move(count),
st::infoSharedMediaButton,
tr::lng_stories_archive_button());
object_ptr<Profile::FloatingIcon>( object_ptr<Profile::FloatingIcon>(
button, archive,
st::infoIconMediaGroup, st::infoIconMediaStoriesArchive,
st::infoSharedMediaButtonIconPosition)->show(); st::infoSharedMediaButtonIconPosition)->show();
_archive->add(object_ptr<Ui::FixedHeightWidget>(
_archive,
st::infoProfileSkip));
_archive->add(object_ptr<Ui::BoxContentDivider>(_archive));
_archive->resizeToWidth(width()); const auto recentWrap = _buttons->add(
_archive->heightValue( object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
_buttons,
::Settings::CreateButton(
_buttons,
tr::lng_stories_recent_button(),
st::infoSharedMediaButton)));
using namespace Dialogs::Stories;
auto last = LastForPeer(
self
) | rpl::map([=](Content &&content) {
for (auto &element : content.elements) {
element.unread = false;
}
return std::move(content);
}) | rpl::start_spawning(recentWrap->lifetime());
const auto recent = recentWrap->entity();
const auto thumbs = Ui::CreateChild<List>(
recent,
st::dialogsStoriesListMine,
rpl::duplicate(last) | rpl::filter([](const Content &content) {
return !content.elements.empty();
}),
[] { return st::dialogsStories.height; });
rpl::combine(
recent->sizeValue(),
rpl::duplicate(last)
) | rpl::start_with_next([=](QSize size, const Content &content) {
if (content.elements.empty()) {
return;
}
const auto width = st::defaultDialogRow.padding.left()
+ st::defaultDialogRow.photoSize
+ st::defaultDialogRow.padding.left();
const auto &small = st::dialogsStories;
const auto count = int(content.elements.size());
const auto smallWidth = small.photo + (count - 1) * small.shift;
const auto real = smallWidth;
const auto top = st::dialogsStories.height
- st::dialogsStoriesFull.height
+ (size.height() - st::dialogsStories.height) / 2;
const auto right = st::settingsButtonRightSkip - (width - real) / 2;
thumbs->resizeToWidth(width);
thumbs->moveToRight(right, top);
}, thumbs->lifetime());
thumbs->setAttribute(Qt::WA_TransparentForMouseEvents);
recent->addClickHandler([=] {
_controller->parentController()->openPeerStories(self->id);
});
object_ptr<Profile::FloatingIcon>(
recent,
st::infoIconMediaStoriesRecent,
st::infoSharedMediaButtonIconPosition)->show();
recentWrap->toggleOn(rpl::duplicate(
last
) | rpl::map([](const Content &content) {
return !content.elements.empty();
}));
_buttons->add(object_ptr<Ui::FixedHeightWidget>(
_buttons,
st::infoProfileSkip));
_buttons->add(object_ptr<Ui::BoxContentDivider>(_buttons));
_buttons->resizeToWidth(width());
_buttons->heightValue(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
refreshHeight(); refreshHeight();
}, _archive->lifetime()); }, _buttons->lifetime());
} }
void InnerWidget::visibleTopBottomUpdated( void InnerWidget::visibleTopBottomUpdated(
@ -194,8 +277,8 @@ int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true; _inResize = true;
auto guard = gsl::finally([this] { _inResize = false; }); auto guard = gsl::finally([this] { _inResize = false; });
if (_archive) { if (_buttons) {
_archive->resizeToWidth(newWidth); _buttons->resizeToWidth(newWidth);
} }
_list->resizeToWidth(newWidth); _list->resizeToWidth(newWidth);
_empty->resizeToWidth(newWidth); _empty->resizeToWidth(newWidth);
@ -211,9 +294,9 @@ void InnerWidget::refreshHeight() {
int InnerWidget::recountHeight() { int InnerWidget::recountHeight() {
auto top = 0; auto top = 0;
if (_archive) { if (_buttons) {
_archive->moveToLeft(0, top); _buttons->moveToLeft(0, top);
top += _archive->heightNoMargins() - st::lineWidth; top += _buttons->heightNoMargins() - st::lineWidth;
} }
auto listHeight = 0; auto listHeight = 0;
if (_list) { if (_list) {

View File

@ -71,7 +71,7 @@ private:
const not_null<Controller*> _controller; const not_null<Controller*> _controller;
object_ptr<Ui::VerticalLayout> _archive = { nullptr }; object_ptr<Ui::VerticalLayout> _buttons = { nullptr };
object_ptr<Media::ListWidget> _list = { nullptr }; object_ptr<Media::ListWidget> _list = { nullptr };
object_ptr<EmptyWidget> _empty; object_ptr<EmptyWidget> _empty;

View File

@ -61,7 +61,7 @@ Type Provider::type() {
} }
bool Provider::hasSelectRestriction() { bool Provider::hasSelectRestriction() {
return false; return true; // #TODO stories
} }
rpl::producer<bool> Provider::hasSelectRestrictionChanges() { rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
@ -292,22 +292,18 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
} }
return nullptr; return nullptr;
}; };
// #TODO stories
const auto maybeStory = item->history()->owner().stories().lookup(
{ item->history()->peer->id, StoryIdFromMsgId(item->id) });
const auto spoiler = maybeStory && !(*maybeStory)->expired();
using namespace Overview::Layout; using namespace Overview::Layout;
const auto options = MediaOptions{ .story = true };
if (const auto photo = getPhoto()) { if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(delegate, item, photo, spoiler); return std::make_unique<Photo>(delegate, item, photo, options);
} else if (const auto file = getFile()) { } else if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, spoiler); return std::make_unique<Video>(delegate, item, file, options);
} else { } else {
return std::make_unique<Photo>( return std::make_unique<Photo>(
delegate, delegate,
item, item,
Data::MediaStory::LoadingStoryPhoto(&item->history()->owner()), Data::MediaStory::LoadingStoryPhoto(&item->history()->owner()),
spoiler); options);
} }
return nullptr; return nullptr;
} }
@ -316,9 +312,8 @@ ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item, not_null<const HistoryItem*> item,
TextSelection selection) { TextSelection selection) {
auto result = ListItemSelectionData(selection); auto result = ListItemSelectionData(selection);
result.canDelete = true; result.canDelete = item->canDelete();
result.canForward = item->allowsForward() result.canForward = item->allowsForward();
&& (&item->history()->session() == &_controller->session());
return result; return result;
} }

View File

@ -65,12 +65,42 @@ TextParseOptions _documentNameOptions = {
}; };
constexpr auto kMaxInlineArea = 1280 * 720; constexpr auto kMaxInlineArea = 1280 * 720;
constexpr auto kStoryRatio = 1.46;
[[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) { [[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {
const auto dimensions = document->dimensions; const auto dimensions = document->dimensions;
return dimensions.width() * dimensions.height() <= kMaxInlineArea; return dimensions.width() * dimensions.height() <= kMaxInlineArea;
} }
[[nodiscard]] QImage CropMediaFrame(QImage image, int width, int height) {
const auto ratio = style::DevicePixelRatio();
width *= ratio;
height *= ratio;
const auto finalize = [&](QImage result) {
result = result.scaled(
width,
height,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
result.setDevicePixelRatio(ratio);
return result;
};
if (image.width() * height == image.height() * width) {
if (image.width() != width) {
return finalize(std::move(image));
}
image.setDevicePixelRatio(ratio);
return image;
} else if (image.width() * height > image.height() * width) {
const auto use = (image.height() * width) / height;
const auto skip = (image.width() - use) / 2;
return finalize(image.copy(skip, 0, use, image.height()));
} else {
const auto use = (image.width() * height) / width;
const auto skip = (image.height() - use) / 2;
return finalize(image.copy(0, skip, image.width(), use));
}
}
} // namespace } // namespace
@ -298,7 +328,7 @@ Photo::Photo(
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
bool spoiler) MediaOptions options)
: ItemBase(delegate, parent) : ItemBase(delegate, parent)
, _data(photo) , _data(photo)
, _link(std::make_shared<PhotoOpenClickHandler>( , _link(std::make_shared<PhotoOpenClickHandler>(
@ -308,9 +338,10 @@ Photo::Photo(
delegate->openPhoto(photo, id); delegate->openPhoto(photo, id);
}), }),
parent->fullId())) parent->fullId()))
, _spoiler(spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] { , _spoiler(options.spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
delegate->repaintItem(this); delegate->repaintItem(this);
}) : nullptr) { }) : nullptr)
, _story(options.story) {
if (_data->inlineThumbnailBytes().isEmpty() if (_data->inlineThumbnailBytes().isEmpty()
&& (_data->hasExact(Data::PhotoSize::Small) && (_data->hasExact(Data::PhotoSize::Small)
|| _data->hasExact(Data::PhotoSize::Thumbnail))) { || _data->hasExact(Data::PhotoSize::Thumbnail))) {
@ -320,14 +351,14 @@ Photo::Photo(
void Photo::initDimensions() { void Photo::initDimensions() {
_maxw = 2 * st::overviewPhotoMinSize; _maxw = 2 * st::overviewPhotoMinSize;
_minh = _maxw; _minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;
} }
int32 Photo::resizeGetHeight(int32 width) { int32 Photo::resizeGetHeight(int32 width) {
width = qMin(width, _maxw); width = qMin(width, _maxw);
if (width != _width || width != _height) { if (_width != width) {
_width = qMin(width, _maxw); _width = width;
_height = _width; _height = _story ? qRound(_width * kStoryRatio) : _width;
} }
return _height; return _height;
} }
@ -382,21 +413,14 @@ void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const
} }
void Photo::setPixFrom(not_null<Image*> image) { void Photo::setPixFrom(not_null<Image*> image) {
const auto size = _width * cIntRetinaFactor(); Expects(_width > 0 && _height > 0);
auto img = image->original(); auto img = image->original();
if (!_goodLoaded) { if (!_goodLoaded) {
img = Images::Blur(std::move(img)); img = Images::Blur(std::move(img));
} }
if (img.width() == img.height()) { _pix = Ui::PixmapFromImage(
if (img.width() != size) { CropMediaFrame(std::move(img), _width, _height));
img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
}
} else if (img.width() > img.height()) {
img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
} else {
img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
}
img.setDevicePixelRatio(cRetinaFactor());
// In case we have inline thumbnail we can unload all images and we still // In case we have inline thumbnail we can unload all images and we still
// won't get a blank image in the media viewer when the photo is opened. // won't get a blank image in the media viewer when the photo is opened.
@ -404,8 +428,6 @@ void Photo::setPixFrom(not_null<Image*> image) {
_dataMedia = nullptr; _dataMedia = nullptr;
delegate()->unregisterHeavyItem(this); delegate()->unregisterHeavyItem(this);
} }
_pix = Ui::PixmapFromImage(std::move(img));
} }
void Photo::ensureDataMediaCreated() const { void Photo::ensureDataMediaCreated() const {
@ -445,13 +467,14 @@ Video::Video(
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<DocumentData*> video, not_null<DocumentData*> video,
bool spoiler) MediaOptions options)
: RadialProgressItem(delegate, parent) : RadialProgressItem(delegate, parent)
, _data(video) , _data(video)
, _duration(Ui::FormatDurationText(_data->duration() / 1000)) , _duration(Ui::FormatDurationText(_data->duration() / 1000))
, _spoiler(spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] { , _spoiler(options.spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
delegate->repaintItem(this); delegate->repaintItem(this);
}) : nullptr) { }) : nullptr)
, _story(options.story) {
setDocumentLinks(_data); setDocumentLinks(_data);
_data->loadThumbnail(parent->fullId()); _data->loadThumbnail(parent->fullId());
} }
@ -460,12 +483,15 @@ Video::~Video() = default;
void Video::initDimensions() { void Video::initDimensions() {
_maxw = 2 * st::overviewPhotoMinSize; _maxw = 2 * st::overviewPhotoMinSize;
_minh = _maxw; _minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;
} }
int32 Video::resizeGetHeight(int32 width) { int32 Video::resizeGetHeight(int32 width) {
_width = qMin(width, _maxw); width = qMin(width, _maxw);
_height = _width; if (_width != width) {
_width = width;
_height = _story ? qRound(_width * kStoryRatio) : _width;
}
return _height; return _height;
} }
@ -497,18 +523,8 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const
: thumbnail : thumbnail
? thumbnail->original() ? thumbnail->original()
: Images::Blur(blurred->original()); : Images::Blur(blurred->original());
if (img.width() == img.height()) { _pix = Ui::PixmapFromImage(
if (img.width() != size) { CropMediaFrame(std::move(img), _width, _height));
img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
}
} else if (img.width() > img.height()) {
img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
} else {
img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
}
img.setDevicePixelRatio(cRetinaFactor());
_pix = Ui::PixmapFromImage(std::move(img));
_pixBlurred = !(thumbnail || good); _pixBlurred = !(thumbnail || good);
} }

View File

@ -183,13 +183,18 @@ struct Info : public RuntimeComponent<Info, LayoutItemBase> {
int top = 0; int top = 0;
}; };
struct MediaOptions {
bool spoiler = false;
bool story = false;
};
class Photo final : public ItemBase { class Photo final : public ItemBase {
public: public:
Photo( Photo(
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
bool spoiler); MediaOptions options);
void initDimensions() override; void initDimensions() override;
int32 resizeGetHeight(int32 width) override; int32 resizeGetHeight(int32 width) override;
@ -212,6 +217,7 @@ private:
QPixmap _pix; QPixmap _pix;
bool _goodLoaded = false; bool _goodLoaded = false;
bool _story = false;
}; };
@ -279,7 +285,7 @@ public:
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<DocumentData*> video, not_null<DocumentData*> video,
bool spoiler); MediaOptions options);
~Video(); ~Video();
void initDimensions() override; void initDimensions() override;
@ -311,6 +317,7 @@ private:
QPixmap _pix; QPixmap _pix;
bool _pixBlurred = true; bool _pixBlurred = true;
bool _story = false;
}; };

View File

@ -2530,7 +2530,7 @@ void SessionController::openPeerStory(
void SessionController::openPeerStories( void SessionController::openPeerStories(
PeerId peerId, PeerId peerId,
Data::StorySourcesList list) { std::optional<Data::StorySourcesList> list) {
using namespace Media::View; using namespace Media::View;
using namespace Data; using namespace Data;
@ -2541,7 +2541,9 @@ void SessionController::openPeerStories(
openPeerStory( openPeerStory(
source->user, source->user,
j != source->ids.end() ? j->id : source->ids.front().id, j != source->ids.end() ? j->id : source->ids.front().id,
{ list }); (list
? StoriesContext{ *list }
: StoriesContext{ StoriesContextPeer() }));
} }
} }

View File

@ -582,7 +582,9 @@ public:
not_null<PeerData*> peer, not_null<PeerData*> peer,
StoryId storyId, StoryId storyId,
Data::StoriesContext context); Data::StoriesContext context);
void openPeerStories(PeerId peerId, Data::StorySourcesList list); void openPeerStories(
PeerId peerId,
std::optional<Data::StorySourcesList> list = std::nullopt);
struct PaintContextArgs { struct PaintContextArgs {
not_null<Ui::ChatTheme*> theme; not_null<Ui::ChatTheme*> theme;