Add carousel animation for emoji fingerprint.

This commit is contained in:
John Preston 2025-04-15 17:00:43 +04:00
parent b1b2798be1
commit 29d87f692a
15 changed files with 644 additions and 103 deletions

View file

@ -4967,6 +4967,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_confcall_not_accessible" = "This call is no longer accessible."; "lng_confcall_not_accessible" = "This call is no longer accessible.";
"lng_confcall_participants_limit" = "This call reached the participants limit."; "lng_confcall_participants_limit" = "This call reached the participants limit.";
"lng_confcall_sure_remove" = "Remove {user} from the call?"; "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."; "lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";

View file

@ -1316,7 +1316,7 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {
user->madeAction(base::unixtime::now()); user->madeAction(base::unixtime::now());
} }
} }
ClearMediaAsExpired(item); item->clearMediaAsExpired();
} }
} else { } else {
// Perhaps it was an unread mention! // Perhaps it was an unread mention!

View file

@ -1669,3 +1669,14 @@ groupCallLinkMenu: IconButton(confcallLinkMenu) {
color: groupCallMembersBgOver; 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;

View file

@ -7,11 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "calls/calls_emoji_fingerprint.h" #include "calls/calls_emoji_fingerprint.h"
#include "base/random.h"
#include "calls/calls_call.h" #include "calls/calls_call.h"
#include "calls/calls_signal_bars.h" #include "calls/calls_signal_bars.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h" #include "ui/widgets/tooltip.h"
#include "ui/abstract_button.h"
#include "ui/emoji_config.h" #include "ui/emoji_config.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
@ -20,7 +23,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Calls { namespace Calls {
namespace { 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[] = { const ushort Data[] = {
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21, 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, 620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
642, 643, 644, 646, 648, 650, 652, 654, 656, 658 }; 642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };
constexpr auto kEmojiCount = (base::array_size(Offsets) - 1);
uint64 ComputeEmojiIndex(bytes::const_span bytes) { uint64 ComputeEmojiIndex(bytes::const_span bytes) {
Expects(bytes.size() == 8); Expects(bytes.size() == 8);
return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56) return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)
| (gsl::to_integer<uint64>(bytes[1]) << 48) | (gsl::to_integer<uint64>(bytes[1]) << 48)
| (gsl::to_integer<uint64>(bytes[2]) << 40) | (gsl::to_integer<uint64>(bytes[2]) << 40)
@ -121,6 +131,17 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) {
| (gsl::to_integer<uint64>(bytes[7])); | (gsl::to_integer<uint64>(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<const QChar*>(Data + offset),
size);
return Ui::Emoji::Find(string);
}
} // namespace } // namespace
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) { std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
@ -133,22 +154,13 @@ std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
std::vector<EmojiPtr> ComputeEmojiFingerprint( std::vector<EmojiPtr> ComputeEmojiFingerprint(
bytes::const_span fingerprint) { bytes::const_span fingerprint) {
auto result = std::vector<EmojiPtr>(); auto result = std::vector<EmojiPtr>();
constexpr auto EmojiCount = (base::array_size(Offsets) - 1);
constexpr auto kPartSize = 8; constexpr auto kPartSize = 8;
for (auto partOffset = 0 for (auto partOffset = 0
; partOffset != fingerprint.size() ; partOffset != fingerprint.size()
; partOffset += kPartSize) { ; partOffset += kPartSize) {
auto value = ComputeEmojiIndex( const auto value = ComputeEmojiIndex(
fingerprint.subspan(partOffset, kPartSize)); fingerprint.subspan(partOffset, kPartSize));
auto index = value % EmojiCount; result.push_back(EmojiByIndex(value % kEmojiCount));
auto offset = Offsets[index];
auto size = Offsets[index + 1] - offset;
auto string = QString::fromRawData(
reinterpret_cast<const QChar*>(Data + offset),
size);
auto emoji = Ui::Emoji::Find(string);
Assert(emoji != nullptr);
result.push_back(emoji);
} }
return result; return result;
} }
@ -294,4 +306,407 @@ base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
return result; return result;
} }
FingerprintBadge SetupFingerprintBadge(
rpl::lifetime &on,
rpl::producer<QByteArray> fingerprint) {
struct State {
FingerprintBadgeState data;
Ui::Animations::Basic animation;
Fn<void(crl::time)> update;
rpl::event_stream<> repaints;
};
const auto state = on.make_state<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<uint32> &buffered) {
auto &entry = state->data.entries[index];
auto indices = std::vector<int>();
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<uint32> &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<uint32>(
kEmojiInCarousel * kEmojiInFingerprint);
const auto now = crl::now();
const auto emoji = (fingerprint.size() >= 32)
? ComputeEmojiFingerprint(
bytes::make_span(fingerprint).subspan(0, 32))
: std::vector<EmojiPtr>();
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<Ui::RpWidget*> widget,
not_null<const FingerprintBadgeState*> state,
rpl::producer<> repaints) {
auto &lifetime = widget->lifetime();
const auto button = Ui::CreateChild<Ui::AbstractButton>(widget);
button->show();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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<FingerprintBadgeCache>();
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 } // namespace Calls

View file

@ -26,4 +26,45 @@ class Call;
not_null<QWidget*> parent, not_null<QWidget*> parent,
not_null<Call*> call); not_null<Call*> call);
struct FingerprintBadgeState {
struct Entry {
EmojiPtr emoji = nullptr;
std::vector<EmojiPtr> sliding;
std::vector<EmojiPtr> carousel;
crl::time time = 0;
float64 speed = 0.;
float64 position = 0.;
int added = 0;
};
std::vector<Entry> entries;
};
struct FingerprintBadge {
not_null<const FingerprintBadgeState*> state;
rpl::producer<> repaints;
};
FingerprintBadge SetupFingerprintBadge(
rpl::lifetime &on,
rpl::producer<QByteArray> fingerprint);
void SetupFingerprintBadgeWidget(
not_null<Ui::RpWidget*> widget,
not_null<const FingerprintBadgeState*> state,
rpl::producer<> repaints);
struct FingerprintBadgeCache {
struct Emoji {
EmojiPtr ptr = nullptr;
QImage image;
};
struct Entry {
std::vector<Emoji> emoji;
};
std::vector<Entry> entries;
};
void PaintFingerprintEntry(
QPainter &p,
const FingerprintBadgeState::Entry &entry,
FingerprintBadgeCache::Entry &cache,
int esize);
} // namespace Calls } // namespace Calls

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_volume_item.h" #include "calls/group/calls_volume_item.h"
#include "calls/group/calls_group_members_row.h" #include "calls/group/calls_group_members_row.h"
#include "calls/group/calls_group_viewport.h" #include "calls/group/calls_group_viewport.h"
#include "calls/calls_emoji_fingerprint.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
@ -1745,6 +1746,9 @@ Members::Members(
, _listController(std::make_unique<Controller>(call, parent, mode)) , _listController(std::make_unique<Controller>(call, parent, mode))
, _layout(_scroll->setOwnedWidget( , _layout(_scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(_scroll.data()))) object_ptr<Ui::VerticalLayout>(_scroll.data())))
, _fingerprint(call->conference()
? _layout->add(object_ptr<Ui::RpWidget>(_layout.get()))
: nullptr)
, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get()))) , _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))
, _viewport( , _viewport(
std::make_unique<Viewport>( std::make_unique<Viewport>(
@ -1753,6 +1757,7 @@ Members::Members(
backend)) { backend)) {
setupList(); setupList();
setupAddMember(call); setupAddMember(call);
setupFingerprint();
setContent(_list); setContent(_list);
setupFakeRoundCorners(); setupFakeRoundCorners();
_listController->setDelegate(static_cast<PeerListDelegate*>(this)); _listController->setDelegate(static_cast<PeerListDelegate*>(this));
@ -1854,6 +1859,8 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
_canInviteByLink = canInviteByLinkByPeer(channel); _canInviteByLink = canInviteByLinkByPeer(channel);
}); });
const auto baseIndex = _layout->count() - 2;
rpl::combine( rpl::combine(
_canAddMembers.value(), _canAddMembers.value(),
_canInviteByLink.value(), _canInviteByLink.value(),
@ -1887,7 +1894,7 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
addMember->resizeToWidth(_layout->width()); addMember->resizeToWidth(_layout->width());
delete _addMemberButton.current(); delete _addMemberButton.current();
_addMemberButton = addMember.data(); _addMemberButton = addMember.data();
_layout->insert(3, std::move(addMember)); _layout->insert(baseIndex, std::move(addMember));
if (conference) { if (conference) {
auto shareLink = Settings::CreateButtonWithIcon( auto shareLink = Settings::CreateButtonWithIcon(
_layout.get(), _layout.get(),
@ -1901,7 +1908,7 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
shareLink->resizeToWidth(_layout->width()); shareLink->resizeToWidth(_layout->width());
delete _shareLinkButton.current(); delete _shareLinkButton.current();
_shareLinkButton = shareLink.data(); _shareLinkButton = shareLink.data();
_layout->insert(4, std::move(shareLink)); _layout->insert(baseIndex + 1, std::move(shareLink));
} }
}, lifetime()); }, lifetime());
@ -2000,6 +2007,23 @@ void Members::setupList() {
}, _scroll->lifetime()); }, _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() { void Members::trackViewportGeometry() {
_call->videoEndpointLargeValue( _call->videoEndpointLargeValue(
) | rpl::start_with_next([=](const VideoEndpoint &large) { ) | rpl::start_with_next([=](const VideoEndpoint &large) {

View file

@ -25,6 +25,7 @@ class GroupCall;
namespace Calls { namespace Calls {
class GroupCall; class GroupCall;
struct FingerprintBadgeState;
} // namespace Calls } // namespace Calls
namespace Calls::Group { namespace Calls::Group {
@ -96,6 +97,7 @@ private:
void setupAddMember(not_null<GroupCall*> call); void setupAddMember(not_null<GroupCall*> call);
void resizeToList(); void resizeToList();
void setupList(); void setupList();
void setupFingerprint();
void setupFakeRoundCorners(); void setupFakeRoundCorners();
void trackViewportGeometry(); void trackViewportGeometry();
@ -106,6 +108,9 @@ private:
object_ptr<Ui::ScrollArea> _scroll; object_ptr<Ui::ScrollArea> _scroll;
std::unique_ptr<Controller> _listController; std::unique_ptr<Controller> _listController;
not_null<Ui::VerticalLayout*> _layout; not_null<Ui::VerticalLayout*> _layout;
const not_null<Ui::RpWidget*> _fingerprint;
rpl::event_stream<> _fingerprintRepaints;
const FingerprintBadgeState *_fingerprintState = nullptr;
const not_null<Ui::RpWidget*> _videoWrap; const not_null<Ui::RpWidget*> _videoWrap;
std::unique_ptr<Viewport> _viewport; std::unique_ptr<Viewport> _viewport;
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr; rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;

View file

@ -426,10 +426,12 @@ HistoryItem::HistoryItem(
} else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive) } else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive)
|| (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) { || (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) {
createServiceFromMtp(data); createServiceFromMtp(data);
setReactions(data.vreactions());
applyTTL(data); applyTTL(data);
} else if (checked == MediaCheckResult::HasStoryMention) { } else if (checked == MediaCheckResult::HasStoryMention) {
setMedia(*data.vmedia()); setMedia(*data.vmedia());
createServiceFromMtp(data); createServiceFromMtp(data);
setReactions(data.vreactions());
applyTTL(data); applyTTL(data);
} else { } else {
createComponents(data); createComponents(data);
@ -1830,8 +1832,18 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
auto updatedText = checkedMedia auto updatedText = checkedMedia
? edition.textWithEntities ? edition.textWithEntities
: EnsureNonEmpty(edition.textWithEntities); : EnsureNonEmpty(edition.textWithEntities);
auto serviceText = (!checkedMedia
&& edition.textWithEntities.empty()
&& edition.mtpMedia)
? prepareServiceTextForMessage(
*edition.mtpMedia,
edition.isMediaUnread)
: PreparedServiceText();
if (updatingSavedLocalEdit) { if (updatingSavedLocalEdit) {
Get<HistoryMessageSavedMediaData>()->text = std::move(updatedText); Get<HistoryMessageSavedMediaData>()->text = std::move(updatedText);
} else if (!serviceText.text.empty()) {
setServiceText(std::move(serviceText));
addToSharedMediaIndex();
} else { } else {
setText(std::move(updatedText)); setText(std::move(updatedText));
addToSharedMediaIndex(); addToSharedMediaIndex();
@ -1920,6 +1932,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
addToSharedMediaIndex(); addToSharedMediaIndex();
finishEdition(-1); finishEdition(-1);
_flags &= ~MessageFlag::DisplayFromChecked; _flags &= ~MessageFlag::DisplayFromChecked;
updateReactions(message.vreactions());
} else if (isService()) { } else if (isService()) {
if (const auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
reply->clearData(this); reply->clearData(this);
@ -1930,6 +1944,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
applyServiceDateEdition(message); applyServiceDateEdition(message);
finishEdition(-1); finishEdition(-1);
_flags &= ~MessageFlag::DisplayFromChecked; _flags &= ~MessageFlag::DisplayFromChecked;
updateReactions(message.vreactions());
} }
const auto nowSublist = savedSublist(); const auto nowSublist = savedSublist();
if (wasSublist && nowSublist != wasSublist) { 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) { void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) {
if (!isRegular()) { if (!isRegular()) {
return; 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) { void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
AddComponents(HistoryServiceData::Bit()); AddComponents(HistoryServiceData::Bit());
_flags |= MessageFlag::ReactionsAllowed;
const auto unread = message.is_media_unread(); const auto unread = message.is_media_unread();
const auto media = message.vmedia(); const auto media = message.vmedia();
Assert(media != nullptr); Assert(media != nullptr);
@ -4182,24 +4300,6 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
Assert(ttl != nullptr); Assert(ttl != nullptr);
setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, *ttl); 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) { }, [&](const MTPDmessageMediaDocument &data) {
if (unread) { if (unread) {
@ -4210,49 +4310,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
setSelfDestruct( setSelfDestruct(
HistoryServiceSelfDestruct::Type::Video, HistoryServiceSelfDestruct::Type::Video,
*ttl); *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) { }, [](const auto &) {});
setServiceText(prepareStoryMentionText());
}, [](const auto &) {
Unexpected("Media type in HistoryItem::createServiceFromMtp()");
});
if (const auto reactions = message.vreactions()) { setServiceText(prepareServiceTextForMessage(*media, unread));
updateReactions(reactions);
}
} }
void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {

View file

@ -380,6 +380,8 @@ public:
void updateReplyMarkup(HistoryMessageMarkupData &&markup); void updateReplyMarkup(HistoryMessageMarkupData &&markup);
void contributeToSlowmode(TimeId realDate = 0); void contributeToSlowmode(TimeId realDate = 0);
void clearMediaAsExpired();
void addToUnreadThings(HistoryUnreadThings::AddType type); void addToUnreadThings(HistoryUnreadThings::AddType type);
void destroyHistoryEntry(); void destroyHistoryEntry();
[[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const; [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const;
@ -670,6 +672,10 @@ private:
[[nodiscard]] PreparedServiceText prepareCallScheduledText( [[nodiscard]] PreparedServiceText prepareCallScheduledText(
TimeId scheduleDate); TimeId scheduleDate);
[[nodiscard]] PreparedServiceText prepareServiceTextForMessage(
const MTPMessageMedia &media,
bool unread);
void flagSensitiveContent(); void flagSensitiveContent();
[[nodiscard]] PeerData *computeDisplayFrom() const; [[nodiscard]] PeerData *computeDisplayFrom() const;

View file

@ -14,6 +14,7 @@ HistoryMessageEdition::HistoryMessageEdition(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const MTPDmessage &message) { const MTPDmessage &message) {
isEditHide = message.is_edit_hide(); isEditHide = message.is_edit_hide();
isMediaUnread = message.is_media_unread();
editDate = message.vedit_date().value_or(-1); editDate = message.vedit_date().value_or(-1);
textWithEntities = TextWithEntities{ textWithEntities = TextWithEntities{
qs(message.vmessage()), qs(message.vmessage()),

View file

@ -20,6 +20,7 @@ struct HistoryMessageEdition {
const MTPDmessage &message); const MTPDmessage &message);
bool isEditHide = false; bool isEditHide = false;
bool isMediaUnread = false;
int editDate = 0; int editDate = 0;
int views = -1; int views = -1;
int forwards = -1; int forwards = -1;

View file

@ -1195,30 +1195,6 @@ void ShowTrialTranscribesToast(int left, TimeId until) {
}); });
} }
void ClearMediaAsExpired(not_null<HistoryItem*> 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) { int ItemsForwardSendersCount(const HistoryItemsList &list) {
auto peers = base::flat_set<not_null<PeerData*>>(); auto peers = base::flat_set<not_null<PeerData*>>();
auto names = base::flat_set<QString>(); auto names = base::flat_set<QString>();

View file

@ -257,7 +257,5 @@ ClickHandlerPtr JumpToStoryClickHandler(
void ShowTrialTranscribesToast(int left, TimeId until); void ShowTrialTranscribesToast(int left, TimeId until);
void ClearMediaAsExpired(not_null<HistoryItem*> item);
[[nodiscard]] int ItemsForwardSendersCount(const HistoryItemsList &list); [[nodiscard]] int ItemsForwardSendersCount(const HistoryItemsList &list);
[[nodiscard]] int ItemsForwardCaptionsCount(const HistoryItemsList &list); [[nodiscard]] int ItemsForwardCaptionsCount(const HistoryItemsList &list);

View file

@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio.h" #include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h" #include "media/player/media_player_instance.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "history/history_item_helpers.h" // ClearMediaAsExpired. #include "history/history_item.h"
#include "history/history.h" #include "history/history.h"
#include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty. #include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty.
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
@ -330,7 +330,7 @@ Document::Document(
} }
if (const auto item = data->message(fullId)) { if (const auto item = data->message(fullId)) {
// Destroys this. // Destroys this.
ClearMediaAsExpired(item); item->clearMediaAsExpired();
} }
}, *lifetime); }, *lifetime);

View file

@ -25,7 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/history.h" #include "history/history.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
@ -176,7 +175,7 @@ Gif::Gif(
if (!isOut) { if (!isOut) {
if (const auto item = data->message(fullId)) { if (const auto item = data->message(fullId)) {
// Destroys this. // Destroys this.
ClearMediaAsExpired(item); item->clearMediaAsExpired();
} }
} }
}, *lifetime); }, *lifetime);