PoC confcall invite, emoji.

This commit is contained in:
John Preston 2025-03-28 22:13:53 +05:00
parent dfe6ad3a32
commit 4401ea388c
11 changed files with 238 additions and 42 deletions

View file

@ -124,22 +124,22 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) {
} // namespace } // namespace
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) { std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
if (!call->isKeyShaForFingerprintReady()) {
return {};
}
return ComputeEmojiFingerprint(call->getKeyShaForFingerprint());
}
std::vector<EmojiPtr> ComputeEmojiFingerprint(
bytes::const_span fingerprint) {
auto result = std::vector<EmojiPtr>(); auto result = std::vector<EmojiPtr>();
constexpr auto EmojiCount = (base::array_size(Offsets) - 1); constexpr auto EmojiCount = (base::array_size(Offsets) - 1);
for (auto index = 0; index != EmojiCount; ++index) {
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);
}
if (call->isKeyShaForFingerprintReady()) {
auto sha256 = call->getKeyShaForFingerprint();
constexpr auto kPartSize = 8; constexpr auto kPartSize = 8;
for (auto partOffset = 0; partOffset != sha256.size(); partOffset += kPartSize) { for (auto partOffset = 0
auto value = ComputeEmojiIndex(gsl::make_span(sha256).subspan(partOffset, kPartSize)); ; partOffset != fingerprint.size()
; partOffset += kPartSize) {
auto value = ComputeEmojiIndex(
fingerprint.subspan(partOffset, kPartSize));
auto index = value % EmojiCount; auto index = value % EmojiCount;
auto offset = Offsets[index]; auto offset = Offsets[index];
auto size = Offsets[index + 1] - offset; auto size = Offsets[index + 1] - offset;
@ -150,7 +150,6 @@ std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
Assert(emoji != nullptr); Assert(emoji != nullptr);
result.push_back(emoji); result.push_back(emoji);
} }
}
return result; return result;
} }

View file

