Improve emoji animation.

This commit is contained in:
John Preston 2025-04-15 20:05:20 +04:00
parent 29d87f692a
commit 70378bfac8
6 changed files with 158 additions and 32 deletions

View file

@ -4969,6 +4969,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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" = "End-to-End Encrypted";
"lng_confcall_e2e_badge_small" = "E2E Encrypted"; "lng_confcall_e2e_badge_small" = "E2E Encrypted";
"lng_confcall_e2e_about" = "These four emoji represent the call's encryption key. They must match for all participants and change when someone joins or leaves.";
"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

@ -1680,3 +1680,7 @@ confcallFingerprintText: FlatLabel(defaultFlatLabel) {
} }
} }
confcallFingerprintSkip: 2px; confcallFingerprintSkip: 2px;
confcallFingerprintTooltipLabel: defaultImportantTooltipLabel;
confcallFingerprintTooltip: defaultImportantTooltip;
confcallFingerprintTooltipSkip: 12px;
confcallFingerprintTooltipMaxWidth: 220px;

View file

@ -12,20 +12,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#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/text/text_utilities.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h" #include "ui/widgets/tooltip.h"
#include "ui/abstract_button.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"
#include "ui/ui_utility.h"
#include "styles/style_calls.h" #include "styles/style_calls.h"
namespace Calls { namespace Calls {
namespace { namespace {
constexpr auto kTooltipShowTimeoutMs = crl::time(1000); constexpr auto kTooltipShowTimeoutMs = crl::time(1000);
constexpr auto kCarouselOneDuration = crl::time(1000);// crl::time(100); constexpr auto kCarouselOneDuration = crl::time(100);
constexpr auto kStartTimeShift = crl::time(500);// crl::time(50); constexpr auto kStartTimeShift = crl::time(50);
constexpr auto kEmojiInFingerprint = 4; constexpr auto kEmojiInFingerprint = 4;
constexpr auto kEmojiInCarousel = 10; constexpr auto kEmojiInCarousel = 10;
@ -317,6 +319,7 @@ FingerprintBadge SetupFingerprintBadge(
}; };
const auto state = on.make_state<State>(); const auto state = on.make_state<State>();
state->data.speed = 1. / kCarouselOneDuration;
state->update = [=](crl::time now) { state->update = [=](crl::time now) {
// speed-up-duration = 2 * one / speed. // speed-up-duration = 2 * one / speed.
const auto one = 1.; const auto one = 1.;
@ -546,13 +549,112 @@ FingerprintBadge SetupFingerprintBadge(
return { .state = &state->data, .repaints = state->repaints.events() }; return { .state = &state->data, .repaints = state->repaints.events() };
} }
void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
struct State {
std::unique_ptr<Ui::ImportantTooltip> tooltip;
Fn<void()> updateGeometry;
Fn<void(bool)> toggleTooltip;
};
const auto state = widget->lifetime().make_state<State>();
state->updateGeometry = [=] {
if (!state->tooltip.get()) {
return;
}
const auto geometry = Ui::MapFrom(
widget->window(),
widget,
widget->rect());
if (geometry.isEmpty()) {
state->toggleTooltip(false);
return;
}
const auto weak = QPointer<QWidget>(state->tooltip.get());
const auto countPosition = [=](QSize size) {
const auto result = geometry.bottomLeft()
+ QPoint(
geometry.width() / 2,
st::confcallFingerprintTooltipSkip)
- QPoint(size.width() / 2, 0);
return result;
};
state->tooltip.get()->pointAt(
geometry,
RectPart::Bottom,
countPosition);
};
state->toggleTooltip = [=](bool show) {
if (const auto was = state->tooltip.release()) {
was->toggleAnimated(false);
}
if (!show) {
return;
}
const auto text = tr::lng_confcall_e2e_about(
tr::now,
Ui::Text::WithEntities);
if (text.empty()) {
return;
}
state->tooltip = std::make_unique<Ui::ImportantTooltip>(
widget->window(),
Ui::MakeNiceTooltipLabel(
widget,
rpl::single(text),
st::confcallFingerprintTooltipMaxWidth,
st::confcallFingerprintTooltipLabel),
st::confcallFingerprintTooltip);
const auto raw = state->tooltip.get();
const auto weak = QPointer<QWidget>(raw);
const auto destroy = [=] {
delete weak.data();
};
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
raw->setHiddenCallback(destroy);
state->updateGeometry();
raw->toggleAnimated(true);
};
widget->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::Enter) {
state->toggleTooltip(true);
} else if (type == QEvent::Leave) {
state->toggleTooltip(false);
}
}, widget->lifetime());
}
QImage MakeVerticalShadow(int height) {
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(1, height) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
auto g = QLinearGradient(0, 0, 0, height);
auto color = st::groupCallMembersBg->c;
auto trans = color;
trans.setAlpha(0);
g.setStops({
{ 0.0, color },
{ 0.4, trans },
{ 0.6, trans },
{ 1.0, color },
});
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(0, 0, 1, height, g);
p.end();
return result;
}
void SetupFingerprintBadgeWidget( void SetupFingerprintBadgeWidget(
not_null<Ui::RpWidget*> widget, not_null<Ui::RpWidget*> widget,
not_null<const FingerprintBadgeState*> state, not_null<const FingerprintBadgeState*> state,
rpl::producer<> repaints) { rpl::producer<> repaints) {
auto &lifetime = widget->lifetime(); auto &lifetime = widget->lifetime();
const auto button = Ui::CreateChild<Ui::AbstractButton>(widget); const auto button = Ui::CreateChild<Ui::RpWidget>(widget);
button->show(); button->show();
const auto label = Ui::CreateChild<Ui::FlatLabel>( const auto label = Ui::CreateChild<Ui::FlatLabel>(
@ -633,12 +735,19 @@ void SetupFingerprintBadgeWidget(
st::confcallFingerprintTextMargins); st::confcallFingerprintTextMargins);
const auto count = int(state->entries.size()); const auto count = int(state->entries.size());
cache->entries.resize(count); cache->entries.resize(count);
cache->shadow = MakeVerticalShadow(outer.height());
for (auto i = 0; i != count; ++i) { for (auto i = 0; i != count; ++i) {
PaintFingerprintEntry( const auto &entry = state->entries[i];
p, auto &cached = cache->entries[i];
state->entries[i], const auto shadowed = entry.speed / state->speed;
cache->entries[i], PaintFingerprintEntry(p, entry, cached, esize);
esize); if (shadowed > 0.) {
p.setOpacity(shadowed);
p.drawImage(
QRect(0, -st::confcallFingerprintMargins.top(), size, outer.height()),
cache->shadow);
p.setOpacity(1.);
}
if (i + 1 == count / 2) { if (i + 1 == count / 2) {
p.translate(size + textOuter.width(), 0); p.translate(size + textOuter.width(), 0);
} else { } else {
@ -650,6 +759,8 @@ void SetupFingerprintBadgeWidget(
std::move(repaints) | rpl::start_with_next([=] { std::move(repaints) | rpl::start_with_next([=] {
button->update(); button->update();
}, lifetime); }, lifetime);
SetupFingerprintTooltip(button);
} }
void PaintFingerprintEntry( void PaintFingerprintEntry(

View file

@ -37,6 +37,7 @@ struct FingerprintBadgeState {
int added = 0; int added = 0;
}; };
std::vector<Entry> entries; std::vector<Entry> entries;
float64 speed = 1.;
}; };
struct FingerprintBadge { struct FingerprintBadge {
not_null<const FingerprintBadgeState*> state; not_null<const FingerprintBadgeState*> state;
@ -60,6 +61,7 @@ struct FingerprintBadgeCache {
std::vector<Emoji> emoji; std::vector<Emoji> emoji;
}; };
std::vector<Entry> entries; std::vector<Entry> entries;
QImage shadow;
}; };
void PaintFingerprintEntry( void PaintFingerprintEntry(
QPainter &p, QPainter &p,

View file

@ -115,6 +115,7 @@ private:
struct PrioritizedSelector { struct PrioritizedSelector {
object_ptr<Ui::RpWidget> content = { nullptr }; object_ptr<Ui::RpWidget> content = { nullptr };
Fn<void()> init;
Fn<bool(int, int, int)> overrideKey; Fn<bool(int, int, int)> overrideKey;
Fn<void(PeerListRowId)> deselect; Fn<void(PeerListRowId)> deselect;
Fn<void()> activate; Fn<void()> activate;
@ -320,7 +321,7 @@ void ConfInviteRow::elementsPaint(
[[nodiscard]] PrioritizedSelector PrioritizedInviteSelector( [[nodiscard]] PrioritizedSelector PrioritizedInviteSelector(
const ConfInviteStyles &st, const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users, std::vector<not_null<UserData*>> users,
Fn<bool(not_null<PeerListRow*>, bool)> toggleGetChecked, Fn<bool(not_null<PeerListRow*>, bool, anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo, Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo) { Fn<void(bool)> setLastSelectWithVideo) {
class PrioritizedController final : public PeerListController { class PrioritizedController final : public PeerListController {
@ -328,7 +329,10 @@ void ConfInviteRow::elementsPaint(
PrioritizedController( PrioritizedController(
const ConfInviteStyles &st, const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users, std::vector<not_null<UserData*>> users,
Fn<bool(not_null<PeerListRow*>, bool)> toggleGetChecked, Fn<bool(
not_null<PeerListRow*>,
bool,
anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo, Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo) Fn<void(bool)> setLastSelectWithVideo)
: _st(st) : _st(st)
@ -368,7 +372,7 @@ void ConfInviteRow::elementsPaint(
void toggleRowSelected(not_null<PeerListRow*> row, bool video) { void toggleRowSelected(not_null<PeerListRow*> row, bool video) {
delegate()->peerListSetRowChecked( delegate()->peerListSetRowChecked(
row, row,
_toggleGetChecked(row, video)); _toggleGetChecked(row, video, anim::type::normal));
} }
Main::Session &session() const override { Main::Session &session() const override {
@ -382,7 +386,7 @@ void ConfInviteRow::elementsPaint(
private: private:
const ConfInviteStyles &_st; const ConfInviteStyles &_st;
std::vector<not_null<UserData*>> _users; std::vector<not_null<UserData*>> _users;
Fn<bool(not_null<PeerListRow*>, bool)> _toggleGetChecked; Fn<bool(not_null<PeerListRow*>, bool, anim::type)> _toggleGetChecked;
Fn<bool()> _lastSelectWithVideo; Fn<bool()> _lastSelectWithVideo;
Fn<void(bool)> _setLastSelectWithVideo; Fn<void(bool)> _setLastSelectWithVideo;
@ -448,8 +452,19 @@ void ConfInviteRow::elementsPaint(
} }
}; };
const auto init = [=] {
for (const auto &user : users) {
if (const auto row = delegate->peerListFindRow(user->id.value)) {
delegate->peerListSetRowChecked(
row,
toggleGetChecked(row, false, anim::type::instant));
}
}
};
return { return {
.content = std::move(result), .content = std::move(result),
.init = init,
.overrideKey = overrideKey, .overrideKey = overrideKey,
.deselect = deselect, .deselect = deselect,
.activate = activate, .activate = activate,
@ -608,12 +623,15 @@ void ConfInviteController::prepareViewHook() {
} }
void ConfInviteController::addPriorityInvites() { void ConfInviteController::addPriorityInvites() {
const auto toggleGetChecked = [=](not_null<PeerListRow*> row, bool video) { const auto toggleGetChecked = [=](
not_null<PeerListRow*> row,
bool video,
anim::type animated) {
const auto result = toggleRowGetChecked(row, video); const auto result = toggleRowGetChecked(row, video);
delegate()->peerListSetForeignRowChecked( delegate()->peerListSetForeignRowChecked(
row, row,
result, result,
anim::type::normal); animated);
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0); _hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
@ -630,6 +648,13 @@ void ConfInviteController::addPriorityInvites() {
scrollTo scrollTo
) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime()); ) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime());
} }
if (const auto onstack = _prioritizeRows.init) {
onstack();
// Force finishing in instant adding checked rows bunch.
delegate()->peerListAddSelectedPeers(
std::vector<not_null<PeerData*>>());
}
delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content)); delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content));
} }

View file

@ -77,23 +77,6 @@ constexpr auto kControlsBackgroundOpacity = 0.8;
constexpr auto kOverrideActiveColorBgAlpha = 172; constexpr auto kOverrideActiveColorBgAlpha = 172;
constexpr auto kHideControlsTimeout = 5 * crl::time(1000); constexpr auto kHideControlsTimeout = 5 * crl::time(1000);
[[nodiscard]] QString ComposeTitle(const QByteArray &hash) {
auto result = tr::lng_confcall_join_title(tr::now);
if (hash.size() >= 32) {
const auto fp = bytes::make_span(hash).subspan(0, 32);
const auto emoji = Calls::ComputeEmojiFingerprint(fp);
result += QString::fromUtf8(" \xc2\xb7 ");
const auto base = result.size();
for (const auto &single : emoji) {
result += single->text();
}
MTP_LOG(0, ("Got Emoji: %1.").arg(result.mid(base)));
} else {
MTP_LOG(0, ("Cleared Emoji."));
}
return result;
}
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
void UnpinMaximized(not_null<QWidget*> widget) { void UnpinMaximized(not_null<QWidget*> widget) {
SetWindowPos( SetWindowPos(
@ -2417,7 +2400,7 @@ void Panel::updateMembersGeometry() {
rpl::producer<QString> Panel::titleText() { rpl::producer<QString> Panel::titleText() {
if (_call->conference()) { if (_call->conference()) {
return _call->emojiHashValue() | rpl::map(ComposeTitle); return tr::lng_confcall_join_title();
} }
return rpl::combine( return rpl::combine(
Info::Profile::NameValue(_peer), Info::Profile::NameValue(_peer),