From 4401ea388cf9a83675052762d356fc1ea13d275b Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 Mar 2025 22:13:53 +0500 Subject: [PATCH] PoC confcall invite, emoji. --- .../calls/calls_emoji_fingerprint.cpp | 33 +++++---- .../calls/calls_emoji_fingerprint.h | 2 + .../calls/group/calls_group_call.cpp | 56 +++++++++++++-- .../calls/group/calls_group_call.h | 12 ++++ .../group/calls_group_invite_controller.cpp | 72 ++++++++++++++++++- .../calls/group/calls_group_panel.cpp | 51 +++++++++---- .../calls/group/calls_group_panel.h | 1 + Telegram/SourceFiles/history/history.cpp | 7 ++ Telegram/SourceFiles/history/history_item.cpp | 16 +++++ .../window/window_session_controller.cpp | 27 +++++-- .../window/window_session_controller.h | 3 +- 11 files changed, 238 insertions(+), 42 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index d6b6ca32a3..5c00536ca3 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -124,9 +124,23 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) { } // namespace std::vector ComputeEmojiFingerprint(not_null call) { + if (!call->isKeyShaForFingerprintReady()) { + return {}; + } + return ComputeEmojiFingerprint(call->getKeyShaForFingerprint()); +} + +std::vector ComputeEmojiFingerprint( + bytes::const_span fingerprint) { auto result = std::vector(); constexpr auto EmojiCount = (base::array_size(Offsets) - 1); - for (auto index = 0; index != EmojiCount; ++index) { + constexpr auto kPartSize = 8; + for (auto partOffset = 0 + ; partOffset != fingerprint.size() + ; partOffset += kPartSize) { + 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( @@ -134,22 +148,7 @@ std::vector ComputeEmojiFingerprint(not_null call) { size); auto emoji = Ui::Emoji::Find(string); Assert(emoji != nullptr); - } - if (call->isKeyShaForFingerprintReady()) { - auto sha256 = call->getKeyShaForFingerprint(); - constexpr auto kPartSize = 8; - for (auto partOffset = 0; partOffset != sha256.size(); partOffset += kPartSize) { - auto value = ComputeEmojiIndex(gsl::make_span(sha256).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(emoji); } return result; } diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h index 9ec4e9b684..285f9ebaff 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h @@ -19,6 +19,8 @@ class Call; [[nodiscard]] std::vector ComputeEmojiFingerprint( not_null call); +[[nodiscard]] std::vector ComputeEmojiFingerprint( + bytes::const_span fingerprint); [[nodiscard]] object_ptr CreateFingerprintAndSignalBars( not_null parent, diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 14125ca7c0..30d940693e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -1112,6 +1112,10 @@ bool GroupCall::rtmp() const { return _rtmp; } +bool GroupCall::conference() const { + return _conferenceCall != nullptr; +} + bool GroupCall::listenersHidden() const { return _listenersHidden; } @@ -1152,6 +1156,12 @@ rpl::producer> GroupCall::real() const { return _realChanges.events(); } +rpl::producer GroupCall::emojiHashValue() const { + Expects(_e2e != nullptr); + + return _e2e->emojiHashValue(); +} + void GroupCall::start(TimeId scheduleDate, bool rtmp) { using Flag = MTPphone_CreateGroupCall::Flag; _createRequestId = _api.request(MTPphone_CreateGroupCall( @@ -1610,8 +1620,13 @@ void GroupCall::requestSubchainBlocks(int subchain, int height) { MTP_int(kShortPollChainBlocksPerRequest) )).done([=](const MTPUpdates &result) { auto &state = _subchains[subchain]; - _peer->session().api().applyUpdates(result); state.requestId = 0; + state.inShortPoll = true; + _peer->session().api().applyUpdates(result); + state.inShortPoll = false; + for (const auto &data : base::take(state.pending)) { + applySubChainUpdate(subchain, data.blocks, data.next); + } _e2e->subchainBlocksRequestFinished(subchain); }).fail([=](const MTP::Error &error) { auto &state = _subchains[subchain]; @@ -2221,11 +2236,26 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallChainBlocks &data) { return; } auto &entry = _subchains[subchain]; - const auto inpoll = entry.requestId != 0; + const auto &blocks = data.vblocks().v; const auto next = data.vnext_offset().v; - auto now = next - int(data.vblocks().v.size()); - for (const auto &block : data.vblocks().v) { - _e2e->apply(subchain, now++, { block.v }, inpoll); + if (entry.requestId) { + Assert(!entry.inShortPoll); + entry.pending.push_back({ blocks, next }); + } else { + applySubChainUpdate(subchain, blocks, next); + } +} + +void GroupCall::applySubChainUpdate( + int subchain, + const QVector &blocks, + int next) { + Expects(subchain >= 0 && subchain < kSubChainsCount); + + auto &entry = _subchains[subchain]; + auto now = next - int(blocks.size()); + for (const auto &block : blocks) { + _e2e->apply(subchain, now++, { block.v }, entry.inShortPoll); } } @@ -3667,6 +3697,22 @@ std::variant> GroupCall::inviteUsers( } const auto owner = &_peer->owner(); + if (_conferenceCall) { + for (const auto &user : users) { + _api.request(MTPphone_InviteConferenceCallParticipant( + inputCall(), + user->inputUser + )).send(); + } + auto result = std::variant>(0); + if (users.size() != 1) { + result = int(users.size()); + } else { + result = users.front(); + } + return result; + } + auto count = 0; auto slice = QVector(); auto result = std::variant>(0); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index 71c124f408..64d3a15ad5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -245,6 +245,7 @@ public: } [[nodiscard]] bool scheduleStartSubscribed() const; [[nodiscard]] bool rtmp() const; + [[nodiscard]] bool conference() const; [[nodiscard]] bool listenersHidden() const; [[nodiscard]] bool emptyRtmp() const; [[nodiscard]] rpl::producer emptyRtmpValue() const; @@ -256,6 +257,7 @@ public: [[nodiscard]] Data::GroupCall *lookupReal() const; [[nodiscard]] rpl::producer> real() const; + [[nodiscard]] rpl::producer emojiHashValue() const; void start(TimeId scheduleDate, bool rtmp); void hangup(); @@ -479,8 +481,14 @@ private: ssrc = updatedSsrc; } }; + struct SubChainPending { + QVector blocks; + int next = 0; + }; struct SubChainState { + std::vector pending; mtpRequestId requestId = 0; + bool inShortPoll = false; }; friend inline constexpr bool is_flag_type(SendUpdateType) { @@ -515,6 +523,10 @@ private: void handleUpdate(const MTPDupdateGroupCall &data); void handleUpdate(const MTPDupdateGroupCallParticipants &data); void handleUpdate(const MTPDupdateGroupCallChainBlocks &data); + void applySubChainUpdate( + int subchain, + const QVector &blocks, + int next); bool tryCreateController(); void destroyController(); bool tryCreateScreencast(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index bb8c908559..1e13d45a82 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -56,6 +56,43 @@ namespace { return result; } +class ConfInviteController final : public ContactsBoxController { +public: + ConfInviteController( + not_null session, + base::flat_set> alreadyIn, + Fn)> choose) + : ContactsBoxController(session) + , _alreadyIn(std::move(alreadyIn)) + , _choose(std::move(choose)) { + } + +protected: + std::unique_ptr createRow( + not_null user) override { + if (user->isSelf() + || user->isBot() + || user->isServiceUser() + || user->isInaccessible()) { + return nullptr; + } + auto result = ContactsBoxController::createRow(user); + if (_alreadyIn.contains(user)) { + result->setDisabledState(PeerListRow::State::DisabledChecked); + } + return result; + } + + void rowClicked(not_null row) override { + _choose(row->peer()); + } + +private: + const base::flat_set> _alreadyIn; + const Fn)> _choose; + +}; + } // namespace InviteController::InviteController( @@ -173,6 +210,7 @@ object_ptr PrepareInviteBox( return nullptr; } const auto peer = call->peer(); + const auto weak = base::make_weak(call); auto alreadyIn = peer->owner().invitedToCallUsers(real->id()); for (const auto &participant : real->participants()) { if (const auto user = participant.peer->asUser()) { @@ -180,6 +218,39 @@ object_ptr PrepareInviteBox( } } alreadyIn.emplace(peer->session().user()); + if (call->conference()) { + const auto close = std::make_shared>(); + const auto invite = [=](not_null peer) { + Expects(peer->isUser()); + + const auto call = weak.get(); + if (!call) { + return; + } + const auto user = peer->asUser(); + call->inviteUsers({ user }); + showToast(tr::lng_group_call_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold(user->firstName), + Ui::Text::WithEntities)); + (*close)(); + }; + auto controller = std::make_unique( + &real->session(), + alreadyIn, + invite); + controller->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + auto initBox = [=](not_null box) { + box->setTitle(tr::lng_group_call_invite_title()); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + *close = [=] { box->closeBox(); }; + }; + return Box(std::move(controller), initBox); + } + auto controller = std::make_unique(peer, alreadyIn); controller->setStyleOverrides( &st::groupCallInviteMembersList, @@ -194,7 +265,6 @@ object_ptr PrepareInviteBox( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); - const auto weak = base::make_weak(call); const auto invite = [=](const std::vector> &users) { const auto call = weak.get(); if (!call) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 4ae84ffcc6..e50397b40f 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_invite_controller.h" #include "calls/group/ui/calls_group_scheduled_labels.h" #include "calls/group/ui/desktop_capture_choose_source.h" +#include "calls/calls_emoji_fingerprint.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/platform/ui_platform_utility.h" #include "ui/controls/call_mute_button.h" @@ -76,6 +77,19 @@ 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 "); + for (const auto &single : emoji) { + result += single->text(); + } + } + return result; +} + class Show final : public Main::SessionShow { public: explicit Show(not_null panel); @@ -367,7 +381,13 @@ void Panel::initWindow() { window()->setAttribute(Qt::WA_NoSystemBackground); window()->setTitleStyle(st::groupCallTitle); - subscribeToPeerChanges(); + if (_call->conference()) { + titleText() | rpl::start_with_next([=](const QString &text) { + window()->setTitle(text); + }, lifetime()); + } else { + subscribeToPeerChanges(); + } base::install_event_filter(window().get(), [=](not_null e) { if (e->type() == QEvent::Close && handleClose()) { @@ -2445,19 +2465,26 @@ void Panel::updateMembersGeometry() { } } +rpl::producer Panel::titleText() { + if (_call->conference()) { + return _call->emojiHashValue() | rpl::map(ComposeTitle); + } + return rpl::combine( + Info::Profile::NameValue(_peer), + rpl::single( + QString() + ) | rpl::then(_call->real( + ) | rpl::map([=](not_null real) { + return real->titleValue(); + }) | rpl::flatten_latest()) + ) | rpl::map([=](const QString &name, const QString &title) { + return title.isEmpty() ? name : title; + }); +} + void Panel::refreshTitle() { if (!_title) { - auto text = rpl::combine( - Info::Profile::NameValue(_peer), - rpl::single( - QString() - ) | rpl::then(_call->real( - ) | rpl::map([=](not_null real) { - return real->titleValue(); - }) | rpl::flatten_latest()) - ) | rpl::map([=](const QString &name, const QString &title) { - return title.isEmpty() ? name : title; - }) | rpl::after_next([=] { + auto text = titleText() | rpl::after_next([=] { refreshTitleGeometry(); }); _title.create( diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 851cc91d8f..a591273d06 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -155,6 +155,7 @@ private: void setupMembers(); void setupVideo(not_null viewport); void setupRealMuteButtonState(not_null real); + [[nodiscard]] rpl::producer titleText(); bool handleClose(); void startScheduledNow(); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index bde7012f1b..9e6f44410c 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -52,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "main/main_session.h" #include "window/notifications_manager.h" +#include "window/window_session_controller.h" #include "calls/calls_instance.h" #include "spellcheck/spellcheck_types.h" #include "storage/localstorage.h" @@ -1225,6 +1226,12 @@ void History::applyServiceChanges( topic->setHidden(mtpIsTrue(*hidden)); } } + }, [&](const MTPDmessageActionConferenceCall &data) { + if (!data.is_active() && !data.is_missed()) { + if (const auto window = session().tryResolveWindow()) { + window->resolveConferenceCall(qs(data.vslug()), item->id); + } + } }, [](const auto &) { }); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index dd80c64291..eb227ac323 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -5634,6 +5634,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { : duration ? tr::lng_action_confcall_finished(tr::now) : tr::lng_action_confcall_invitation(tr::now); + + if (duration) { + result.text.text += " (" + QString::number(duration) + " seconds)"; + } + + const auto id = this->id; + const auto slug = qs(action.vslug()); + setCustomServiceLink(std::make_shared([=]( + ClickContext context) { + const auto my = context.other.value(); + const auto weak = my.sessionWindow; + if (const auto strong = weak.get()) { + strong->resolveConferenceCall(slug, id); + } + })); + return result; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index a493903ab3..a8d30105c7 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -840,12 +840,16 @@ void SessionNavigation::resolveCollectible( }).send(); } -void SessionNavigation::resolveConferenceCall(const QString &slug) { - if (_conferenceCallSlug == slug) { +void SessionNavigation::resolveConferenceCall( + const QString &slug, + MsgId inviteMsgId) { + if (_conferenceCallSlug == slug + && _conferenceCallInviteMsgId == inviteMsgId) { return; } _api.request(base::take(_conferenceCallRequestId)).cancel(); _conferenceCallSlug = slug; + _conferenceCallInviteMsgId = inviteMsgId; const auto limit = 5; _conferenceCallRequestId = _api.request(MTPphone_GetGroupCall( @@ -864,15 +868,26 @@ void SessionNavigation::resolveConferenceCall(const QString &slug) { false, // rtmp true); // conference call->processFullCall(result); + const auto confirmed = std::make_shared(); const auto join = [=] { - Core::App().calls().startOrJoinConferenceCall( - uiShow(), - { .call = call, .linkSlug = slug }); + *confirmed = true; + Core::App().calls().startOrJoinConferenceCall(uiShow(), { + .call = call, + .linkSlug = slug, + .joinMessageId = inviteMsgId, + }); }; - uiShow()->show(Box( + const auto box = uiShow()->show(Box( Calls::Group::ConferenceCallJoinConfirm, call, join)); + box->boxClosing() | rpl::start_with_next([=] { + if (inviteMsgId && !*confirmed) { + _api.request(MTPphone_DeclineConferenceCallInvite( + MTP_int(inviteMsgId) + )).send(); + } + }, box->lifetime()); }); }).fail([=] { _conferenceCallRequestId = 0; diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index fd7056a1e0..33b665a2ba 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -264,7 +264,7 @@ public: PeerId ownerId, const QString &entity, Fn fail = nullptr); - void resolveConferenceCall(const QString &slug); + void resolveConferenceCall(const QString &slug, MsgId inviteMsgId = 0); base::weak_ptr showToast( Ui::Toast::Config &&config); @@ -331,6 +331,7 @@ private: mtpRequestId _collectibleRequestId = 0; QString _conferenceCallSlug; + MsgId _conferenceCallInviteMsgId; mtpRequestId _conferenceCallRequestId = 0; };