From 29d87f692ac4a85a103e59a695a4738508c61e2b Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 15 Apr 2025 17:00:43 +0400 Subject: [PATCH] Add carousel animation for emoji fingerprint. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/api/api_updates.cpp | 2 +- Telegram/SourceFiles/calls/calls.style | 11 + .../calls/calls_emoji_fingerprint.cpp | 439 +++++++++++++++++- .../calls/calls_emoji_fingerprint.h | 41 ++ .../calls/group/calls_group_members.cpp | 28 +- .../calls/group/calls_group_members.h | 5 + Telegram/SourceFiles/history/history_item.cpp | 178 ++++--- Telegram/SourceFiles/history/history_item.h | 6 + .../history/history_item_edition.cpp | 1 + .../history/history_item_edition.h | 1 + .../history/history_item_helpers.cpp | 24 - .../history/history_item_helpers.h | 2 - .../view/media/history_view_document.cpp | 4 +- .../history/view/media/history_view_gif.cpp | 3 +- 15 files changed, 644 insertions(+), 103 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 671476eef9..74c0ad438f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4967,6 +4967,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_confcall_not_accessible" = "This call is no longer accessible."; "lng_confcall_participants_limit" = "This call reached the participants limit."; "lng_confcall_sure_remove" = "Remove {user} from the call?"; +"lng_confcall_e2e_badge" = "End-to-End Encrypted"; +"lng_confcall_e2e_badge_small" = "E2E Encrypted"; "lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages."; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index ec65bd15d3..ce13487861 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1316,7 +1316,7 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) { user->madeAction(base::unixtime::now()); } } - ClearMediaAsExpired(item); + item->clearMediaAsExpired(); } } else { // Perhaps it was an unread mention! diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 121f93f426..bc2d11a4e2 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1669,3 +1669,14 @@ groupCallLinkMenu: IconButton(confcallLinkMenu) { color: groupCallMembersBgOver; } } + +confcallFingerprintBottomSkip: 8px; +confcallFingerprintMargins: margins(8px, 5px, 8px, 5px); +confcallFingerprintTextMargins: margins(3px, 3px, 3px, 0px); +confcallFingerprintText: FlatLabel(defaultFlatLabel) { + textFg: groupCallMembersFg; + style: TextStyle(defaultTextStyle) { + font: font(10px semibold); + } +} +confcallFingerprintSkip: 2px; diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index f6c568b48c..580005ec8c 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -7,11 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/calls_emoji_fingerprint.h" +#include "base/random.h" #include "calls/calls_call.h" #include "calls/calls_signal_bars.h" #include "lang/lang_keys.h" #include "data/data_user.h" +#include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" +#include "ui/abstract_button.h" #include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/rp_widget.h" @@ -20,7 +23,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Calls { namespace { -constexpr auto kTooltipShowTimeoutMs = 1000; +constexpr auto kTooltipShowTimeoutMs = crl::time(1000); +constexpr auto kCarouselOneDuration = crl::time(1000);// crl::time(100); +constexpr auto kStartTimeShift = crl::time(500);// crl::time(50); +constexpr auto kEmojiInFingerprint = 4; +constexpr auto kEmojiInCarousel = 10; const ushort Data[] = { 0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21, @@ -109,8 +116,11 @@ const ushort Offsets[] = { 620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641, 642, 643, 644, 646, 648, 650, 652, 654, 656, 658 }; +constexpr auto kEmojiCount = (base::array_size(Offsets) - 1); + uint64 ComputeEmojiIndex(bytes::const_span bytes) { Expects(bytes.size() == 8); + return ((gsl::to_integer(bytes[0]) & 0x7F) << 56) | (gsl::to_integer(bytes[1]) << 48) | (gsl::to_integer(bytes[2]) << 40) @@ -121,6 +131,17 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) { | (gsl::to_integer(bytes[7])); } +[[nodiscard]] EmojiPtr EmojiByIndex(int index) { + Expects(index >= 0 && index < kEmojiCount); + + const auto offset = Offsets[index]; + const auto size = Offsets[index + 1] - offset; + const auto string = QString::fromRawData( + reinterpret_cast(Data + offset), + size); + return Ui::Emoji::Find(string); +} + } // namespace std::vector ComputeEmojiFingerprint(not_null call) { @@ -133,22 +154,13 @@ std::vector ComputeEmojiFingerprint(not_null call) { std::vector ComputeEmojiFingerprint( bytes::const_span fingerprint) { auto result = std::vector(); - constexpr auto EmojiCount = (base::array_size(Offsets) - 1); constexpr auto kPartSize = 8; for (auto partOffset = 0 ; partOffset != fingerprint.size() ; partOffset += kPartSize) { - auto value = ComputeEmojiIndex( + const auto value = ComputeEmojiIndex( fingerprint.subspan(partOffset, kPartSize)); - auto index = value % EmojiCount; - auto offset = Offsets[index]; - auto size = Offsets[index + 1] - offset; - auto string = QString::fromRawData( - reinterpret_cast(Data + offset), - size); - auto emoji = Ui::Emoji::Find(string); - Assert(emoji != nullptr); - result.push_back(emoji); + result.push_back(EmojiByIndex(value % kEmojiCount)); } return result; } @@ -294,4 +306,407 @@ base::unique_qptr CreateFingerprintAndSignalBars( return result; } +FingerprintBadge SetupFingerprintBadge( + rpl::lifetime &on, + rpl::producer fingerprint) { + struct State { + FingerprintBadgeState data; + Ui::Animations::Basic animation; + Fn update; + rpl::event_stream<> repaints; + }; + const auto state = on.make_state(); + + state->update = [=](crl::time now) { + // speed-up-duration = 2 * one / speed. + const auto one = 1.; + const auto speedUpDuration = 2 * kCarouselOneDuration; + const auto speed0 = one / kCarouselOneDuration; + + auto updated = false; + auto animating = false; + for (auto &entry : state->data.entries) { + if (!entry.time) { + continue; + } + animating = true; + if (entry.time >= now) { + continue; + } + + updated = true; + const auto elapsed = (now - entry.time) * 1.; + entry.time = now; + + Assert(!entry.emoji || entry.sliding.size() > 1); + const auto slideCount = entry.emoji + ? (int(entry.sliding.size()) - 1) * one + : (kEmojiInCarousel + (elapsed / kCarouselOneDuration)); + const auto finalPosition = slideCount * one; + const auto distance = finalPosition - entry.position; + + const auto accelerate0 = speed0 - entry.speed; + const auto decelerate0 = speed0; + const auto acceleration0 = speed0 / speedUpDuration; + const auto taccelerate0 = accelerate0 / acceleration0; + const auto tdecelerate0 = decelerate0 / acceleration0; + const auto paccelerate0 = entry.speed * taccelerate0 + + acceleration0 * taccelerate0 * taccelerate0 / 2.; + const auto pdecelerate0 = 0 + + acceleration0 * tdecelerate0 * tdecelerate0 / 2.; + const auto ttozero = entry.speed / acceleration0; + if (paccelerate0 + pdecelerate0 <= distance) { + // We have time to accelerate to speed0, + // maybe go some time on speed0 and then decelerate to 0. + const auto uaccelerate0 = std::min(taccelerate0, elapsed); + const auto left = distance - paccelerate0 - pdecelerate0; + const auto tconstant = left / speed0; + const auto uconstant = std::min( + tconstant, + elapsed - uaccelerate0); + const auto udecelerate0 = std::min( + tdecelerate0, + elapsed - uaccelerate0 - uconstant); + if (udecelerate0 >= tdecelerate0) { + Assert(entry.emoji != nullptr); + entry = { .emoji = entry.emoji }; + } else { + entry.position += entry.speed * uaccelerate0 + + acceleration0 * uaccelerate0 * uaccelerate0 / 2. + + speed0 * uconstant + + speed0 * udecelerate0 + - acceleration0 * udecelerate0 * udecelerate0 / 2.; + entry.speed += acceleration0 + * (uaccelerate0 - udecelerate0); + } + } else if (acceleration0 * ttozero * ttozero / 2 <= distance) { + // We have time to accelerate at least for some time >= 0, + // and then decelerate to 0 to make it to final position. + // + // peak = entry.speed + acceleration0 * t + // tdecelerate = peak / acceleration0 + // distance = entry.speed * t + // + acceleration0 * t * t / 2 + // + acceleration0 * tdecelerate * tdecelerate / 2 + const auto det = entry.speed * entry.speed / 2 + + distance * acceleration0; + const auto t = std::max( + (sqrt(det) - entry.speed) / acceleration0, + 0.); + + const auto taccelerate = t; + const auto uaccelerate = std::min(taccelerate, elapsed); + const auto tdecelerate = t + (entry.speed / acceleration0); + const auto udecelerate = std::min( + tdecelerate, + elapsed - uaccelerate); + if (udecelerate >= tdecelerate) { + Assert(entry.emoji != nullptr); + entry = { .emoji = entry.emoji }; + } else { + const auto topspeed = entry.speed + + acceleration0 * taccelerate; + entry.position += entry.speed * uaccelerate + + acceleration0 * uaccelerate * uaccelerate / 2. + + topspeed * udecelerate + - acceleration0 * udecelerate * udecelerate / 2.; + entry.speed += acceleration0 + * (uaccelerate - udecelerate); + } + } else { + // We just need to decelerate to 0, + // faster than acceleration0. + Assert(entry.speed > 0); + const auto tdecelerate = 2 * distance / entry.speed; + const auto udecelerate = std::min(tdecelerate, elapsed); + if (udecelerate >= tdecelerate) { + Assert(entry.emoji != nullptr); + entry = { .emoji = entry.emoji }; + } else { + const auto a = entry.speed / tdecelerate; + entry.position += entry.speed * udecelerate + - a * udecelerate * udecelerate / 2; + entry.speed -= a * udecelerate; + } + } + + if (entry.position >= kEmojiInCarousel) { + entry.position -= qFloor(entry.position / kEmojiInCarousel) + * kEmojiInCarousel; + } + while (entry.position >= 1.) { + Assert(!entry.sliding.empty()); + entry.position -= 1.; + entry.sliding.erase(begin(entry.sliding)); + if (entry.emoji && entry.sliding.size() < 2) { + entry = { .emoji = entry.emoji }; + break; + } else if (entry.sliding.empty()) { + const auto index = (entry.added++) % kEmojiInCarousel; + entry.sliding.push_back(entry.carousel[index]); + } + } + if (!entry.emoji + && entry.position > 0. + && entry.sliding.size() < 2) { + const auto index = (entry.added++) % kEmojiInCarousel; + entry.sliding.push_back(entry.carousel[index]); + } + } + if (!animating) { + state->animation.stop(); + } else if (updated) { + state->repaints.fire({}); + } + }; + state->animation.init(state->update); + state->data.entries.resize(kEmojiInFingerprint); + + const auto fillCarousel = [=]( + int index, + base::BufferedRandom &buffered) { + auto &entry = state->data.entries[index]; + auto indices = std::vector(); + indices.reserve(kEmojiInCarousel); + auto count = kEmojiCount; + for (auto i = 0; i != kEmojiInCarousel; ++i, --count) { + auto index = base::RandomIndex(count, buffered); + for (const auto &already : indices) { + if (index >= already) { + ++index; + } + } + indices.push_back(index); + } + + entry.carousel.clear(); + entry.carousel.reserve(kEmojiInCarousel); + for (const auto index : indices) { + entry.carousel.push_back(EmojiByIndex(index)); + } + }; + + const auto startTo = [=]( + int index, + EmojiPtr emoji, + crl::time now, + base::BufferedRandom &buffered) { + auto &entry = state->data.entries[index]; + if ((entry.emoji == emoji) && (emoji || entry.time)) { + return; + } else if (!entry.time) { + Assert(entry.sliding.empty()); + + if (entry.emoji) { + entry.sliding.push_back(entry.emoji); + } else if (emoji) { + // Just initialize if we get emoji right from the start. + entry.emoji = emoji; + return; + } + entry.time = now + index * kStartTimeShift; + + fillCarousel(index, buffered); + } + entry.emoji = emoji; + if (entry.emoji) { + entry.sliding.push_back(entry.emoji); + } else { + const auto index = (entry.added++) % kEmojiInCarousel; + entry.sliding.push_back(entry.carousel[index]); + } + }; + + std::move( + fingerprint + ) | rpl::start_with_next([=](const QByteArray &fingerprint) { + auto buffered = base::BufferedRandom( + kEmojiInCarousel * kEmojiInFingerprint); + const auto now = crl::now(); + const auto emoji = (fingerprint.size() >= 32) + ? ComputeEmojiFingerprint( + bytes::make_span(fingerprint).subspan(0, 32)) + : std::vector(); + state->update(now); + + if (emoji.size() == kEmojiInFingerprint) { + for (auto i = 0; i != kEmojiInFingerprint; ++i) { + startTo(i, emoji[i], now, buffered); + } + } else { + for (auto i = 0; i != kEmojiInFingerprint; ++i) { + startTo(i, nullptr, now, buffered); + } + } + if (!state->animation.animating()) { + state->animation.start(); + } + }, on); + + return { .state = &state->data, .repaints = state->repaints.events() }; +} + +void SetupFingerprintBadgeWidget( + not_null widget, + not_null state, + rpl::producer<> repaints) { + auto &lifetime = widget->lifetime(); + + const auto button = Ui::CreateChild(widget); + button->show(); + + const auto label = Ui::CreateChild( + button, + QString(), + st::confcallFingerprintText); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); + + const auto ratio = style::DevicePixelRatio(); + const auto esize = Ui::Emoji::GetSizeNormal(); + const auto size = esize / ratio; + widget->widthValue() | rpl::start_with_next([=](int width) { + static_assert(!(kEmojiInFingerprint % 2)); + + const auto available = width + - st::confcallFingerprintMargins.left() + - st::confcallFingerprintMargins.right() + - (kEmojiInFingerprint * size) + - (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip + - st::confcallFingerprintTextMargins.left() + - st::confcallFingerprintTextMargins.right(); + if (available <= 0) { + return; + } + label->setText(tr::lng_confcall_e2e_badge(tr::now)); + if (label->textMaxWidth() > available) { + label->setText(tr::lng_confcall_e2e_badge_small(tr::now)); + } + const auto use = std::min(available, label->textMaxWidth()); + label->resizeToWidth(use); + + const auto ontheleft = kEmojiInFingerprint / 2; + const auto ontheside = ontheleft * size + + (ontheleft - 1) * st::confcallFingerprintSkip; + const auto text = QRect( + (width - use) / 2, + (st::confcallFingerprintMargins.top() + + st::confcallFingerprintTextMargins.top()), + use, + label->height()); + const auto textOuter = text.marginsAdded( + st::confcallFingerprintTextMargins); + const auto withEmoji = QRect( + textOuter.x() - ontheside, + textOuter.y(), + textOuter.width() + ontheside * 2, + size); + const auto outer = withEmoji.marginsAdded( + st::confcallFingerprintMargins); + + button->setGeometry(outer); + label->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width); + + widget->resize( + width, + button->height() + st::confcallFingerprintBottomSkip); + }, lifetime); + + const auto cache = lifetime.make_state(); + button->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(button); + + const auto outer = button->rect(); + const auto radius = outer.height() / 2.; + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::groupCallMembersBg); + p.drawRoundedRect(outer, radius, radius); + p.setClipRect(outer); + + const auto withEmoji = outer.marginsRemoved( + st::confcallFingerprintMargins); + p.translate(withEmoji.topLeft()); + + const auto text = label->geometry(); + const auto textOuter = text.marginsAdded( + st::confcallFingerprintTextMargins); + const auto count = int(state->entries.size()); + cache->entries.resize(count); + for (auto i = 0; i != count; ++i) { + PaintFingerprintEntry( + p, + state->entries[i], + cache->entries[i], + esize); + if (i + 1 == count / 2) { + p.translate(size + textOuter.width(), 0); + } else { + p.translate(size + st::confcallFingerprintSkip, 0); + } + } + }, lifetime); + + std::move(repaints) | rpl::start_with_next([=] { + button->update(); + }, lifetime); +} + +void PaintFingerprintEntry( + QPainter &p, + const FingerprintBadgeState::Entry &entry, + FingerprintBadgeCache::Entry &cache, + int esize) { + const auto stationary = !entry.time; + if (stationary) { + Ui::Emoji::Draw(p, entry.emoji, esize, 0, 0); + return; + } + const auto ratio = style::DevicePixelRatio(); + const auto size = esize / ratio; + const auto add = 4; + const auto height = size + 2 * add; + const auto validateCache = [&](int index, EmojiPtr e) { + if (cache.emoji.size() <= index) { + cache.emoji.reserve(entry.carousel.size() + 2); + cache.emoji.resize(index + 1); + } + auto &emoji = cache.emoji[index]; + if (emoji.ptr != e) { + emoji.ptr = e; + emoji.image = QImage( + QSize(size, height) * ratio, + QImage::Format_ARGB32_Premultiplied); + emoji.image.setDevicePixelRatio(ratio); + emoji.image.fill(Qt::transparent); + auto q = QPainter(&emoji.image); + Ui::Emoji::Draw(q, e, esize, 0, add); + q.end(); + + //emoji.image = Images::Blur( + // std::move(emoji.image), + // false, + // Qt::Vertical); + } + return &emoji; + }; + auto shift = entry.position * height - add; + p.translate(0, shift); + for (const auto &e : entry.sliding) { + const auto index = [&] { + const auto i = ranges::find(entry.carousel, e); + if (i != end(entry.carousel)) { + return int(i - begin(entry.carousel)); + } + return int(entry.carousel.size()) + + ((e == entry.sliding.back()) ? 1 : 0); + }(); + const auto entry = validateCache(index, e); + p.drawImage(0, 0, entry->image); + p.translate(0, -height); + shift -= height; + } + p.translate(0, -shift); +} + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h index 36a10d4929..21b063a946 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h @@ -26,4 +26,45 @@ class Call; not_null parent, not_null call); +struct FingerprintBadgeState { + struct Entry { + EmojiPtr emoji = nullptr; + std::vector sliding; + std::vector carousel; + crl::time time = 0; + float64 speed = 0.; + float64 position = 0.; + int added = 0; + }; + std::vector entries; +}; +struct FingerprintBadge { + not_null state; + rpl::producer<> repaints; +}; +FingerprintBadge SetupFingerprintBadge( + rpl::lifetime &on, + rpl::producer fingerprint); + +void SetupFingerprintBadgeWidget( + not_null widget, + not_null state, + rpl::producer<> repaints); + +struct FingerprintBadgeCache { + struct Emoji { + EmojiPtr ptr = nullptr; + QImage image; + }; + struct Entry { + std::vector emoji; + }; + std::vector entries; +}; +void PaintFingerprintEntry( + QPainter &p, + const FingerprintBadgeState::Entry &entry, + FingerprintBadgeCache::Entry &cache, + int esize); + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index f9a1727729..d5ce0e5791 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_volume_item.h" #include "calls/group/calls_group_members_row.h" #include "calls/group/calls_group_viewport.h" +#include "calls/calls_emoji_fingerprint.h" #include "calls/calls_instance.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -1745,6 +1746,9 @@ Members::Members( , _listController(std::make_unique(call, parent, mode)) , _layout(_scroll->setOwnedWidget( object_ptr(_scroll.data()))) +, _fingerprint(call->conference() + ? _layout->add(object_ptr(_layout.get())) + : nullptr) , _videoWrap(_layout->add(object_ptr(_layout.get()))) , _viewport( std::make_unique( @@ -1753,6 +1757,7 @@ Members::Members( backend)) { setupList(); setupAddMember(call); + setupFingerprint(); setContent(_list); setupFakeRoundCorners(); _listController->setDelegate(static_cast(this)); @@ -1854,6 +1859,8 @@ void Members::setupAddMember(not_null call) { _canInviteByLink = canInviteByLinkByPeer(channel); }); + const auto baseIndex = _layout->count() - 2; + rpl::combine( _canAddMembers.value(), _canInviteByLink.value(), @@ -1887,7 +1894,7 @@ void Members::setupAddMember(not_null call) { addMember->resizeToWidth(_layout->width()); delete _addMemberButton.current(); _addMemberButton = addMember.data(); - _layout->insert(3, std::move(addMember)); + _layout->insert(baseIndex, std::move(addMember)); if (conference) { auto shareLink = Settings::CreateButtonWithIcon( _layout.get(), @@ -1901,7 +1908,7 @@ void Members::setupAddMember(not_null call) { shareLink->resizeToWidth(_layout->width()); delete _shareLinkButton.current(); _shareLinkButton = shareLink.data(); - _layout->insert(4, std::move(shareLink)); + _layout->insert(baseIndex + 1, std::move(shareLink)); } }, lifetime()); @@ -2000,6 +2007,23 @@ void Members::setupList() { }, _scroll->lifetime()); } +void Members::setupFingerprint() { + if (const auto raw = _fingerprint) { + auto badge = SetupFingerprintBadge( + raw->lifetime(), + _call->emojiHashValue()); + std::move(badge.repaints) | rpl::start_to_stream( + _fingerprintRepaints, + raw->lifetime()); + _fingerprintState = badge.state; + + SetupFingerprintBadgeWidget( + raw, + _fingerprintState, + _fingerprintRepaints.events()); + } +} + void Members::trackViewportGeometry() { _call->videoEndpointLargeValue( ) | rpl::start_with_next([=](const VideoEndpoint &large) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index 355f45f5f1..7de1a75ed2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -25,6 +25,7 @@ class GroupCall; namespace Calls { class GroupCall; +struct FingerprintBadgeState; } // namespace Calls namespace Calls::Group { @@ -96,6 +97,7 @@ private: void setupAddMember(not_null call); void resizeToList(); void setupList(); + void setupFingerprint(); void setupFakeRoundCorners(); void trackViewportGeometry(); @@ -106,6 +108,9 @@ private: object_ptr _scroll; std::unique_ptr _listController; not_null _layout; + const not_null _fingerprint; + rpl::event_stream<> _fingerprintRepaints; + const FingerprintBadgeState *_fingerprintState = nullptr; const not_null _videoWrap; std::unique_ptr _viewport; rpl::variable _addMemberButton = nullptr; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 6309d4965a..ca55741f67 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -426,10 +426,12 @@ HistoryItem::HistoryItem( } else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive) || (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) { createServiceFromMtp(data); + setReactions(data.vreactions()); applyTTL(data); } else if (checked == MediaCheckResult::HasStoryMention) { setMedia(*data.vmedia()); createServiceFromMtp(data); + setReactions(data.vreactions()); applyTTL(data); } else { createComponents(data); @@ -1830,8 +1832,18 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { auto updatedText = checkedMedia ? edition.textWithEntities : EnsureNonEmpty(edition.textWithEntities); + auto serviceText = (!checkedMedia + && edition.textWithEntities.empty() + && edition.mtpMedia) + ? prepareServiceTextForMessage( + *edition.mtpMedia, + edition.isMediaUnread) + : PreparedServiceText(); if (updatingSavedLocalEdit) { Get()->text = std::move(updatedText); + } else if (!serviceText.text.empty()) { + setServiceText(std::move(serviceText)); + addToSharedMediaIndex(); } else { setText(std::move(updatedText)); addToSharedMediaIndex(); @@ -1920,6 +1932,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { addToSharedMediaIndex(); finishEdition(-1); _flags &= ~MessageFlag::DisplayFromChecked; + + updateReactions(message.vreactions()); } else if (isService()) { if (const auto reply = Get()) { reply->clearData(this); @@ -1930,6 +1944,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { applyServiceDateEdition(message); finishEdition(-1); _flags &= ~MessageFlag::DisplayFromChecked; + + updateReactions(message.vreactions()); } const auto nowSublist = savedSublist(); if (wasSublist && nowSublist != wasSublist) { @@ -2091,6 +2107,31 @@ void HistoryItem::contributeToSlowmode(TimeId realDate) { } } +void HistoryItem::clearMediaAsExpired() { + const auto media = this->media(); + if (!media->ttlSeconds()) { + return; + } + if (const auto document = media->document()) { + applyEditionToHistoryCleared(); + auto text = (document->isVideoFile() + ? tr::lng_ttl_video_expired + : document->isVoiceMessage() + ? tr::lng_ttl_voice_expired + : document->isVideoMessage() + ? tr::lng_ttl_round_expired + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + updateServiceText({ std::move(text) }); + _flags |= MessageFlag::ReactionsAllowed; + } else if (media->photo()) { + applyEditionToHistoryCleared(); + updateServiceText({ + tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) + }); + _flags |= MessageFlag::ReactionsAllowed; + } +} + void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { if (!isRegular()) { return; @@ -4169,9 +4210,86 @@ void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) { } } +PreparedServiceText HistoryItem::prepareServiceTextForMessage( + const MTPMessageMedia &media, + bool unread) { + return media.match([&](const MTPDmessageMediaStory &data) { + return prepareStoryMentionText(); + }, [&](const MTPDmessageMediaPhoto &data) -> PreparedServiceText { + if (unread) { + const auto ttl = data.vttl_seconds(); + Assert(ttl != nullptr); + + if (out()) { + return { + tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities) + }; + } else { + auto result = PreparedServiceText(); + result.links.push_back(fromLink()); + result.text = tr::lng_ttl_photo_received( + tr::now, + lt_from, + fromLinkText(), // Link 1. + Ui::Text::WithEntities); + return result; + } + } else { + return { + tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) + }; + } + }, [&](const MTPDmessageMediaDocument &data) -> PreparedServiceText { + if (unread) { + const auto ttl = data.vttl_seconds(); + Assert(ttl != nullptr); + + if (data.is_video()) { + if (out()) { + return { + tr::lng_ttl_video_sent( + tr::now, + Ui::Text::WithEntities) + }; + } else { + auto result = PreparedServiceText(); + result.links.push_back(fromLink()); + result.text = tr::lng_ttl_video_received( + tr::now, + lt_from, + fromLinkText(), // Link 1. + Ui::Text::WithEntities); + return result; + } + } else if (out()) { + auto text = (data.is_voice() + ? tr::lng_ttl_voice_sent + : data.is_round() + ? tr::lng_ttl_round_sent + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + return { std::move(text) }; + } + return {}; + } else { + auto text = (data.is_video() + ? tr::lng_ttl_video_expired + : data.is_voice() + ? tr::lng_ttl_voice_expired + : data.is_round() + ? tr::lng_ttl_round_expired + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + return { std::move(text) }; + } + }, [](const auto &) { + return PreparedServiceText(); + }); +} + void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { AddComponents(HistoryServiceData::Bit()); + _flags |= MessageFlag::ReactionsAllowed; + const auto unread = message.is_media_unread(); const auto media = message.vmedia(); Assert(media != nullptr); @@ -4182,24 +4300,6 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, *ttl); - if (out()) { - setServiceText({ - tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities) - }); - } else { - auto result = PreparedServiceText(); - result.links.push_back(fromLink()); - result.text = tr::lng_ttl_photo_received( - tr::now, - lt_from, - fromLinkText(), // Link 1. - Ui::Text::WithEntities); - setServiceText(std::move(result)); - } - } else { - setServiceText({ - tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) - }); } }, [&](const MTPDmessageMediaDocument &data) { if (unread) { @@ -4210,49 +4310,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { setSelfDestruct( HistoryServiceSelfDestruct::Type::Video, *ttl); - if (out()) { - setServiceText({ - tr::lng_ttl_video_sent( - tr::now, - Ui::Text::WithEntities) - }); - } else { - auto result = PreparedServiceText(); - result.links.push_back(fromLink()); - result.text = tr::lng_ttl_video_received( - tr::now, - lt_from, - fromLinkText(), // Link 1. - Ui::Text::WithEntities); - setServiceText(std::move(result)); - } - } else if (out()) { - auto text = (data.is_voice() - ? tr::lng_ttl_voice_sent - : data.is_round() - ? tr::lng_ttl_round_sent - : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); - setServiceText({ std::move(text) }); } - } else { - auto text = (data.is_video() - ? tr::lng_ttl_video_expired - : data.is_voice() - ? tr::lng_ttl_voice_expired - : data.is_round() - ? tr::lng_ttl_round_expired - : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); - setServiceText({ std::move(text) }); } - }, [&](const MTPDmessageMediaStory &data) { - setServiceText(prepareStoryMentionText()); - }, [](const auto &) { - Unexpected("Media type in HistoryItem::createServiceFromMtp()"); - }); + }, [](const auto &) {}); - if (const auto reactions = message.vreactions()) { - updateReactions(reactions); - } + setServiceText(prepareServiceTextForMessage(*media, unread)); } void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 06a1156953..05db24526e 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -380,6 +380,8 @@ public: void updateReplyMarkup(HistoryMessageMarkupData &&markup); void contributeToSlowmode(TimeId realDate = 0); + void clearMediaAsExpired(); + void addToUnreadThings(HistoryUnreadThings::AddType type); void destroyHistoryEntry(); [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -670,6 +672,10 @@ private: [[nodiscard]] PreparedServiceText prepareCallScheduledText( TimeId scheduleDate); + [[nodiscard]] PreparedServiceText prepareServiceTextForMessage( + const MTPMessageMedia &media, + bool unread); + void flagSensitiveContent(); [[nodiscard]] PeerData *computeDisplayFrom() const; diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index d66d0cf7bc..1fb3482394 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -14,6 +14,7 @@ HistoryMessageEdition::HistoryMessageEdition( not_null session, const MTPDmessage &message) { isEditHide = message.is_edit_hide(); + isMediaUnread = message.is_media_unread(); editDate = message.vedit_date().value_or(-1); textWithEntities = TextWithEntities{ qs(message.vmessage()), diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index a44109299b..c22ce713c8 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -20,6 +20,7 @@ struct HistoryMessageEdition { const MTPDmessage &message); bool isEditHide = false; + bool isMediaUnread = false; int editDate = 0; int views = -1; int forwards = -1; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index b585fd4526..7c38991768 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -1195,30 +1195,6 @@ void ShowTrialTranscribesToast(int left, TimeId until) { }); } -void ClearMediaAsExpired(not_null item) { - if (const auto media = item->media()) { - if (!media->ttlSeconds()) { - return; - } - if (const auto document = media->document()) { - item->applyEditionToHistoryCleared(); - auto text = (document->isVideoFile() - ? tr::lng_ttl_video_expired - : document->isVoiceMessage() - ? tr::lng_ttl_voice_expired - : document->isVideoMessage() - ? tr::lng_ttl_round_expired - : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); - item->updateServiceText(PreparedServiceText{ std::move(text) }); - } else if (media->photo()) { - item->applyEditionToHistoryCleared(); - item->updateServiceText(PreparedServiceText{ - tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) - }); - } - } -} - int ItemsForwardSendersCount(const HistoryItemsList &list) { auto peers = base::flat_set>(); auto names = base::flat_set(); diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index dfe2c737c3..11270500f6 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -257,7 +257,5 @@ ClickHandlerPtr JumpToStoryClickHandler( void ShowTrialTranscribesToast(int left, TimeId until); -void ClearMediaAsExpired(not_null item); - [[nodiscard]] int ItemsForwardSendersCount(const HistoryItemsList &list); [[nodiscard]] int ItemsForwardCaptionsCount(const HistoryItemsList &list); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 0c9521d4db..bfaddd394a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" #include "history/history_item_components.h" -#include "history/history_item_helpers.h" // ClearMediaAsExpired. +#include "history/history_item.h" #include "history/history.h" #include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty. #include "history/view/history_view_element.h" @@ -330,7 +330,7 @@ Document::Document( } if (const auto item = data->message(fullId)) { // Destroys this. - ClearMediaAsExpired(item); + item->clearMediaAsExpired(); } }, *lifetime); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 8f76983390..ad561899ef 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -25,7 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/rect.h" #include "history/history_item_components.h" -#include "history/history_item_helpers.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" @@ -176,7 +175,7 @@ Gif::Gif( if (!isOut) { if (const auto item = data->message(fullId)) { // Destroys this. - ClearMediaAsExpired(item); + item->clearMediaAsExpired(); } } }, *lifetime);