diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 74c0ad438f..cb2373a855 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4969,6 +4969,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_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."; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index bc2d11a4e2..4728b41d37 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1680,3 +1680,7 @@ confcallFingerprintText: FlatLabel(defaultFlatLabel) { } } confcallFingerprintSkip: 2px; +confcallFingerprintTooltipLabel: defaultImportantTooltipLabel; +confcallFingerprintTooltip: defaultImportantTooltip; +confcallFingerprintTooltipSkip: 12px; +confcallFingerprintTooltipMaxWidth: 220px; diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index 580005ec8c..164f754c3d 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -12,20 +12,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_signal_bars.h" #include "lang/lang_keys.h" #include "data/data_user.h" +#include "ui/text/text_utilities.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" +#include "ui/ui_utility.h" #include "styles/style_calls.h" namespace Calls { namespace { 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 kCarouselOneDuration = crl::time(100); +constexpr auto kStartTimeShift = crl::time(50); constexpr auto kEmojiInFingerprint = 4; constexpr auto kEmojiInCarousel = 10; @@ -317,6 +319,7 @@ FingerprintBadge SetupFingerprintBadge( }; const auto state = on.make_state(); + state->data.speed = 1. / kCarouselOneDuration; state->update = [=](crl::time now) { // speed-up-duration = 2 * one / speed. const auto one = 1.; @@ -546,13 +549,112 @@ FingerprintBadge SetupFingerprintBadge( return { .state = &state->data, .repaints = state->repaints.events() }; } +void SetupFingerprintTooltip(not_null widget) { + struct State { + std::unique_ptr tooltip; + Fn updateGeometry; + Fn toggleTooltip; + }; + const auto state = widget->lifetime().make_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(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( + widget->window(), + Ui::MakeNiceTooltipLabel( + widget, + rpl::single(text), + st::confcallFingerprintTooltipMaxWidth, + st::confcallFingerprintTooltipLabel), + st::confcallFingerprintTooltip); + const auto raw = state->tooltip.get(); + const auto weak = QPointer(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 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( not_null widget, not_null state, rpl::producer<> repaints) { auto &lifetime = widget->lifetime(); - const auto button = Ui::CreateChild(widget); + const auto button = Ui::CreateChild(widget); button->show(); const auto label = Ui::CreateChild( @@ -633,12 +735,19 @@ void SetupFingerprintBadgeWidget( st::confcallFingerprintTextMargins); const auto count = int(state->entries.size()); cache->entries.resize(count); + cache->shadow = MakeVerticalShadow(outer.height()); for (auto i = 0; i != count; ++i) { - PaintFingerprintEntry( - p, - state->entries[i], - cache->entries[i], - esize); + const auto &entry = state->entries[i]; + auto &cached = cache->entries[i]; + const auto shadowed = entry.speed / state->speed; + PaintFingerprintEntry(p, entry, cached, 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) { p.translate(size + textOuter.width(), 0); } else { @@ -650,6 +759,8 @@ void SetupFingerprintBadgeWidget( std::move(repaints) | rpl::start_with_next([=] { button->update(); }, lifetime); + + SetupFingerprintTooltip(button); } void PaintFingerprintEntry( diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h index 21b063a946..56776b8e1b 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h @@ -37,6 +37,7 @@ struct FingerprintBadgeState { int added = 0; }; std::vector entries; + float64 speed = 1.; }; struct FingerprintBadge { not_null state; @@ -60,6 +61,7 @@ struct FingerprintBadgeCache { std::vector emoji; }; std::vector entries; + QImage shadow; }; void PaintFingerprintEntry( QPainter &p, diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index 1bdf5ed562..cd04eb32fa 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -115,6 +115,7 @@ private: struct PrioritizedSelector { object_ptr content = { nullptr }; + Fn init; Fn overrideKey; Fn deselect; Fn activate; @@ -320,7 +321,7 @@ void ConfInviteRow::elementsPaint( [[nodiscard]] PrioritizedSelector PrioritizedInviteSelector( const ConfInviteStyles &st, std::vector> users, - Fn, bool)> toggleGetChecked, + Fn, bool, anim::type)> toggleGetChecked, Fn lastSelectWithVideo, Fn setLastSelectWithVideo) { class PrioritizedController final : public PeerListController { @@ -328,7 +329,10 @@ void ConfInviteRow::elementsPaint( PrioritizedController( const ConfInviteStyles &st, std::vector> users, - Fn, bool)> toggleGetChecked, + Fn, + bool, + anim::type)> toggleGetChecked, Fn lastSelectWithVideo, Fn setLastSelectWithVideo) : _st(st) @@ -368,7 +372,7 @@ void ConfInviteRow::elementsPaint( void toggleRowSelected(not_null row, bool video) { delegate()->peerListSetRowChecked( row, - _toggleGetChecked(row, video)); + _toggleGetChecked(row, video, anim::type::normal)); } Main::Session &session() const override { @@ -382,7 +386,7 @@ void ConfInviteRow::elementsPaint( private: const ConfInviteStyles &_st; std::vector> _users; - Fn, bool)> _toggleGetChecked; + Fn, bool, anim::type)> _toggleGetChecked; Fn _lastSelectWithVideo; Fn _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 { .content = std::move(result), + .init = init, .overrideKey = overrideKey, .deselect = deselect, .activate = activate, @@ -608,12 +623,15 @@ void ConfInviteController::prepareViewHook() { } void ConfInviteController::addPriorityInvites() { - const auto toggleGetChecked = [=](not_null row, bool video) { + const auto toggleGetChecked = [=]( + not_null row, + bool video, + anim::type animated) { const auto result = toggleRowGetChecked(row, video); delegate()->peerListSetForeignRowChecked( row, result, - anim::type::normal); + animated); _hasSelected = (delegate()->peerListSelectedRowsCount() > 0); @@ -630,6 +648,13 @@ void ConfInviteController::addPriorityInvites() { scrollTo ) | 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>()); + } delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content)); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index a0157c3de0..3a40c36f49 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -77,23 +77,6 @@ constexpr auto kControlsBackgroundOpacity = 0.8; constexpr auto kOverrideActiveColorBgAlpha = 172; 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 void UnpinMaximized(not_null widget) { SetWindowPos( @@ -2417,7 +2400,7 @@ void Panel::updateMembersGeometry() { rpl::producer Panel::titleText() { if (_call->conference()) { - return _call->emojiHashValue() | rpl::map(ComposeTitle); + return tr::lng_confcall_join_title(); } return rpl::combine( Info::Profile::NameValue(_peer),