diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 3e14909094..c8cea7dcc7 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -543,7 +543,7 @@ void PeerData::addPinnedSlice( return; } ensurePinnedMessagesCreated(); - _pinnedMessages->add(std::move(ids), noSkipRange, std::nullopt); + _pinnedMessages->add(std::move(ids), noSkipRange, count); session().changes().peerUpdated(this, UpdateFlag::PinnedMessage); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 768efb5571..233a027af5 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5225,7 +5225,8 @@ void HistoryWidget::checkPinnedBarState() { ) | rpl::map([=](HistoryView::PinnedId messageId) { return HistoryView::PinnedBarId{ FullMsgId{ peerToChannel(_peer->id), messageId.message }, - messageId.type + messageId.index, + messageId.count }; }); _pinnedBar = std::make_unique( diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index a73c9e4cfe..96aa406de9 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -23,10 +23,7 @@ namespace { [[nodiscard]] Ui::MessageBarContent ContentWithoutPreview( not_null item) { - const auto media = item->media(); - const auto poll = media ? media->poll() : nullptr; return Ui::MessageBarContent{ - .id = item->id, .text = { item->inReplyText() }, }; } @@ -126,17 +123,13 @@ auto WithPinnedTitle(not_null session, PinnedBarId id) { if (!item) { return std::move(content); } - const auto media = item->media(); - const auto poll = media ? media->poll() : nullptr; - content.title = (id.type == PinnedIdType::First) - ? tr::lng_pinned_previous(tr::now) // #TODO pinned first? - : (id.type == PinnedIdType::Middle) - ? tr::lng_pinned_previous(tr::now) - : !poll + content.title = (id.index + 1 >= id.count) ? tr::lng_pinned_message(tr::now) - : poll->quiz() - ? tr::lng_pinned_quiz(tr::now) - : tr::lng_pinned_poll(tr::now); + : (tr::lng_pinned_message(tr::now) // #TODO pinned + + " #" + + QString::number(id.count - id.index)); + content.count = std::max(id.count, 1); + content.index = std::clamp(id.index, 0, content.count - 1); return std::move(content); }; } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.h b/Telegram/SourceFiles/history/view/history_view_pinned_bar.h index d09394e7ea..41c20d30e5 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.h @@ -30,13 +30,16 @@ namespace HistoryView { enum class PinnedIdType; struct PinnedBarId { FullMsgId message; - PinnedIdType type = PinnedIdType(); + int index = 0; + int count = 1; bool operator<(const PinnedBarId &other) const { - return std::tie(message, type) < std::tie(other.message, other.type); + return std::tie(message, index, count) + < std::tie(other.message, other.index, other.count); } bool operator==(const PinnedBarId &other) const { - return std::tie(message, type) == std::tie(other.message, other.type); + return std::tie(message, index, count) + == std::tie(other.message, other.index, other.count); } bool operator!=(const PinnedBarId &other) const { return !(*this == other); diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp index bb18c8356f..6962a49030 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp @@ -102,15 +102,18 @@ void PinnedTracker::setupViewer(not_null data) { _current = PinnedId(); return; } - const auto type = (!after && (snapshot.skippedAfter == 0)) - ? PinnedIdType::Last - : (before < 2 && (snapshot.skippedBefore == 0)) - ? PinnedIdType::First - : PinnedIdType::Middle; + const auto count = std::max( + snapshot.fullCount.value_or(1), + int(snapshot.ids.size())); + const auto index = snapshot.skippedBefore.has_value() + ? (*snapshot.skippedBefore + before) + : snapshot.skippedAfter.has_value() + ? (count - *snapshot.skippedAfter - after) + : 1; if (i != begin(snapshot.ids)) { - _current = PinnedId{ *(i - 1), type }; + _current = PinnedId{ *(i - 1), index - 1, count }; } else if (snapshot.skippedBefore == 0) { - _current = PinnedId{ snapshot.ids.front(), type }; + _current = PinnedId{ snapshot.ids.front(), 0, count }; } }, _dataLifetime); } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.h b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.h index 1fbf061b4e..ac762d996c 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.h @@ -16,20 +16,18 @@ enum class LoadDirection : char; namespace HistoryView { -enum class PinnedIdType { - First, - Middle, - Last, -}; struct PinnedId { MsgId message = 0; - PinnedIdType type = PinnedIdType::Middle; + int index = 0; + int count = 1; bool operator<(const PinnedId &other) const { - return std::tie(message, type) < std::tie(other.message, other.type); + return std::tie(message, index, count) + < std::tie(other.message, other.index, other.count); } bool operator==(const PinnedId &other) const { - return std::tie(message, type) == std::tie(other.message, other.type); + return std::tie(message, index, count) + == std::tie(other.message, other.index, other.count); } }; diff --git a/Telegram/SourceFiles/ui/chat/message_bar.cpp b/Telegram/SourceFiles/ui/chat/message_bar.cpp index 1cb11a7ae7..42ccc6f1c6 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/message_bar.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bar.h" #include "ui/text/text_options.h" +#include "ui/image/image_prepare.h" #include "styles/style_chat.h" #include "styles/palette.h" @@ -50,7 +51,8 @@ MessageBar::BodyAnimation MessageBar::DetectBodyAnimationType( ? currentAnimation->bodyAnimation : BodyAnimation::None; const auto somethingChanged = (currentContent.text != nextContent.text) - || (currentContent.id != nextContent.id); + || (currentContent.index != nextContent.index) + || (currentContent.count != nextContent.count); return (now == BodyAnimation::Full || currentContent.title != nextContent.title || (currentContent.title.isEmpty() && somethingChanged)) @@ -61,6 +63,9 @@ MessageBar::BodyAnimation MessageBar::DetectBodyAnimationType( } void MessageBar::tweenTo(MessageBarContent &&content) { + Expects(content.count > 0); + Expects(content.index >= 0 && content.index < content.count); + _widget.update(); if (!_st.duration || anim::Disabled() || _widget.size().isEmpty()) { updateFromContent(std::move(content)); @@ -68,18 +73,22 @@ void MessageBar::tweenTo(MessageBarContent &&content) { } const auto hasImageChanged = (_content.preview.isNull() != content.preview.isNull()); - const auto bodyChanged = (_content.id != content.id + const auto bodyChanged = (_content.index != content.index + || _content.count != content.count || _content.title != content.title || _content.text != content.text || _content.preview.constBits() != content.preview.constBits()); + const auto barCountChanged = (_content.count != content.count); + const auto barFrom = _content.index; + const auto barTo = content.index; auto animation = Animation(); animation.bodyAnimation = DetectBodyAnimationType( _animation.get(), _content, content); - animation.movingTo = (content.id > _content.id) + animation.movingTo = (content.index > _content.index) ? RectPart::Top - : (content.id < _content.id) + : (content.index < _content.index) ? RectPart::Bottom : RectPart::None; animation.imageFrom = grabImagePart(); @@ -92,6 +101,8 @@ void MessageBar::tweenTo(MessageBarContent &&content) { _animation = std::move(was); std::swap(*_animation, animation); _animation->imageShown = std::move(animation.imageShown); + _animation->barScroll = std::move(animation.barScroll); + _animation->barTop = std::move(animation.barTop); } else { _animation = std::make_unique(std::move(animation)); } @@ -110,6 +121,23 @@ void MessageBar::tweenTo(MessageBarContent &&content) { 1., _st.duration); } + if (barCountChanged) { + _animation->barScroll.stop(); + _animation->barTop.stop(); + } else if (barFrom != barTo) { + const auto wasState = countBarState(barFrom); + const auto nowState = countBarState(barTo); + _animation->barScroll.start( + [=] { _widget.update(); }, + wasState.scroll, + nowState.scroll, + _st.duration); + _animation->barTop.start( + [] {}, + wasState.offset, + nowState.offset, + _st.duration); + } } void MessageBar::updateFromContent(MessageBarContent &&content) { @@ -239,12 +267,7 @@ void MessageBar::paint(Painter &p) { ? (shiftTo - shiftFull) : (shiftTo + shiftFull); - const auto bar = QRect( - st::msgReplyBarSkip + st::msgReplyBarPos.x(), - st::msgReplyPadding.top() + st::msgReplyBarPos.y(), - st::msgReplyBarSize.width(), - st::msgReplyBarSize.height()); - p.fillRect(bar, st::msgInReplyBarColor); + paintLeftBar(p); if (!_animation) { if (!_image.isNull()) { @@ -315,4 +338,150 @@ void MessageBar::paint(Painter &p) { } } +auto MessageBar::countBarState(int index) const -> BarState { + Expects(index >= 0 && index < _content.count); + + auto result = BarState(); + const auto line = st::msgReplyBarSize.width(); + const auto height = st::msgReplyBarSize.height(); + const auto count = _content.count; + const auto shownCount = std::min(count, 4); + const auto dividers = (shownCount - 1) * line; + const auto size = float64(st::msgReplyBarSize.height() - dividers) + / shownCount; + const auto fullHeight = count * size + (count - 1) * line; + const auto topByIndex = [&](int index) { + return index * (size + line); + }; + result.scroll = (count < 5 || index < 2) + ? 0 + : (index >= count - 2) + ? (fullHeight - height) + : (topByIndex(index) - (height - size) / 2); + result.size = size; + result.skip = line; + result.offset = topByIndex(index); + return result; +} + +auto MessageBar::countBarState() const -> BarState { + return countBarState(_content.index); +} + +void MessageBar::ensureGradientsCreated(int size) { + if (!_topBarGradient.isNull()) { + return; + } + const auto rows = size * style::DevicePixelRatio() - 2; + auto bottomMask = QImage( + QSize(1, size) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + const auto step = ((1ULL << 24) - 1) / rows; + const auto limit = step * rows; + auto bits = bottomMask.bits(); + const auto perLine = bottomMask.bytesPerLine(); + for (auto counter = uint32(0); counter != limit; counter += step) { + const auto value = (counter >> 16); + memset(bits, int(value), perLine); + bits += perLine; + } + memset(bits, 255, perLine * 2); + auto bottom = style::colorizeImage(bottomMask, st::historyPinnedBg); + bottom.setDevicePixelRatio(style::DevicePixelRatio()); + auto top = bottom.mirrored(); + _bottomBarGradient = Images::PixmapFast(std::move(bottom)); + _topBarGradient = Images::PixmapFast(std::move(top)); +} + +void MessageBar::paintLeftBar(Painter &p) { + const auto state = countBarState(); + const auto gradientSize = int(std::ceil(state.size * 2.5)); + if (_content.count > 4) { + ensureGradientsCreated(gradientSize); + } + + const auto scroll = _animation + ? _animation->barScroll.value(state.scroll) + : state.scroll; + const auto offset = _animation + ? _animation->barTop.value(state.offset) + : state.offset; + const auto line = st::msgReplyBarSize.width(); + const auto height = st::msgReplyBarSize.height(); + const auto activeFrom = offset - scroll; + const auto activeTill = activeFrom + state.size; + const auto single = state.size + state.skip; + + const auto barSkip = st::msgReplyPadding.top() + st::msgReplyBarPos.y(); + const auto fullHeight = barSkip + height + barSkip; + const auto bar = QRect( + st::msgReplyBarSkip + st::msgReplyBarPos.x(), + barSkip, + line, + state.size); + const auto paintFromScroll = std::max(scroll - barSkip, 0.); + const auto paintFrom = int(std::floor(paintFromScroll / single)); + const auto paintTillScroll = (scroll + height + barSkip); + const auto paintTill = std::min( + int(std::floor(paintTillScroll / single)) + 1, + _content.count); + + p.setPen(Qt::NoPen); + const auto activeBrush = QBrush(st::msgInReplyBarColor); + const auto inactiveBrush = QBrush(QColor( + st::msgInReplyBarColor->c.red(), + st::msgInReplyBarColor->c.green(), + st::msgInReplyBarColor->c.blue(), + st::msgInReplyBarColor->c.alpha() / 3)); + const auto radius = line / 2.; + auto hq = PainterHighQualityEnabler(p); + for (auto i = paintFrom; i != paintTill; ++i) { + const auto top = i * single - scroll; + const auto bottom = top + state.size; + const auto active = (top == activeFrom); + p.setBrush(active ? activeBrush : inactiveBrush); + p.drawRoundedRect(bar.translated(0, top), radius, radius); + if (active + || bottom - line <= activeFrom + || top + line >= activeTill) { + continue; + } + const auto partFrom = std::max(top, activeFrom); + const auto partTill = std::min(bottom, activeTill); + p.setBrush(activeBrush); + p.drawRoundedRect( + QRect(bar.x(), bar.y() + partFrom, line, partTill - partFrom), + radius, + radius); + } + if (_content.count > 4) { + const auto firstScroll = countBarState(2).scroll; + const auto gradientTop = (scroll >= firstScroll) + ? 0 + : anim::interpolate(-gradientSize, 0, scroll / firstScroll); + const auto lastScroll = countBarState(_content.count - 3).scroll; + const auto largestScroll = countBarState(_content.count - 1).scroll; + const auto gradientBottom = (scroll <= lastScroll) + ? fullHeight + : anim::interpolate( + fullHeight, + fullHeight + gradientSize, + (scroll - lastScroll) / (largestScroll - lastScroll)); + if (gradientTop > -gradientSize) { + p.drawPixmap( + QRect(bar.x(), gradientTop, bar.width(), gradientSize), + _topBarGradient); + } + if (gradientBottom < fullHeight + gradientSize) { + p.drawPixmap( + QRect( + bar.x(), + gradientBottom - gradientSize, + bar.width(), + gradientSize), + _bottomBarGradient); + } + } +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/message_bar.h b/Telegram/SourceFiles/ui/chat/message_bar.h index 1c82d5672f..bf15d219a2 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.h +++ b/Telegram/SourceFiles/ui/chat/message_bar.h @@ -19,7 +19,8 @@ struct MessageBar; namespace Ui { struct MessageBarContent { - int id = 0; + int index = 0; + int count = 1; QString title; TextWithEntities text; QImage preview; @@ -47,6 +48,8 @@ private: struct Animation { Ui::Animations::Simple bodyMoved; Ui::Animations::Simple imageShown; + Ui::Animations::Simple barScroll; + Ui::Animations::Simple barTop; QPixmap bodyOrTextFrom; QPixmap bodyOrTextTo; QPixmap imageFrom; @@ -54,8 +57,15 @@ private: BodyAnimation bodyAnimation = BodyAnimation::None; RectPart movingTo = RectPart::None; }; + struct BarState { + float64 scroll = 0.; + float64 size = 0.; + float64 skip = 0.; + float64 offset = 0.; + }; void setup(); void paint(Painter &p); + void paintLeftBar(Painter &p); void tweenTo(MessageBarContent &&content); void updateFromContent(MessageBarContent &&content); [[nodiscard]] QPixmap prepareImage(const QImage &preview); @@ -71,6 +81,10 @@ private: [[nodiscard]] QPixmap grabBodyPart(); [[nodiscard]] QPixmap grabTextPart(); + [[nodiscard]] BarState countBarState(int index) const; + [[nodiscard]] BarState countBarState() const; + void ensureGradientsCreated(int size); + [[nodiscard]] static BodyAnimation DetectBodyAnimationType( Animation *currentAnimation, const MessageBarContent ¤tContent, @@ -81,7 +95,7 @@ private: MessageBarContent _content; rpl::lifetime _contentLifetime; Ui::Text::String _title, _text; - QPixmap _image; + QPixmap _image, _topBarGradient, _bottomBarGradient; std::unique_ptr _animation; };