diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f88ce9414..c427185d9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1024,6 +1024,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_user_action_upload_file" = "{user} is sending a file"; "lng_unread_bar#one" = "{count} unread message"; "lng_unread_bar#other" = "{count} unread messages"; +"lng_unread_bar_some" = "Unread messages"; "lng_maps_point" = "Location"; "lng_save_photo" = "Save image"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a0e575f85..7bf60421b 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -55,6 +55,7 @@ constexpr auto kSharedMediaLimit = 100; constexpr auto kFeedMessagesLimit = 50; constexpr auto kReadFeaturedSetsTimeout = TimeMs(1000); constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000); +constexpr auto kFeedReadTimeout = TimeMs(1000); bool IsSilentPost(not_null item, bool silent) { const auto history = item->history(); @@ -133,7 +134,8 @@ ApiWrap::ApiWrap(not_null session) , _webPagesTimer([this] { resolveWebPages(); }) , _draftsSaveTimer([this] { saveDraftsToCloud(); }) , _featuredSetsReadTimer([this] { readFeaturedSets(); }) -, _fileLoader(std::make_unique(kFileLoaderQueueStopTimeout)) { +, _fileLoader(std::make_unique(kFileLoaderQueueStopTimeout)) +, _feedReadTimer([this] { readFeeds(); }) { } void ApiWrap::requestChangelog( @@ -3076,9 +3078,12 @@ void ApiWrap::feedMessagesDone( if (data.has_read_max_position()) { return Data::FeedPositionFromMTP(data.vread_max_position); } else if (!messageId) { - return ids.empty() + const auto result = ids.empty() ? noSkipRange.till : ids.back(); + return Data::MessagePosition( + result.date, + FullMsgId(result.fullId.channel, result.fullId.msg - 1)); } return Data::MessagePosition(); }(); @@ -3712,6 +3717,56 @@ void ApiWrap::readServerHistoryForce(not_null history) { } } +void ApiWrap::readFeed( + not_null feed, + Data::MessagePosition position) { + const auto already = feed->unreadPosition(); + if (already && already >= position) { + return; + } + feed->setUnreadPosition(position); + if (!_feedReadsDelayed.contains(feed)) { + if (_feedReadsDelayed.empty()) { + _feedReadTimer.callOnce(kFeedReadTimeout); + } + _feedReadsDelayed.emplace(feed, getms(true) + kFeedReadTimeout); + } +} + +void ApiWrap::readFeeds() { + auto delay = kFeedReadTimeout; + const auto now = getms(true); + for (auto i = begin(_feedReadsDelayed); i != end(_feedReadsDelayed);) { + const auto [feed, time] = *i; + if (time > now) { + accumulate_min(delay, time - now); + ++i; + } else if (_feedReadRequests.contains(feed)) { + ++i; + } else { + const auto position = feed->unreadPosition(); + const auto requestId = request(MTPchannels_ReadFeed( + MTP_int(feed->id()), + MTP_feedPosition( + MTP_int(position.date), + MTP_peerChannel(MTP_int(position.fullId.channel)), + MTP_int(position.fullId.msg)) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + _feedReadRequests.remove(feed); + }).fail([=](const RPCError &error) { + _feedReadRequests.remove(feed); + }).send(); + _feedReadRequests.emplace(feed, requestId); + + i = _feedReadsDelayed.erase(i); + } + } + if (!_feedReadRequests.empty()) { + _feedReadTimer.callOnce(delay); + } +} + void ApiWrap::sendReadRequest(not_null peer, MsgId upTo) { const auto requestId = [&] { const auto finished = [=] { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index bbc32a0ce..91a7f3604 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -226,6 +226,9 @@ public: void shareContact(not_null user, const SendOptions &options); void readServerHistory(not_null history); void readServerHistoryForce(not_null history); + void readFeed( + not_null feed, + Data::MessagePosition position); void sendVoiceMessage( QByteArray result, @@ -399,6 +402,8 @@ private: bool silent, uint64 randomId); + void readFeeds(); + not_null _session; MessageDataRequests _messageDataRequests; @@ -511,6 +516,7 @@ private: }; base::flat_map, ReadRequest> _readRequests; base::flat_map, MsgId> _readRequestsPending; + std::unique_ptr _fileLoader; base::flat_map> _sendingAlbums; @@ -518,4 +524,8 @@ private: rpl::event_stream _stickerSetInstalled; + base::flat_map, TimeMs> _feedReadsDelayed; + base::flat_map, mtpRequestId> _feedReadRequests; + base::Timer _feedReadTimer; + }; diff --git a/Telegram/SourceFiles/history/feed/history_feed_section.cpp b/Telegram/SourceFiles/history/feed/history_feed_section.cpp index 4e8705d2d..ff586ff79 100644 --- a/Telegram/SourceFiles/history/feed/history_feed_section.cpp +++ b/Telegram/SourceFiles/history/feed/history_feed_section.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "storage/storage_feed_messages.h" #include "mainwidget.h" +#include "apiwrap.h" #include "auth_session.h" #include "styles/style_widgets.h" #include "styles/style_history.h" @@ -241,6 +242,40 @@ void Widget::listSelectionChanged(HistoryView::SelectedItems &&items) { _topBar->showSelected(state); } +void Widget::listVisibleItemsChanged(HistoryItemsList &&items) { + const auto reversed = ranges::view::reverse(items); + const auto good = ranges::find_if(reversed, [](auto item) { + return IsServerMsgId(item->id); + }); + if (good != end(reversed)) { + Auth().api().readFeed(_feed, (*good)->position()); + } +} + +base::optional Widget::listUnreadBarView( + const std::vector> &elements) { + const auto position = _feed->unreadPosition(); + if (!position || elements.empty()) { + return base::none; + } + const auto minimal = ranges::upper_bound( + elements, + position, + std::less<>(), + [](auto view) { return view->data()->position(); }); + if (minimal == end(elements)) { + return base::none; + } + const auto view = *minimal; + const auto unreadMessagesHeight = elements.back()->y() + + elements.back()->height() + - view->y(); + if (unreadMessagesHeight < _scroll->height()) { + return base::none; + } + return base::make_optional(int(minimal - begin(elements))); +} + std::unique_ptr Widget::createMemento() { auto result = std::make_unique(_feed); saveState(result.get()); @@ -272,7 +307,9 @@ void Widget::resizeEvent(QResizeEvent *e) { void Widget::updateControlsGeometry() { const auto contentWidth = width(); - const auto newScrollTop = _scroll->scrollTop() + topDelta(); + const auto newScrollTop = _scroll->isHidden() + ? base::none + : base::make_optional(_scroll->scrollTop() + topDelta()); _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); @@ -282,14 +319,14 @@ void Widget::updateControlsGeometry() { - _showNext->height(); const auto scrollSize = QSize(contentWidth, scrollHeight); if (_scroll->size() != scrollSize) { + _skipScrollEvent = true; _scroll->resize(scrollSize); _inner->resizeToWidth(scrollSize.width(), _scroll->height()); - //_inner->restoreScrollPosition(); + _skipScrollEvent = false; } - if (!_scroll->isHidden()) { - if (topDelta()) { - _scroll->scrollToY(newScrollTop); + if (newScrollTop) { + _scroll->scrollToY(*newScrollTop); } updateInnerVisibleArea(); } @@ -320,12 +357,16 @@ void Widget::paintEvent(QPaintEvent *e) { } void Widget::onScroll() { + if (_skipScrollEvent) { + return; + } updateInnerVisibleArea(); } void Widget::updateInnerVisibleArea() { const auto scrollTop = _scroll->scrollTop(); _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + } void Widget::showAnimatedHook( diff --git a/Telegram/SourceFiles/history/feed/history_feed_section.h b/Telegram/SourceFiles/history/feed/history_feed_section.h index da199936c..184a8e71e 100644 --- a/Telegram/SourceFiles/history/feed/history_feed_section.h +++ b/Telegram/SourceFiles/history/feed/history_feed_section.h @@ -21,6 +21,7 @@ class FlatButton; namespace HistoryView { class ListWidget; class TopBarWidget; +class Element; } // namespace HistoryView namespace HistoryFeed { @@ -31,6 +32,8 @@ class Widget final : public Window::SectionWidget , public HistoryView::ListDelegate { public: + using Element = HistoryView::Element; + Widget( QWidget *parent, not_null controller, @@ -75,6 +78,9 @@ public: not_null second) override; void listSelectionChanged( HistoryView::SelectedItems &&items) override; + void listVisibleItemsChanged(HistoryItemsList &&items) override; + base::optional listUnreadBarView( + const std::vector> &elements) override; protected: void resizeEvent(QResizeEvent *e) override; @@ -104,6 +110,7 @@ private: object_ptr _topBar; object_ptr _topBarShadow; object_ptr _showNext; + bool _skipScrollEvent = false; bool _undefinedAroundPosition = false; }; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 1a5e856a7..8f04e0dc7 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -57,11 +57,14 @@ TextSelection ShiftItemSelection( return ShiftItemSelection(selection, byText.length()); } -void UnreadBar::init(int count) { +void UnreadBar::init(int newCount) { if (freezed) { return; } - text = lng_unread_bar(lt_count, count); + count = newCount; + text = (count == kCountUnknown) + ? lang(lng_unread_bar_some) + : lng_unread_bar(lt_count, count); width = st::semiboldFont->width(text); } @@ -311,8 +314,6 @@ void Element::destroyUnreadBar() { } void Element::setUnreadBarCount(int count) { - Expects(count > 0); - const auto changed = AddComponents(UnreadBar::Bit()); const auto bar = Get(); if (bar->freezed) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 519a00a0c..fde010dd8 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -61,15 +61,18 @@ TextSelection ShiftItemSelection( // Any HistoryView::Element can have this Component for // displaying the unread messages bar above the message. struct UnreadBar : public RuntimeComponent { - void init(int count); + void init(int newCount); static int height(); static int marginTop(); void paint(Painter &p, int y, int w) const; + static constexpr auto kCountUnknown = std::numeric_limits::max(); + QString text; int width = 0; + int count = 0; // If unread bar is freezed the new messages do not // increment the counter displayed by this bar. @@ -150,8 +153,6 @@ public: bool computeIsAttachToPrevious(not_null previous); - // count > 0 - creates the unread bar if necessary and - // sets unread messages count if bar is not freezed yet void setUnreadBarCount(int count); void destroyUnreadBar(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index e192b40a3..8b62827d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -238,6 +238,7 @@ ListWidget::ListWidget( , _context(_delegate->listContext()) , _itemAverageHeight(itemMinimalHeight()) , _scrollDateCheck([this] { scrollDateCheck(); }) +, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); }) , _selectEnabled(_delegate->listAllowsMultiSelect()) { setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); @@ -320,10 +321,21 @@ void ListWidget::refreshRows() { updateAroundPositionFromRows(); updateItemsGeometry(); + checkUnreadBarCreation(); restoreScrollState(); mouseActionUpdate(QCursor::pos()); } +void ListWidget::checkUnreadBarCreation() { + if (!_unreadBarElement) { + if (const auto index = _delegate->listUnreadBarView(_items)) { + _unreadBarElement = _items[*index].get(); + _unreadBarElement->setUnreadBarCount(UnreadBar::kCountUnknown); + refreshAttachmentsAtIndex(*index); + } + } +} + void ListWidget::saveScrollState() { if (!_scrollTopState.item) { _scrollTopState = countScrollState(); @@ -331,9 +343,16 @@ void ListWidget::saveScrollState() { } void ListWidget::restoreScrollState() { - if (_items.empty() || !_scrollTopState.item) { + if (_items.empty()) { return; } + if (!_scrollTopState.item) { + if (!_unreadBarElement) { + return; + } + _scrollTopState.item = _unreadBarElement->data()->position(); + _scrollTopState.shift = st::lineWidth + st::historyUnreadBarMargin; + } const auto index = findNearestItem(_scrollTopState.item); if (index >= 0) { const auto view = _items[index]; @@ -393,21 +412,57 @@ int ListWidget::findNearestItem(Data::MessagePosition position) const { : int(after - begin(_items)); } +HistoryItemsList ListWidget::collectVisibleItems() const { + auto result = HistoryItemsList(); + const auto from = std::lower_bound( + begin(_items), + end(_items), + _visibleTop, + [this](auto &elem, int top) { + return this->itemTop(elem) + elem->height() <= top; + }); + const auto to = std::lower_bound( + begin(_items), + end(_items), + _visibleBottom, + [this](auto &elem, int bottom) { + return this->itemTop(elem) < bottom; + }); + result.reserve(to - from); + for (auto i = from; i != to; ++i) { + result.push_back((*i)->data()); + } + return result; +} + void ListWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { - auto scrolledUp = (visibleTop < _visibleTop); + if (!(visibleTop < visibleBottom)) { + return; + } + + const auto initializing = !(_visibleTop < _visibleBottom); + const auto scrolledUp = (visibleTop < _visibleTop); _visibleTop = visibleTop; _visibleBottom = visibleBottom; + if (initializing) { + checkUnreadBarCreation(); + } updateVisibleTopItem(); - checkMoveToOtherViewer(); if (scrolledUp) { _scrollDateCheck.call(); } else { scrollDateHideByTimer(); } _controller->floatPlayerAreaUpdated().notify(true); + _applyUpdatedScrollState.call(); +} + +void ListWidget::applyUpdatedScrollState() { + checkMoveToOtherViewer(); + _delegate->listVisibleItemsChanged(collectVisibleItems()); } void ListWidget::updateVisibleTopItem() { @@ -938,11 +993,16 @@ void ListWidget::updateItemsGeometry() { } void ListWidget::updateSize() { - TWidget::resizeToWidth(width()); - restoreScrollPosition(); + resizeToWidth(width(), _minHeight); updateVisibleTopItem(); } +void ListWidget::resizeToWidth(int newWidth, int minHeight) { + _minHeight = minHeight; + TWidget::resizeToWidth(newWidth); + restoreScrollPosition(); +} + int ListWidget::resizeGetHeight(int newWidth) { update(); @@ -963,7 +1023,9 @@ int ListWidget::resizeGetHeight(int newWidth) { } _itemsWidth = newWidth; _itemsHeight = newHeight; - _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0; + _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) + ? (_minHeight - _itemsHeight - st::historyPaddingBottom) + : 0; return _itemsTop + _itemsHeight + st::historyPaddingBottom; } @@ -2131,6 +2193,18 @@ void ListWidget::viewReplaced(not_null was, Element *now) { if (_visibleTopItem == was) _visibleTopItem = now; if (_scrollDateLastItem == was) _scrollDateLastItem = now; if (_overElement == was) _overElement = now; + if (_unreadBarElement == was) { + const auto bar = _unreadBarElement->Get(); + const auto count = bar ? bar->count : 0; + const auto freezed = bar ? bar->freezed : false; + _unreadBarElement = now; + if (now && count) { + _unreadBarElement->setUnreadBarCount(count); + if (freezed) { + _unreadBarElement->setUnreadBarFreezed(); + } + } + } } void ListWidget::itemRemoved(not_null item) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 901889863..47e8fe29d 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -61,6 +61,9 @@ public: not_null first, not_null second) = 0; virtual void listSelectionChanged(SelectedItems &&items) = 0; + virtual void listVisibleItemsChanged(HistoryItemsList &&items) = 0; + virtual base::optional listUnreadBarView( + const std::vector> &elements) = 0; }; @@ -127,10 +130,7 @@ public: // Set the correct scroll position after being resized. void restoreScrollPosition(); - void resizeToWidth(int newWidth, int minHeight) { - _minHeight = minHeight; - return TWidget::resizeToWidth(newWidth); - } + void resizeToWidth(int newWidth, int minHeight); void saveState(not_null memento); void restoreState(not_null memento); @@ -264,6 +264,7 @@ private: Element *strictFindItemByY(int y) const; int findNearestItem(Data::MessagePosition position) const; void viewReplaced(not_null was, Element *now); + HistoryItemsList collectVisibleItems() const; void checkMoveToOtherViewer(); void updateVisibleTopItem(); @@ -352,6 +353,8 @@ private: TextSelection computeRenderSelection( not_null selected, not_null view) const; + void checkUnreadBarCreation(); + void applyUpdatedScrollState(); // This function finds all history items that are displayed and calls template method // for each found message (in given direction) in the passed history with passed top offset. @@ -409,6 +412,9 @@ private: base::Timer _scrollDateHideTimer; Element *_scrollDateLastItem = nullptr; int _scrollDateLastItemTop = 0; + SingleQueuedInvokation _applyUpdatedScrollState; + + Element *_unreadBarElement = nullptr; MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 9838d825b..c1daef1a4 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_drafts.h" #include "data/data_session.h" #include "data/data_media_types.h" +#include "data/data_feed.h" #include "ui/special_buttons.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" @@ -4676,7 +4677,17 @@ void MainWidget::feedUpdate(const MTPUpdate &update) { case mtpc_updateReadFeed: { const auto &d = update.c_updateReadFeed(); const auto feedId = d.vfeed_id.v; - // #TODO feeds + if (const auto feed = Auth().data().feedLoaded(feedId)) { + feed->setUnreadPosition( + Data::FeedPositionFromMTP(d.vmax_position)); + if (d.has_unread_count() && d.has_unread_muted_count()) { + feed->setUnreadCounts( + d.vunread_count.v, + d.vunread_muted_count.v); + } else { + Auth().api().requestDialogEntry(feed); + } + } } break; // Deleted messages.