diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 55a15d2a7..3d7584a64 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2524,6 +2524,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->data().stories().apply(update.c_updateStory()); } break; + case mtpc_updateReadStories: { + _session->data().stories().apply(update.c_updateReadStories()); + } break; + } } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index ea176d2bf..ca39658a6 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -525,6 +525,13 @@ not_null Session::processUser(const MTPUser &data) { | Flag::DiscardMinPhoto | Flag::StoriesHidden : Flag()); + const auto storiesState = minimal + ? std::optional() + : data.is_stories_unavailable() + ? Data::Stories::PeerSourceState() + : !data.vstories_max_id() + ? std::optional() + : stories().peerSourceState(result, data.vstories_max_id()->v); const auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) | (data.is_scam() ? Flag::Scam : Flag()) @@ -551,6 +558,13 @@ not_null Session::processUser(const MTPUser &data) { MTP_long(data.vaccess_hash().value_or_empty())); } } else { + if (storiesState) { + result->setStoriesState(!storiesState->maxId + ? UserData::StoriesState::None + : (storiesState->maxId > storiesState->readTill) + ? UserData::StoriesState::HasUnread + : UserData::StoriesState::HasRead); + } if (data.is_self()) { result->input = MTP_inputPeerSelf(); result->inputUser = MTP_inputUserSelf(); diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 8f27352a0..5b265f8a7 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -136,7 +136,7 @@ void Stories::apply(const MTPDupdateStory &data) { i->second.ids.emplace(idDates); const auto nowInfo = i->second.info(); if (user->isSelf() && i->second.readTill < idDates.id) { - i->second.readTill = idDates.id; + _readTill[user->id] = i->second.readTill = idDates.id; } if (wasInfo == nowInfo) { return; @@ -158,6 +158,11 @@ void Stories::apply(const MTPDupdateStory &data) { refreshInList(StorySourcesList::NotHidden); } _sourceChanged.fire_copy(peerId); + updateUserStoriesState(user); +} + +void Stories::apply(const MTPDupdateReadStories &data) { + bumpReadTill(peerFromUser(data.vuser_id()), data.vmax_id().v); } void Stories::apply(not_null peer, const MTPUserStories *data) { @@ -166,6 +171,7 @@ void Stories::apply(not_null peer, const MTPUserStories *data) { applyDeletedFromSources(peer->id, StorySourcesList::Hidden); _all.erase(peer->id); _sourceChanged.fire_copy(peer->id); + updateUserStoriesState(peer); } else { parseAndApply(*data); } @@ -281,6 +287,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } else if (user->isSelf()) { result.readTill = result.ids.back().id; } + _readTill[peerId] = result.readTill; const auto info = result.info(); const auto i = _all.find(peerId); if (i != end(_all)) { @@ -317,6 +324,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) { applyDeletedFromSources(peerId, StorySourcesList::Hidden); } _sourceChanged.fire_copy(peerId); + updateUserStoriesState(result.user); } Story *Stories::parseAndApply( @@ -684,12 +692,14 @@ void Stories::applyRemovedFromActive(FullStoryId id) { const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j != end(i->second.ids) && j->id == id.story) { i->second.ids.erase(j); + const auto user = i->second.user; if (i->second.ids.empty()) { _all.erase(i); removeFromList(StorySourcesList::NotHidden); removeFromList(StorySourcesList::Hidden); } _sourceChanged.fire_copy(id.peer); + updateUserStoriesState(user); } } } @@ -891,22 +901,35 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { _incrementViewsTimer.callOnce(kIncrementViewsDelay); } } - const auto i = _all.find(id.peer); - if (i == end(_all) || i->second.readTill >= id.story) { + if (!bumpReadTill(id.peer, id.story)) { return; - } else if (!_markReadPending.contains(id.peer)) { + } + if (!_markReadPending.contains(id.peer)) { sendMarkAsReadRequests(); } _markReadPending.emplace(id.peer); + _markReadTimer.callOnce(kMarkAsReadDelay); +} + +bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) { + auto &till = _readTill[peerId]; + if (till < maxReadTill) { + till = maxReadTill; + updateUserStoriesState(_owner->peer(peerId)); + } + const auto i = _all.find(peerId); + if (i == end(_all) || i->second.readTill >= maxReadTill) { + return false; + } const auto wasUnreadCount = i->second.unreadCount(); - i->second.readTill = id.story; + i->second.readTill = maxReadTill; const auto nowUnreadCount = i->second.unreadCount(); if (wasUnreadCount != nowUnreadCount) { const auto refreshInList = [&](StorySourcesList list) { auto &sources = _sources[static_cast(list)]; const auto i = ranges::find( sources, - id.peer, + peerId, &StoriesSourceInfo::id); if (i != end(sources)) { i->unreadCount = nowUnreadCount; @@ -916,7 +939,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { refreshInList(StorySourcesList::NotHidden); refreshInList(StorySourcesList::Hidden); } - _markReadTimer.callOnce(kMarkAsReadDelay); + return true; } void Stories::toggleHidden( @@ -1408,6 +1431,53 @@ void Stories::setPreloadingInViewer(std::vector ids) { } } +std::optional Stories::peerSourceState( + not_null peer, + StoryId storyMaxId) { + const auto i = _readTill.find(peer->id); + if (_readTillReceived || (i != end(_readTill))) { + return PeerSourceState{ + .maxId = storyMaxId, + .readTill = std::min( + storyMaxId, + (i != end(_readTill)) ? i->second : 0), + }; + } + if (!_readTillsRequestId) { + const auto api = &_owner->session().api(); + _readTillsRequestId = api->request(MTPstories_GetAllReadUserStories( + )).done([=](const MTPUpdates &result) { + _readTillReceived = true; + api->applyUpdates(result); + for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) { + updateUserStoriesState(peer); + } + }).send(); + } + _pendingUserStateMaxId[peer] = storyMaxId; + return std::nullopt; +} + +void Stories::updateUserStoriesState(not_null peer) { + const auto till = _readTill.find(peer->id); + const auto readTill = (till != end(_readTill)) ? till->second : 0; + const auto pendingMaxId = [&] { + const auto j = _pendingUserStateMaxId.find(peer); + return (j != end(_pendingUserStateMaxId)) ? j->second : 0; + }; + const auto i = _all.find(peer->id); + const auto max = (i != end(_all)) + ? (i->second.ids.empty() ? 0 : i->second.ids.back().id) + : pendingMaxId(); + if (const auto user = peer->asUser()) { + user->setStoriesState(!max + ? UserData::StoriesState::None + : (max <= readTill) + ? UserData::StoriesState::HasRead + : UserData::StoriesState::HasUnread); + } +} + void Stories::preloadSourcesChanged(StorySourcesList list) { if (rebuildPreloadSources(list)) { continuePreloading(); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 437a26a7b..c15b1fc6b 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -137,6 +137,7 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); + void apply(const MTPDupdateReadStories &data); void apply(not_null peer, const MTPUserStories *data); Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story); void loadAround(FullStoryId id, StoriesContext context); @@ -199,6 +200,14 @@ public: void decrementPreloadingHiddenSources(); void setPreloadingInViewer(std::vector ids); + struct PeerSourceState { + StoryId maxId = 0; + StoryId readTill = 0; + }; + [[nodiscard]] std::optional peerSourceState( + not_null peer, + StoryId storyMaxId); + private: struct Saved { StoriesIds ids; @@ -222,6 +231,7 @@ private: const QVector &list); void sendResolveRequests(); void finalizeResolve(FullStoryId id); + void updateUserStoriesState(not_null peer); void applyDeleted(FullStoryId id); void applyExpired(FullStoryId id); @@ -230,6 +240,7 @@ private: void removeDependencyStory(not_null story); void savedStateUpdated(not_null story); void sort(StorySourcesList list); + bool bumpReadTill(PeerId peerId, StoryId maxReadTill); [[nodiscard]] std::shared_ptr lookupItem( not_null story); @@ -317,6 +328,11 @@ private: int _preloadingHiddenSourcesCounter = 0; int _preloadingMainSourcesCounter = 0; + base::flat_map _readTill; + base::flat_map, StoryId> _pendingUserStateMaxId; + mtpRequestId _readTillsRequestId = 0; + bool _readTillReceived = false; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index be4a9fa6f..d25f37780 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user_names.h" #include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" +#include "history/history.h" #include "api/api_peer_photo.h" #include "apiwrap.h" #include "ui/text/text_options.h" @@ -125,6 +126,38 @@ void UserData::setPrivateForwardName(const QString &name) { _privateForwardName = name; } +bool UserData::hasActiveStories() const { + return flags() & UserDataFlag::HasActiveStories; +} + +bool UserData::hasUnreadStories() const { + return flags() & UserDataFlag::HasUnreadStories; +} + +void UserData::setStoriesState(StoriesState state) { + Expects(state != StoriesState::Unknown); + + const auto was = flags(); + using Flag = UserDataFlag; + switch (state) { + case StoriesState::None: + _flags.remove(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + case StoriesState::HasRead: + _flags.set( + (flags() & ~Flag::HasUnreadStories) | Flag::HasActiveStories); + break; + case StoriesState::HasUnread: + _flags.add(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + } + if (flags() != was) { + if (const auto history = owner().historyLoaded(this)) { + history->updateChatListEntryPostponed(); + } + } +} + void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) { bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty(); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 37005d124..ad6d0b7ae 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -63,6 +63,8 @@ enum class UserDataFlag { VoiceMessagesForbidden = (1 << 16), PersonalPhoto = (1 << 17), StoriesHidden = (1 << 18), + HasActiveStories = (1 << 19), + HasUnreadStories = (1 << 20), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -174,6 +176,16 @@ public: [[nodiscard]] QString privateForwardName() const; void setPrivateForwardName(const QString &name); + enum class StoriesState { + Unknown, + None, + HasRead, + HasUnread, + }; + [[nodiscard]] bool hasActiveStories() const; + [[nodiscard]] bool hasUnreadStories() const; + void setStoriesState(StoriesState state); + private: auto unavailableReasons() const -> const std::vector & override; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 091ebaf16..6b2b7efe7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1294,6 +1294,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { } _mouseSelection = true; _lastMousePosition = globalPosition; + _lastRowLocalMouseX = local.x(); const auto w = width(); const auto mouseY = local.y(); @@ -2209,6 +2210,7 @@ FilterId InnerWidget::filterId() const { void InnerWidget::clearSelection() { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (isSelected()) { updateSelectedRow(); _collapsedSelected = -1; @@ -2907,6 +2909,7 @@ void InnerWidget::resizeEmptyLabel() { void InnerWidget::clearMouseSelection(bool clearSelection) { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (clearSelection) { if (_state == WidgetState::Default) { _collapsedSelected = -1; @@ -3375,29 +3378,30 @@ ChosenRow InnerWidget::computeChosenRow() const { if (_state == WidgetState::Default) { if (_selected) { return { - _selected->key(), - Data::UnreadMessagePosition + .key = _selected->key(), + .message = Data::UnreadMessagePosition, }; } } else if (_state == WidgetState::Filtered) { if (base::in_range(_filteredSelected, 0, _filterResults.size())) { return { - _filterResults[_filteredSelected].key(), - Data::UnreadMessagePosition, - true + .key = _filterResults[_filteredSelected].key(), + .message = Data::UnreadMessagePosition, + .filteredRow = true, }; } else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) { + const auto peer = _peerSearchResults[_peerSearchSelected]->peer; return { - session().data().history(_peerSearchResults[_peerSearchSelected]->peer), - Data::UnreadMessagePosition + .key = session().data().history(peer), + .message = Data::UnreadMessagePosition }; } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { const auto result = _searchResults[_searchedSelected].get(); const auto topic = result->topic(); const auto item = result->item(); return { - (topic ? (Entry*)topic : (Entry*)item->history()), - item->position() + .key = (topic ? (Entry*)topic : (Entry*)item->history()), + .message = item->position() }; } } @@ -3413,10 +3417,12 @@ bool InnerWidget::chooseRow( } else if (chooseHashtag()) { return true; } - const auto modifyChosenRow = []( + const auto modifyChosenRow = [&]( ChosenRow row, Qt::KeyboardModifiers modifiers) { row.newWindow = (modifiers & Qt::ControlModifier); + row.userpicClick = (_lastRowLocalMouseX >= 0) + && (_lastRowLocalMouseX < _st->nameLeft); return row; }; auto chosen = modifyChosenRow(computeChosenRow(), modifiers); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index b23aefa0d..c72d9667f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -65,8 +65,9 @@ class IndexedList; struct ChosenRow { Key key; Data::MessagePosition message; - bool filteredRow = false; - bool newWindow = false; + bool userpicClick : 1 = false; + bool filteredRow : 1 = false; + bool newWindow : 1 = false; }; enum class SearchRequestType { @@ -416,6 +417,7 @@ private: FilterId _filterId = 0; bool _mouseSelection = false; std::optional _lastMousePosition; + int _lastRowLocalMouseX = -1; Qt::MouseButton _pressButton = Qt::LeftButton; Data::Folder *_openedFolder = nullptr; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index c5145c368..77f9aca72 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_session.h" #include "data/data_peer_values.h" +#include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" @@ -330,6 +331,7 @@ void Row::ensureCornerBadgeUserpic() const { void Row::PaintCornerBadgeFrame( not_null data, + int framePadding, not_null peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, @@ -337,6 +339,19 @@ void Row::PaintCornerBadgeFrame( data->frame.fill(Qt::transparent); Painter q(&data->frame); + q.translate(framePadding, framePadding); + auto hq = std::optional(); + if (data->storiesShown) { + hq.emplace(q); + const auto line = st::dialogsStoriesFull.lineTwice / 2.; + const auto skip = line * 3 / 2.; + const auto scale = 1. - (2 * skip / context.st->photoSize); + const auto center = context.st->photoSize / 2.; + q.save(); + q.translate(center, center); + q.scale(scale, scale); + q.translate(-center, -center); + } PaintUserpic( q, peer, @@ -347,9 +362,37 @@ void Row::PaintCornerBadgeFrame( data->frame.width() / data->frame.devicePixelRatio(), context.st->photoSize, context.paused); + if (data->storiesShown) { + q.restore(); + + const auto st = context.st; + const auto storiesUnreadBrush = [&] { + const auto left = st->padding.left(); + const auto top = st->padding.top(); + auto gradient = QLinearGradient( + QPoint(left + st->photoSize, top), + QPoint(left, top + st->photoSize)); + gradient.setStops({ + { 0., st::groupCallLive1->c }, + { 1., st::groupCallMuted1->c }, + }); + return QBrush(gradient); + }; + const auto storiesBrush = data->storiesUnread + ? storiesUnreadBrush() + : context.active + ? st::dialogsUnreadBgMutedActive->b + : st::dialogsUnreadBgMuted->b; + const auto storiesLine = data->storiesUnread + ? (st::dialogsStoriesFull.lineTwice / 2.) + : (st::dialogsStoriesFull.lineReadTwice / 2.); + const auto pen = QPen(storiesBrush, storiesLine); + q.setPen(pen); + q.drawEllipse(0, 0, st->photoSize, st->photoSize); + } const auto &manager = data->layersManager; - if (const auto p = manager.progressForLayer(kBottomLayer); p) { + if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) { const auto size = context.st->photoSize; if (data->cacheTTL.isNull() && peer->messagesTTL()) { data->cacheTTL = CornerBadgeTTL(peer, view, size); @@ -364,7 +407,9 @@ void Row::PaintCornerBadgeFrame( return; } - PainterHighQualityEnabler hq(q); + if (!hq) { + hq.emplace(q); + } q.setCompositionMode(QPainter::CompositionMode_Source); const auto size = peer->isUser() @@ -401,7 +446,14 @@ void Row::paintUserpic( const auto cornerBadgeShown = !_cornerBadgeUserpic ? _cornerBadgeShown : !_cornerBadgeUserpic->layersManager.isDisplayedNone(); - if (!historyForCornerBadge || !cornerBadgeShown) { + const auto storiesUser = historyForCornerBadge + ? historyForCornerBadge->peer->asUser() + : nullptr; + const auto storiesShown = (storiesUser + && storiesUser->hasActiveStories()) ? 1 : 0; + const auto storiesUnread = (storiesShown + && storiesUser->hasUnreadStories()) ? 1 : 0; + if (!historyForCornerBadge || (!cornerBadgeShown && !storiesShown)) { BasicRow::paintUserpic( p, peer, @@ -415,12 +467,12 @@ void Row::paintUserpic( } ensureCornerBadgeUserpic(); const auto ratio = style::DevicePixelRatio(); - const auto added = std::max({ + const auto framePadding = std::max({ -st::dialogsCallBadgeSkip.x(), -st::dialogsCallBadgeSkip.y(), - 0 }); - const auto frameSide = (context.st->photoSize + added) - * style::DevicePixelRatio(); + st::lineWidth * 2 }); + const auto frameSide = (2 * framePadding + context.st->photoSize) + * ratio; const auto frameSize = QSize(frameSide, frameSide); if (_cornerBadgeUserpic->frame.size() != frameSize) { _cornerBadgeUserpic->frame = QImage( @@ -432,6 +484,7 @@ void Row::paintUserpic( key.first += peer->messagesTTL(); const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1; const auto paletteVersion = style::PaletteVersion(); + const auto active = context.active ? 1 : 0; const auto keyChanged = (_cornerBadgeUserpic->key != key) || (_cornerBadgeUserpic->paletteVersion != paletteVersion); if (keyChanged) { @@ -439,24 +492,29 @@ void Row::paintUserpic( } if (keyChanged || !_cornerBadgeUserpic->layersManager.isFinished() - || _cornerBadgeUserpic->active != context.active + || _cornerBadgeUserpic->active != active || _cornerBadgeUserpic->frameIndex != frameIndex + || _cornerBadgeUserpic->storiesShown != storiesShown + || _cornerBadgeUserpic->storiesUnread != storiesUnread || videoUserpic) { _cornerBadgeUserpic->key = key; _cornerBadgeUserpic->paletteVersion = paletteVersion; - _cornerBadgeUserpic->active = context.active; + _cornerBadgeUserpic->active = active; + _cornerBadgeUserpic->storiesShown = storiesShown; + _cornerBadgeUserpic->storiesUnread = storiesUnread; _cornerBadgeUserpic->frameIndex = frameIndex; _cornerBadgeUserpic->layersManager.markFrameShown(); PaintCornerBadgeFrame( _cornerBadgeUserpic.get(), + framePadding, peer, videoUserpic, userpicView(), context); } p.drawImage( - context.st->padding.left(), - context.st->padding.top(), + context.st->padding.left() - framePadding, + context.st->padding.top() - framePadding, _cornerBadgeUserpic->frame); if (historyForCornerBadge->peer->isUser()) { return; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index d79c2efdf..735b03004 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -167,11 +167,13 @@ private: struct CornerBadgeUserpic { InMemoryKey key; CornerLayersManager layersManager; - int paletteVersion = 0; - int frameIndex = -1; - bool active = false; QImage frame; QImage cacheTTL; + int frameIndex = -1; + int paletteVersion : 24 = 0; + int storiesShown : 1 = 0; + int storiesUnread : 1 = 0; + int active : 1 = 0; }; void setCornerBadgeShown( @@ -180,6 +182,7 @@ private: void ensureCornerBadgeUserpic() const; static void PaintCornerBadgeFrame( not_null data, + int framePadding, not_null peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 6c3e2e059..72e8b9ea9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -503,6 +503,14 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history) { const auto peer = history->peer; + if (const auto user = history->peer->asUser()) { + if (row.message.fullId.msg == ShowAtUnreadMsgId) { + if (row.userpicClick && user->hasActiveStories()) { + controller()->openPeerStories(user->id); + return; + } + } + } const auto showAtMsgId = controller()->uniqueChatsInSearchResults() ? ShowAtUnreadMsgId : row.message.fullId.msg;