From 3a5b625d64dc99375854b2b1aa5c64ec75844296 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 28 Nov 2020 21:17:13 +0300 Subject: [PATCH] Allow inviting members to the group call. --- Telegram/Resources/langs/lang.strings | 10 + .../boxes/peers/edit_participants_box.cpp | 49 +++-- .../boxes/peers/edit_participants_box.h | 14 +- Telegram/SourceFiles/calls/calls.style | 2 +- .../SourceFiles/calls/calls_group_call.cpp | 59 +++++ Telegram/SourceFiles/calls/calls_group_call.h | 2 + .../SourceFiles/calls/calls_group_members.cpp | 27 +-- .../SourceFiles/calls/calls_group_members.h | 7 +- .../SourceFiles/calls/calls_group_panel.cpp | 204 +++++++++++++++++- .../SourceFiles/calls/calls_group_panel.h | 2 +- Telegram/SourceFiles/data/data_group_call.cpp | 5 +- Telegram/SourceFiles/data/data_session.cpp | 36 ++++ Telegram/SourceFiles/data/data_session.h | 9 + .../export/data/export_data_types.cpp | 13 +- .../export/data/export_data_types.h | 12 +- .../export/output/export_output_html.cpp | 16 +- .../export/output/export_output_json.cpp | 10 + .../SourceFiles/history/history_service.cpp | 49 ++++- 18 files changed, 478 insertions(+), 48 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index acb95429d..339f79bbb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1062,6 +1062,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_duration_minsec_seconds#other" = "{count} sec"; "lng_duration_minutes_seconds" = "{minutes_count} {seconds_count}"; +"lng_action_invite_user" = "{from} invited {user} to the voice chat"; +"lng_action_invite_users_many" = "{from} invited {users} to the voice chat"; +"lng_action_invite_users_and_one" = "{accumulated}, {user}"; +"lng_action_invite_users_and_last" = "{accumulated} and {user}"; +"lng_action_group_call" = "Group call"; "lng_action_add_user" = "{from} added {user}"; "lng_action_add_users_many" = "{from} added {users}"; "lng_action_add_users_and_one" = "{accumulated}, {user}"; @@ -1824,12 +1829,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_leave_sure" = "Are you sure you want to leave this voice chat?"; "lng_group_call_also_end" = "End voice chat"; "lng_group_call_settings_title" = "Settings"; +"lng_group_call_invite_title" = "Invite members"; +"lng_group_call_invite_button" = "Invite"; "lng_group_call_new_muted" = "Mute new members"; "lng_group_call_speakers" = "Speakers"; "lng_group_call_microphone" = "Microphone"; "lng_group_call_share" = "Share Invite Link"; "lng_group_call_end" = "End Voice Chat"; "lng_group_call_join" = "Join"; +"lng_group_call_invite_done_user" = "You invited {user} to the voice chat."; +"lng_group_call_invite_done_many#one" = "You invited **{count} member** to the voice chat."; +"lng_group_call_invite_done_many#other" = "You invited **{count} members** to the voice chat."; "lng_no_mic_permission" = "Telegram needs access to your microphone so that you can make calls and record voice messages."; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 3cc3aca22..0d866296f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -758,6 +758,14 @@ ParticipantsBoxController::ParticipantsBoxController( not_null navigation, not_null peer, Role role) +: ParticipantsBoxController(CreateTag(), navigation, peer, role) { +} + +ParticipantsBoxController::ParticipantsBoxController( + CreateTag, + Window::SessionNavigation *navigation, + not_null peer, + Role role) : PeerListController(CreateSearchController(peer, role, &_additional)) , _navigation(navigation) , _peer(peer) @@ -795,8 +803,8 @@ void ParticipantsBoxController::setupListChangeViewers() { delegate()->peerListPartitionRows([&](const PeerListRow &row) { return (row.peer() == user); }); - } else { - delegate()->peerListPrependRow(createRow(user)); + } else if (auto row = createRow(user)) { + delegate()->peerListPrependRow(std::move(row)); delegate()->peerListRefreshRows(); if (_onlineSorter) { _onlineSorter->sort(); @@ -929,6 +937,8 @@ void ParticipantsBoxController::addNewItem() { } void ParticipantsBoxController::addNewParticipants() { + Expects(_navigation != nullptr); + const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); if (chat) { @@ -1386,6 +1396,7 @@ void ParticipantsBoxController::rowClicked(not_null row) { && (_peer->isChat() || _peer->isMegagroup())) { showRestricted(user); } else { + Assert(_navigation != nullptr); _navigation->showPeerInfo(user); } } @@ -1413,9 +1424,11 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( const auto channel = _peer->asChannel(); const auto user = row->peer()->asUser(); auto result = base::make_unique_q(parent); - result->addAction( - tr::lng_context_view_profile(tr::now), - crl::guard(this, [=] { _navigation->showPeerInfo(user); })); + if (_navigation) { + result->addAction( + tr::lng_context_view_profile(tr::now), + crl::guard(this, [=] { _navigation->showPeerInfo(user); })); + } if (_role == Role::Kicked) { if (_peer->isMegagroup() && _additional.canRestrictUser(user)) { @@ -1735,16 +1748,18 @@ bool ParticipantsBoxController::appendRow(not_null user) { if (delegate()->peerListFindRow(user->id)) { recomputeTypeFor(user); return false; + } else if (auto row = createRow(user)) { + delegate()->peerListAppendRow(std::move(row)); + if (_role != Role::Kicked) { + setDescriptionText(QString()); + } + return true; } - delegate()->peerListAppendRow(createRow(user)); - if (_role != Role::Kicked) { - setDescriptionText(QString()); - } - return true; + return false; } bool ParticipantsBoxController::prependRow(not_null user) { - if (auto row = delegate()->peerListFindRow(user->id)) { + if (const auto row = delegate()->peerListFindRow(user->id)) { recomputeTypeFor(user); refreshCustomStatus(row); if (_role == Role::Admins) { @@ -1752,12 +1767,14 @@ bool ParticipantsBoxController::prependRow(not_null user) { delegate()->peerListPrependRowFromSearchResult(row); } return false; + } else if (auto row = createRow(user)) { + delegate()->peerListPrependRow(std::move(row)); + if (_role != Role::Kicked) { + setDescriptionText(QString()); + } + return true; } - delegate()->peerListPrependRow(createRow(user)); - if (_role != Role::Kicked) { - setDescriptionText(QString()); - } - return true; + return false; } bool ParticipantsBoxController::removeRow(not_null user) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 0c3264b35..dfcb1ae6d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -170,6 +170,16 @@ public: rpl::producer onlineCountValue() const override; protected: + // Allow child controllers not providing navigation. + // This is their responsibility to override all methods that use it. + struct CreateTag { + }; + ParticipantsBoxController( + CreateTag, + Window::SessionNavigation *navigation, + not_null peer, + Role role); + virtual std::unique_ptr createRow( not_null user) const; @@ -236,7 +246,9 @@ private: void subscribeToCreatorChange(not_null channel); void fullListRefresh(); - not_null _navigation; + // It may be nullptr in subclasses of this controller. + Window::SessionNavigation *_navigation = nullptr; + not_null _peer; MTP::Sender _api; Role _role = Role::Admins; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index e3ad82bb6..afb4668f8 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -425,7 +425,7 @@ groupCallMembersList: PeerList(defaultPeerList) { height: 52px; photoPosition: point(12px, 6px); namePosition: point(68px, 7px); - statusPosition: point(68px, 27px); + statusPosition: point(68px, 26px); photoSize: 40px; nameFg: groupCallMembersFg; nameFgChecked: groupCallMembersFg; diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index 8ebefbb53..0952f814b 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_group_call.h" +#include "data/data_session.h" #include @@ -30,6 +31,11 @@ class GroupInstanceImpl; } // namespace tgcalls namespace Calls { +namespace { + +constexpr auto kMaxInvitePerSlice = 10; + +} // namespace GroupCall::GroupCall( not_null delegate, @@ -505,6 +511,9 @@ void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { } void GroupCall::toggleMute(not_null user, bool mute) { + if (!_id) { + return; + } _api.request(MTPphone_EditGroupCallMember( MTP_flags(mute ? MTPphone_EditGroupCallMember::Flag::f_muted @@ -523,6 +532,56 @@ void GroupCall::toggleMute(not_null user, bool mute) { }).send(); } +std::variant> GroupCall::inviteUsers( + const std::vector> &users) { + const auto real = _channel->call(); + if (!real || real->id() != _id) { + return 0; + } + const auto owner = &_channel->owner(); + const auto &invited = owner->invitedToCallUsers(_id); + const auto &participants = real->participants(); + auto &&toInvite = users | ranges::view::filter([&]( + not_null user) { + return !invited.contains(user) && !ranges::contains( + participants, + user, + &Data::GroupCall::Participant::user); + }); + + auto count = 0; + auto slice = QVector(); + auto result = std::variant>(0); + slice.reserve(kMaxInvitePerSlice); + const auto sendSlice = [&] { + count += slice.size(); + _api.request(MTPphone_InviteToGroupCall( + inputCall(), + MTP_vector(slice) + )).done([=](const MTPUpdates &result) { + _channel->session().api().applyUpdates(result); + }).send(); + slice.clear(); + }; + for (const auto user : users) { + if (!count && slice.empty()) { + result = user; + } + owner->registerInvitedToCallUser(_id, _channel, user); + slice.push_back(user->inputUser); + if (slice.size() == kMaxInvitePerSlice) { + sendSlice(); + } + } + if (count != 0 || slice.size() != 1) { + result = int(count + slice.size()); + } + if (!slice.empty()) { + sendSlice(); + } + return result; +} + //void GroupCall::setAudioVolume(bool input, float level) { // if (_instance) { // if (input) { diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h index 593c15e43..73a66149f 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.h +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -96,6 +96,8 @@ public: void setAudioDuckingEnabled(bool enabled); void toggleMute(not_null user, bool mute); + std::variant> inviteUsers( + const std::vector> &users); [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; diff --git a/Telegram/SourceFiles/calls/calls_group_members.cpp b/Telegram/SourceFiles/calls/calls_group_members.cpp index fb0cfe011..1a401d155 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/calls_group_members.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_changes.h" #include "data/data_group_call.h" +#include "data/data_peer_values.h" // Data::CanWriteValue. #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" @@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "main/main_session.h" #include "base/timer.h" +#include "boxes/peers/edit_participants_box.h" #include "lang/lang_keys.h" #include "facades.h" // Ui::showPeerHistory. #include "mainwindow.h" // App::wnd()->activate. @@ -613,7 +615,7 @@ auto GroupMembers::toggleMuteRequests() const int GroupMembers::desiredHeight() const { auto desired = _header ? _header->height() : 0; - auto count = [this] { + auto count = [&] { if (const auto call = _call.get()) { if (const auto real = call->channel()->call()) { if (call->id() == real->id()) { @@ -623,9 +625,10 @@ int GroupMembers::desiredHeight() const { } return 0; }(); - desired += std::max(count, _list->fullRowsCount()) - * st::groupCallMembersList.item.height; - return desired; + const auto use = std::max(count, _list->fullRowsCount()); + return (_header ? _header->height() : 0) + + (use * st::groupCallMembersList.item.height) + + (use ? st::lineWidth : 0); } rpl::producer GroupMembers::desiredHeightValue() const { @@ -650,7 +653,7 @@ void GroupMembers::setupHeader(not_null call) { _addMember = Ui::CreateChild( parent, st::groupCallAddMember); - setupButtons(); + setupButtons(call); widthValue( ) | rpl::start_with_next([this](int width) { @@ -674,12 +677,14 @@ object_ptr GroupMembers::setupTitle( return result; } -void GroupMembers::setupButtons() { +void GroupMembers::setupButtons(not_null call) { using namespace rpl::mappers; - _addMember->showOn(rpl::single(true)); + _addMember->showOn(Data::CanWriteValue( + call->channel() + )); _addMember->addClickHandler([=] { // TODO throttle(ripple duration) - addMember(); + _addMemberRequests.fire({}); }); } @@ -699,7 +704,7 @@ void GroupMembers::setupList() { _list->heightValue( ) | rpl::start_with_next([=](int listHeight) { auto newHeight = (listHeight > 0) - ? (topSkip + listHeight) + ? (topSkip + listHeight + st::lineWidth) : 0; resize(width(), newHeight); }, _list->lifetime()); @@ -737,10 +742,6 @@ void GroupMembers::updateHeaderControlsGeometry(int newWidth) { _title->moveToLeft(0, 0); } -void GroupMembers::addMember() { - // #TODO calls -} - void GroupMembers::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { diff --git a/Telegram/SourceFiles/calls/calls_group_members.h b/Telegram/SourceFiles/calls/calls_group_members.h index f5481a486..1e02d6a76 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.h +++ b/Telegram/SourceFiles/calls/calls_group_members.h @@ -37,6 +37,9 @@ public: [[nodiscard]] int desiredHeight() const; [[nodiscard]] rpl::producer desiredHeightValue() const override; [[nodiscard]] rpl::producer toggleMuteRequests() const; + [[nodiscard]] rpl::producer<> addMembersRequests() const { + return _addMemberRequests.events(); + } private: using ListWidget = PeerListContent; @@ -66,9 +69,8 @@ private: object_ptr setupTitle(not_null call); void setupList(); - void setupButtons(); + void setupButtons(not_null call); - void addMember(); void updateHeaderControlsGeometry(int newWidth); const base::weak_ptr _call; @@ -76,6 +78,7 @@ private: std::unique_ptr _listController; object_ptr _header = { nullptr }; ListWidget *_list = { nullptr }; + rpl::event_stream<> _addMemberRequests; Ui::RpWidget *_titleWrap = nullptr; Ui::FlatLabel *_title = nullptr; diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp index d5d35d882..143799ba5 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -16,10 +16,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/layers/layer_manager.h" #include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "core/application.h" #include "lang/lang_keys.h" #include "data/data_channel.h" +#include "data/data_user.h" +#include "data/data_group_call.h" +#include "data/data_session.h" +#include "main/main_session.h" #include "base/event_filter.h" +#include "boxes/peers/edit_participants_box.h" #include "app.h" #include "styles/style_calls.h" #include "styles/style_layers.h" @@ -33,6 +40,132 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace Calls { +namespace { + +class InviteController final : public ParticipantsBoxController { +public: + InviteController( + not_null channel, + base::flat_set> alreadyIn, + int fullInCount); + + void prepare() override; + + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + void itemDeselectedHook(not_null peer) override; + + std::variant> inviteSelectedUsers( + not_null box, + not_null call) const; + +private: + void updateTitle() const; + [[nodiscard]] int alreadyInCount() const; + [[nodiscard]] bool isAlreadyIn(not_null user) const; + [[nodiscard]] int fullCount() const; + + std::unique_ptr createRow( + not_null user) const override; + + const not_null _channel; + const base::flat_set> _alreadyIn; + const int _fullInCount = 0; + mutable base::flat_set> _skippedUsers; + +}; + +InviteController::InviteController( + not_null channel, + base::flat_set> alreadyIn, + int fullInCount) +: ParticipantsBoxController(CreateTag{}, nullptr, channel, Role::Members) +, _channel(channel) +, _alreadyIn(std::move(alreadyIn)) +, _fullInCount(std::max(fullInCount, int(_alreadyIn.size()))) { + _skippedUsers.emplace(channel->session().user()); +} + +void InviteController::prepare() { + ParticipantsBoxController::prepare(); + updateTitle(); +} + +void InviteController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked(row, !row->checked()); + updateTitle(); +} + +base::unique_qptr InviteController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +void InviteController::itemDeselectedHook(not_null peer) { + updateTitle(); +} + +int InviteController::alreadyInCount() const { + return std::max(_fullInCount, int(_alreadyIn.size())); +} + +bool InviteController::isAlreadyIn(not_null user) const { + return _alreadyIn.contains(user); +} + +int InviteController::fullCount() const { + return alreadyInCount() + delegate()->peerListSelectedRowsCount(); +} + +std::unique_ptr InviteController::createRow( + not_null user) const { + if (user->isSelf() || user->isBot()) { + if (_skippedUsers.emplace(user).second) { + updateTitle(); + } + return nullptr; + } + auto result = std::make_unique(user); + if (isAlreadyIn(user)) { + result->setDisabledState(PeerListRow::State::DisabledChecked); + } + return result; +} + +void InviteController::updateTitle() const { + const auto inOrInvited = fullCount() - 1; // minus self + const auto canBeInvited = std::max({ + delegate()->peerListFullRowsCount(), // minus self and bots + _channel->membersCount() - int(_skippedUsers.size()), // self + bots + inOrInvited + }); + const auto additional = canBeInvited + ? qsl("%1 / %2").arg(inOrInvited).arg(canBeInvited) + : QString(); + delegate()->peerListSetTitle(tr::lng_group_call_invite_title()); + delegate()->peerListSetAdditionalTitle(rpl::single(additional)); +} + +std::variant> InviteController::inviteSelectedUsers( + not_null box, + not_null call) const { + const auto rows = box->peerListCollectSelectedRows(); + const auto users = ranges::view::all( + rows + ) | ranges::view::transform([](not_null peer) { + Expects(peer->isUser()); + Expects(!peer->isSelf()); + + return not_null(peer->asUser()); + }) | ranges::to_vector; + return call->inviteUsers(users); +} + +} // namespace void LeaveGroupCallBox( not_null box, @@ -159,13 +292,13 @@ void GroupPanel::initWidget() { widget()->sizeValue( ) | rpl::skip(1) | rpl::start_with_next([=] { updateControlsGeometry(); + + // title geometry depends on _controls->geometry, + // which is not updated here yet. + crl::on_main(widget(), [=] { refreshTitle(); }); }, widget()->lifetime()); } -void GroupPanel::copyShareLink() { - -} - void GroupPanel::hangup(bool discardCallChecked) { if (!_call) { return; @@ -230,6 +363,13 @@ void GroupPanel::initWithCall(GroupCall *call) { } }, _callLifetime); + _members->addMembersRequests( + ) | rpl::start_with_next([=] { + if (_call) { + addMembers(); + } + }, _callLifetime); + using namespace rpl::mappers; rpl::combine( _call->mutedValue(), @@ -258,6 +398,62 @@ void GroupPanel::initWithCall(GroupCall *call) { }, _callLifetime); } +void GroupPanel::addMembers() { + const auto real = _channel->call(); + if (!_call || !real || real->id() != _call->id()) { + return; + } + auto alreadyIn = _channel->owner().invitedToCallUsers(real->id()); + for (const auto &participant : real->participants()) { + alreadyIn.emplace(participant.user); + } + alreadyIn.emplace(_channel->session().user()); + auto controller = std::make_unique( + _channel, + std::move(alreadyIn), + real->fullCount()); + const auto weak = base::make_weak(_call); + auto initBox = [=, controller = controller.get()]( + not_null box) { + box->addButton(tr::lng_group_call_invite_button(), [=] { + if (const auto call = weak.get()) { + const auto result = controller->inviteSelectedUsers(box, call); + + if (const auto user = std::get_if>(&result)) { + Ui::Toast::Show( + widget(), + Ui::Toast::Config{ + .text = tr::lng_group_call_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold((*user)->firstName), + Ui::Text::WithEntities), + .st = &st::defaultToast, + }); + } else if (const auto count = std::get_if(&result)) { + if (*count > 0) { + Ui::Toast::Show( + widget(), + Ui::Toast::Config{ + .text = tr::lng_group_call_invite_done_many( + tr::now, + lt_count, + *count, + Ui::Text::RichLangValue), + .st = &st::defaultToast, + }); + } + } else { + Unexpected("Result in GroupCall::inviteUsers."); + } + } + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + _layerBg->showBox(Box(std::move(controller), initBox)); +} + void GroupPanel::initLayout() { initGeometry(); diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h index 6270b2454..691893d55 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -89,9 +89,9 @@ private: void updateControlsGeometry(); void showControls(); - void copyShareLink(); void hangup(bool discardCallChecked); + void addMembers(); [[nodiscard]] int computeMembersListTop() const; [[nodiscard]] std::optional computeTitleRect() const; void refreshTitle(); diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 7d4601e5f..75ca74ee0 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -208,13 +208,14 @@ void GroupCall::applyParticipantsSlice( .canSelfUnmute = !data.is_muted() || data.is_can_self_unmute(), }; if (i == end(_participants)) { - _userBySource.emplace(value.source, value.user); + _userBySource.emplace(value.source, user); _participants.push_back(value); + _channel->owner().unregisterInvitedToCallUser(_id, user); ++fullCount; } else { if (i->source != value.source) { _userBySource.erase(i->source); - _userBySource.emplace(value.source, value.user); + _userBySource.emplace(value.source, user); } *i = value; } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c0b5cedf6..833aae59d 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -805,6 +805,42 @@ GroupCall *Session::groupCall(uint64 callId) const { return (i != end(_groupCalls)) ? i->second.get() : nullptr; } +auto Session::invitedToCallUsers(uint64 callId) const +-> const base::flat_set> & { + static const base::flat_set> kEmpty; + const auto i = _invitedToCallUsers.find(callId); + return (i != _invitedToCallUsers.end()) ? i->second : kEmpty; +} + +void Session::registerInvitedToCallUser( + uint64 callId, + not_null channel, + not_null user) { + const auto call = channel->call(); + if (call && call->id() == callId) { + const auto inCall = ranges::contains( + call->participants(), + user, + &Data::GroupCall::Participant::user); + if (inCall) { + return; + } + } + _invitedToCallUsers[callId].emplace(user); +} + +void Session::unregisterInvitedToCallUser( + uint64 callId, + not_null user) { + const auto i = _invitedToCallUsers.find(callId); + if (i != _invitedToCallUsers.end()) { + i->second.remove(user); + if (i->second.empty()) { + _invitedToCallUsers.erase(i); + } + } +} + PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); for (const auto &[peerId, peer] : _peers) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index fc2fd3b46..b61cce387 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -158,6 +158,14 @@ public: void unregisterGroupCall(not_null call); GroupCall *groupCall(uint64 callId) const; + [[nodiscard]] auto invitedToCallUsers(uint64 callId) const + -> const base::flat_set> &; + void registerInvitedToCallUser( + uint64 callId, + not_null channel, + not_null user); + void unregisterInvitedToCallUser(uint64 callId, not_null user); + void enumerateUsers(Fn)> action) const; void enumerateGroups(Fn)> action) const; void enumerateChannels(Fn)> action) const; @@ -912,6 +920,7 @@ private: base::flat_set> _heavyViewParts; base::flat_map> _groupCalls; + base::flat_map>> _invitedToCallUsers; History *_topPromoted = nullptr; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 97d917f21..c4c30d8cc 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1113,9 +1113,18 @@ ServiceAction ParseServiceAction( content.distance = data.vdistance().v; result.content = content; }, [&](const MTPDmessageActionGroupCall &data) { - // #TODO calls + auto content = ActionGroupCall(); + if (const auto duration = data.vduration()) { + content.duration = duration->v; + } + result.content = content; }, [&](const MTPDmessageActionInviteToGroupCall &data) { - // #TODO calls + auto content = ActionInviteToGroupCall(); + content.userIds.reserve(data.vusers().v.size()); + for (const auto &user : data.vusers().v) { + content.userIds.push_back(user.v); + } + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 8454d58c5..ff169de80 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -458,6 +458,14 @@ struct ActionGeoProximityReached { bool toSelf = false; }; +struct ActionGroupCall { + int duration = 0; +}; + +struct ActionInviteToGroupCall { + std::vector userIds; +}; + struct ServiceAction { std::variant< v::null_t, @@ -482,7 +490,9 @@ struct ServiceAction { ActionSecureValuesSent, ActionContactSignUp, ActionPhoneNumberRequest, - ActionGeoProximityReached> content; + ActionGeoProximityReached, + ActionGroupCall, + ActionInviteToGroupCall> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 6684724f9..0f62a466e 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1110,10 +1110,24 @@ auto HtmlWriter::Wrap::pushMessage( } else if (data.toSelf) { return fromName + " is now within " + distance + " from you"; } else { - return fromName + " is now within " + distance + " from " + toName; + return fromName + + " is now within " + + distance + + " from " + + toName; } }, [&](const ActionPhoneNumberRequest &data) { return serviceFrom + " requested your phone number"; + }, [&](const ActionGroupCall &data) { + return "Group call" + + (data.duration + ? (" (" + QString::number(data.duration) + " seconds)") + : QString()).toUtf8(); + }, [&](const ActionInviteToGroupCall &data) { + return serviceFrom + + " invited " + + peers.wrapUserNames(data.userIds) + + " to the voice chat"; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index f079b017d..4a36652eb 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -487,6 +487,16 @@ QByteArray SerializeMessage( }, [&](const ActionPhoneNumberRequest &data) { pushActor(); pushAction("requested_phone_number"); + }, [&](const ActionGroupCall &data) { + pushActor(); + pushAction("group_call"); + if (data.duration) { + push("duration", data.duration); + } + }, [&](const ActionInviteToGroupCall &data) { + pushActor(); + pushAction("invite_to_group_call"); + pushUserNames(data.userIds); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 6dfafdc58..74ee7373a 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -274,6 +274,49 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareInviteToGroupCall = [this](const MTPDmessageActionInviteToGroupCall &action) { + const auto channel = history()->peer->asChannel(); + const auto callId = action.vcall().match([&](const MTPDinputGroupCall &data) { + return data.vid().v; + }); + const auto owner = &history()->owner(); + const auto registerUser = [&](UserId userId) { + const auto user = owner->user(userId); + if (channel && callId) { + owner->registerInvitedToCallUser(callId, channel, user); + } + return user; + }; + auto result = PreparedText{}; + auto &users = action.vusers().v; + if (users.size() == 1) { + auto user = registerUser(users[0].v); + result.links.push_back(fromLink()); + result.links.push_back(user->createOpenLink()); + result.text = tr::lng_action_invite_user(tr::now, lt_from, fromLinkText(), lt_user, textcmdLink(2, user->name)); + } else if (users.isEmpty()) { + result.links.push_back(fromLink()); + result.text = tr::lng_action_invite_user(tr::now, lt_from, fromLinkText(), lt_user, qsl("somebody")); + } else { + result.links.push_back(fromLink()); + for (auto i = 0, l = users.size(); i != l; ++i) { + auto user = registerUser(users[i].v); + result.links.push_back(user->createOpenLink()); + + auto linkText = textcmdLink(i + 2, user->name); + if (i == 0) { + result.text = linkText; + } else if (i + 1 == l) { + result.text = tr::lng_action_invite_users_and_last(tr::now, lt_accumulated, result.text, lt_user, linkText); + } else { + result.text = tr::lng_action_invite_users_and_one(tr::now, lt_accumulated, result.text, lt_user, linkText); + } + } + result.text = tr::lng_action_invite_users_many(tr::now, lt_from, fromLinkText(), lt_users, result.text); + } + return result; + }; + const auto messageText = action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); @@ -324,11 +367,9 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { LOG(("API Error: messageActionSecureValuesSentMe received.")); return PreparedText{ tr::lng_message_empty(tr::now) }; }, [&](const MTPDmessageActionGroupCall &data) { - // #TODO calls - return PreparedText{ "Group call" }; + return PreparedText{ tr::lng_action_group_call(tr::now) }; }, [&](const MTPDmessageActionInviteToGroupCall &data) { - // #TODO calls - return PreparedText{ "Invite to group call" }; + return prepareInviteToGroupCall(data); }, [](const MTPDmessageActionEmpty &) { return PreparedText{ tr::lng_message_empty(tr::now) }; });