@ -19,6 +19,8 @@ class Call;
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint( [[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
not_null<Call*> call); not_null<Call*> call);
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
bytes::const_span fingerprint);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars( [[nodiscard]] object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
not_null<QWidget*> parent, not_null<QWidget*> parent,

View file

@ -1112,6 +1112,10 @@ bool GroupCall::rtmp() const {
return _rtmp; return _rtmp;
} }
bool GroupCall::conference() const {
return _conferenceCall != nullptr;
}
bool GroupCall::listenersHidden() const { bool GroupCall::listenersHidden() const {
return _listenersHidden; return _listenersHidden;
} }
@ -1152,6 +1156,12 @@ rpl::producer<not_null<Data::GroupCall*>> GroupCall::real() const {
return _realChanges.events(); return _realChanges.events();
} }
rpl::producer<QByteArray> GroupCall::emojiHashValue() const {
Expects(_e2e != nullptr);
return _e2e->emojiHashValue();
}
void GroupCall::start(TimeId scheduleDate, bool rtmp) { void GroupCall::start(TimeId scheduleDate, bool rtmp) {
using Flag = MTPphone_CreateGroupCall::Flag; using Flag = MTPphone_CreateGroupCall::Flag;
_createRequestId = _api.request(MTPphone_CreateGroupCall( _createRequestId = _api.request(MTPphone_CreateGroupCall(
@ -1610,8 +1620,13 @@ void GroupCall::requestSubchainBlocks(int subchain, int height) {
MTP_int(kShortPollChainBlocksPerRequest) MTP_int(kShortPollChainBlocksPerRequest)
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
auto &state = _subchains[subchain]; auto &state = _subchains[subchain];
_peer->session().api().applyUpdates(result);
state.requestId = 0; 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); _e2e->subchainBlocksRequestFinished(subchain);
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
auto &state = _subchains[subchain]; auto &state = _subchains[subchain];
@ -2221,11 +2236,26 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallChainBlocks &data) {
return; return;
} }
auto &entry = _subchains[subchain]; auto &entry = _subchains[subchain];
const auto inpoll = entry.requestId != 0; const auto &blocks = data.vblocks().v;
const auto next = data.vnext_offset().v; const auto next = data.vnext_offset().v;
auto now = next - int(data.vblocks().v.size()); if (entry.requestId) {
for (const auto &block : data.vblocks().v) { Assert(!entry.inShortPoll);
_e2e->apply(subchain, now++, { block.v }, inpoll); entry.pending.push_back({ blocks, next });
} else {
applySubChainUpdate(subchain, blocks, next);
}
}
void GroupCall::applySubChainUpdate(
int subchain,
const QVector<MTPbytes> &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<int, not_null<UserData*>> GroupCall::inviteUsers(
} }
const auto owner = &_peer->owner(); const auto owner = &_peer->owner();
if (_conferenceCall) {
for (const auto &user : users) {
_api.request(MTPphone_InviteConferenceCallParticipant(
inputCall(),
user->inputUser
)).send();
}
auto result = std::variant<int, not_null<UserData*>>(0);
if (users.size() != 1) {
result = int(users.size());
} else {
result = users.front();
}
return result;
}
auto count = 0; auto count = 0;
auto slice = QVector<MTPInputUser>(); auto slice = QVector<MTPInputUser>();
auto result = std::variant<int, not_null<UserData*>>(0); auto result = std::variant<int, not_null<UserData*>>(0);

View file

@ -245,6 +245,7 @@ public:
} }
[[nodiscard]] bool scheduleStartSubscribed() const; [[nodiscard]] bool scheduleStartSubscribed() const;
[[nodiscard]] bool rtmp() const; [[nodiscard]] bool rtmp() const;
[[nodiscard]] bool conference() const;
[[nodiscard]] bool listenersHidden() const; [[nodiscard]] bool listenersHidden() const;
[[nodiscard]] bool emptyRtmp() const; [[nodiscard]] bool emptyRtmp() const;
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const; [[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
@ -256,6 +257,7 @@ public:
[[nodiscard]] Data::GroupCall *lookupReal() const; [[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const; [[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
void start(TimeId scheduleDate, bool rtmp); void start(TimeId scheduleDate, bool rtmp);
void hangup(); void hangup();
@ -479,8 +481,14 @@ private:
ssrc = updatedSsrc; ssrc = updatedSsrc;
} }
}; };
struct SubChainPending {
QVector<MTPbytes> blocks;
int next = 0;
};
struct SubChainState { struct SubChainState {
std::vector<SubChainPending> pending;
mtpRequestId requestId = 0; mtpRequestId requestId = 0;
bool inShortPoll = false;
}; };
friend inline constexpr bool is_flag_type(SendUpdateType) { friend inline constexpr bool is_flag_type(SendUpdateType) {
@ -515,6 +523,10 @@ private:
void handleUpdate(const MTPDupdateGroupCall &data); void handleUpdate(const MTPDupdateGroupCall &data);
void handleUpdate(const MTPDupdateGroupCallParticipants &data); void handleUpdate(const MTPDupdateGroupCallParticipants &data);
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data); void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
void applySubChainUpdate(
int subchain,
const QVector<MTPbytes> &blocks,
int next);
bool tryCreateController(); bool tryCreateController();
void destroyController(); void destroyController();
bool tryCreateScreencast(); bool tryCreateScreencast();

View file

@ -56,6 +56,43 @@ namespace {
return result; return result;
} }
class ConfInviteController final : public ContactsBoxController {
public:
ConfInviteController(
not_null<Main::Session*> session,
base::flat_set<not_null<UserData*>> alreadyIn,
Fn<void(not_null<PeerData*>)> choose)
: ContactsBoxController(session)
, _alreadyIn(std::move(alreadyIn))
, _choose(std::move(choose)) {
}
protected:
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> 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<PeerListRow*> row) override {
_choose(row->peer());
}
private:
const base::flat_set<not_null<UserData*>> _alreadyIn;
const Fn<void(not_null<PeerData*>)> _choose;
};
} // namespace } // namespace
InviteController::InviteController( InviteController::InviteController(
@ -173,6 +210,7 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
return nullptr; return nullptr;
} }
const auto peer = call->peer(); const auto peer = call->peer();
const auto weak = base::make_weak(call);
auto alreadyIn = peer->owner().invitedToCallUsers(real->id()); auto alreadyIn = peer->owner().invitedToCallUsers(real->id());
for (const auto &participant : real->participants()) { for (const auto &participant : real->participants()) {
if (const auto user = participant.peer->asUser()) { if (const auto user = participant.peer->asUser()) {
@ -180,6 +218,39 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
} }
} }
alreadyIn.emplace(peer->session().user()); alreadyIn.emplace(peer->session().user());
if (call->conference()) {
const auto close = std::make_shared<Fn<void()>>();
const auto invite = [=](not_null<PeerData*> 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<ConfInviteController>(
&real->session(),
alreadyIn,
invite);
controller->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto initBox = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_group_call_invite_title());
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
*close = [=] { box->closeBox(); };
};
return Box<PeerListBox>(std::move(controller), initBox);
}
auto controller = std::make_unique<InviteController>(peer, alreadyIn); auto controller = std::make_unique<InviteController>(peer, alreadyIn);
controller->setStyleOverrides( controller->setStyleOverrides(
&st::groupCallInviteMembersList, &st::groupCallInviteMembersList,
@ -194,7 +265,6 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
&st::groupCallInviteMembersList, &st::groupCallInviteMembersList,
&st::groupCallMultiSelect); &st::groupCallMultiSelect);
const auto weak = base::make_weak(call);
const auto invite = [=](const std::vector<not_null<UserData*>> &users) { const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
const auto call = weak.get(); const auto call = weak.get();
if (!call) { if (!call) {

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_group_invite_controller.h" #include "calls/group/calls_group_invite_controller.h"
#include "calls/group/ui/calls_group_scheduled_labels.h" #include "calls/group/ui/calls_group_scheduled_labels.h"
#include "calls/group/ui/desktop_capture_choose_source.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_window_title.h"
#include "ui/platform/ui_platform_utility.h" #include "ui/platform/ui_platform_utility.h"
#include "ui/controls/call_mute_button.h" #include "ui/controls/call_mute_button.h"
@ -76,6 +77,19 @@ 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 ");
for (const auto &single : emoji) {
result += single->text();
}
}
return result;
}
class Show final : public Main::SessionShow { class Show final : public Main::SessionShow {
public: public:
explicit Show(not_null<Panel*> panel); explicit Show(not_null<Panel*> panel);
@ -367,7 +381,13 @@ void Panel::initWindow() {
window()->setAttribute(Qt::WA_NoSystemBackground); window()->setAttribute(Qt::WA_NoSystemBackground);
window()->setTitleStyle(st::groupCallTitle); window()->setTitleStyle(st::groupCallTitle);
if (_call->conference()) {
titleText() | rpl::start_with_next([=](const QString &text) {
window()->setTitle(text);
}, lifetime());
} else {
subscribeToPeerChanges(); subscribeToPeerChanges();
}
base::install_event_filter(window().get(), [=](not_null<QEvent*> e) { base::install_event_filter(window().get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close && handleClose()) { if (e->type() == QEvent::Close && handleClose()) {
@ -2445,9 +2465,11 @@ void Panel::updateMembersGeometry() {
} }
} }
void Panel::refreshTitle() { rpl::producer<QString> Panel::titleText() {
if (!_title) { if (_call->conference()) {
auto text = rpl::combine( return _call->emojiHashValue() | rpl::map(ComposeTitle);
}
return rpl::combine(
Info::Profile::NameValue(_peer), Info::Profile::NameValue(_peer),
rpl::single( rpl::single(
QString() QString()
@ -2457,7 +2479,12 @@ void Panel::refreshTitle() {
}) | rpl::flatten_latest()) }) | rpl::flatten_latest())
) | rpl::map([=](const QString &name, const QString &title) { ) | rpl::map([=](const QString &name, const QString &title) {
return title.isEmpty() ? name : title; return title.isEmpty() ? name : title;
}) | rpl::after_next([=] { });
}
void Panel::refreshTitle() {
if (!_title) {
auto text = titleText() | rpl::after_next([=] {
refreshTitleGeometry(); refreshTitleGeometry();
}); });
_title.create( _title.create(

View file

@ -155,6 +155,7 @@ private:
void setupMembers(); void setupMembers();
void setupVideo(not_null<Viewport*> viewport); void setupVideo(not_null<Viewport*> viewport);
void setupRealMuteButtonState(not_null<Data::GroupCall*> real); void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
[[nodiscard]] rpl::producer<QString> titleText();
bool handleClose(); bool handleClose();
void startScheduledNow(); void startScheduledNow();

View file

@ -52,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h" #include "mainwindow.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "window/notifications_manager.h" #include "window/notifications_manager.h"
#include "window/window_session_controller.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
#include "spellcheck/spellcheck_types.h" #include "spellcheck/spellcheck_types.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
@ -1225,6 +1226,12 @@ void History::applyServiceChanges(
topic->setHidden(mtpIsTrue(*hidden)); 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 &) { }, [](const auto &) {
}); });
} }

View file

@ -5634,6 +5634,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
: duration : duration
? tr::lng_action_confcall_finished(tr::now) ? tr::lng_action_confcall_finished(tr::now)
: tr::lng_action_confcall_invitation(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<LambdaClickHandler>([=](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto weak = my.sessionWindow;
if (const auto strong = weak.get()) {
strong->resolveConferenceCall(slug, id);
}
}));
return result; return result;
}; };

View file

@ -840,12 +840,16 @@ void SessionNavigation::resolveCollectible(
}).send(); }).send();
} }
void SessionNavigation::resolveConferenceCall(const QString &slug) { void SessionNavigation::resolveConferenceCall(
if (_conferenceCallSlug == slug) { const QString &slug,
MsgId inviteMsgId) {
if (_conferenceCallSlug == slug
&& _conferenceCallInviteMsgId == inviteMsgId) {
return; return;
} }
_api.request(base::take(_conferenceCallRequestId)).cancel(); _api.request(base::take(_conferenceCallRequestId)).cancel();
_conferenceCallSlug = slug; _conferenceCallSlug = slug;
_conferenceCallInviteMsgId = inviteMsgId;
const auto limit = 5; const auto limit = 5;
_conferenceCallRequestId = _api.request(MTPphone_GetGroupCall( _conferenceCallRequestId = _api.request(MTPphone_GetGroupCall(
@ -864,15 +868,26 @@ void SessionNavigation::resolveConferenceCall(const QString &slug) {
false, // rtmp false, // rtmp
true); // conference true); // conference
call->processFullCall(result); call->processFullCall(result);
const auto confirmed = std::make_shared<bool>();
const auto join = [=] { const auto join = [=] {
Core::App().calls().startOrJoinConferenceCall( *confirmed = true;
uiShow(), Core::App().calls().startOrJoinConferenceCall(uiShow(), {
{ .call = call, .linkSlug = slug }); .call = call,
.linkSlug = slug,
.joinMessageId = inviteMsgId,
});
}; };
uiShow()->show(Box( const auto box = uiShow()->show(Box(
Calls::Group::ConferenceCallJoinConfirm, Calls::Group::ConferenceCallJoinConfirm,
call, call,
join)); join));
box->boxClosing() | rpl::start_with_next([=] {
if (inviteMsgId && !*confirmed) {
_api.request(MTPphone_DeclineConferenceCallInvite(
MTP_int(inviteMsgId)
)).send();
}
}, box->lifetime());
}); });
}).fail([=] { }).fail([=] {
_conferenceCallRequestId = 0; _conferenceCallRequestId = 0;

View file

@ -264,7 +264,7 @@ public:
PeerId ownerId, PeerId ownerId,
const QString &entity, const QString &entity,
Fn<void(QString)> fail = nullptr); Fn<void(QString)> fail = nullptr);
void resolveConferenceCall(const QString &slug); void resolveConferenceCall(const QString &slug, MsgId inviteMsgId = 0);
base::weak_ptr<Ui::Toast::Instance> showToast( base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config); Ui::Toast::Config &&config);
@ -331,6 +331,7 @@ private:
mtpRequestId _collectibleRequestId = 0; mtpRequestId _collectibleRequestId = 0;
QString _conferenceCallSlug; QString _conferenceCallSlug;
MsgId _conferenceCallInviteMsgId;
mtpRequestId _conferenceCallRequestId = 0; mtpRequestId _conferenceCallRequestId = 0;
}; };