Allow inviting members to the group call.

This commit is contained in:
John Preston 2020-11-28 21:17:13 +03:00
parent 9e5006dd67
commit 3a5b625d64
18 changed files with 478 additions and 48 deletions

View file

@ -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.";

View file

@ -758,6 +758,14 @@ ParticipantsBoxController::ParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Role role)
: ParticipantsBoxController(CreateTag(), navigation, peer, role) {
}
ParticipantsBoxController::ParticipantsBoxController(
CreateTag,
Window::SessionNavigation *navigation,
not_null<PeerData*> 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<PeerListRow*> row) {
&& (_peer->isChat() || _peer->isMegagroup())) {
showRestricted(user);
} else {
Assert(_navigation != nullptr);
_navigation->showPeerInfo(user);
}
}
@ -1413,9 +1424,11 @@ base::unique_qptr<Ui::PopupMenu> ParticipantsBoxController::rowContextMenu(
const auto channel = _peer->asChannel();
const auto user = row->peer()->asUser();
auto result = base::make_unique_q<Ui::PopupMenu>(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<UserData*> 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<UserData*> 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<UserData*> 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<UserData*> user) {

View file

@ -170,6 +170,16 @@ public:
rpl::producer<int> 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<PeerData*> peer,
Role role);
virtual std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const;
@ -236,7 +246,9 @@ private:
void subscribeToCreatorChange(not_null<ChannelData*> channel);
void fullListRefresh();
not_null<Window::SessionNavigation*> _navigation;
// It may be nullptr in subclasses of this controller.
Window::SessionNavigation *_navigation = nullptr;
not_null<PeerData*> _peer;
MTP::Sender _api;
Role _role = Role::Admins;

View file

@ -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;

View file

@ -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 <tgcalls/group/GroupInstanceImpl.h>
@ -30,6 +31,11 @@ class GroupInstanceImpl;
} // namespace tgcalls
namespace Calls {
namespace {
constexpr auto kMaxInvitePerSlice = 10;
} // namespace
GroupCall::GroupCall(
not_null<Delegate*> delegate,
@ -505,6 +511,9 @@ void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) {
}
void GroupCall::toggleMute(not_null<UserData*> 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<UserData*> user, bool mute) {
}).send();
}
std::variant<int, not_null<UserData*>> GroupCall::inviteUsers(
const std::vector<not_null<UserData*>> &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<UserData*> user) {
return !invited.contains(user) && !ranges::contains(
participants,
user,
&Data::GroupCall::Participant::user);
});
auto count = 0;
auto slice = QVector<MTPInputUser>();
auto result = std::variant<int, not_null<UserData*>>(0);
slice.reserve(kMaxInvitePerSlice);
const auto sendSlice = [&] {
count += slice.size();
_api.request(MTPphone_InviteToGroupCall(
inputCall(),
MTP_vector<MTPInputUser>(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) {

View file

@ -96,6 +96,8 @@ public:
void setAudioDuckingEnabled(bool enabled);
void toggleMute(not_null<UserData*> user, bool mute);
std::variant<int, not_null<UserData*>> inviteUsers(
const std::vector<not_null<UserData*>> &users);
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;

View file

@ -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<int> GroupMembers::desiredHeightValue() const {
@ -650,7 +653,7 @@ void GroupMembers::setupHeader(not_null<GroupCall*> call) {
_addMember = Ui::CreateChild<Ui::IconButton>(
parent,
st::groupCallAddMember);
setupButtons();
setupButtons(call);
widthValue(
) | rpl::start_with_next([this](int width) {
@ -674,12 +677,14 @@ object_ptr<Ui::FlatLabel> GroupMembers::setupTitle(
return result;
}
void GroupMembers::setupButtons() {
void GroupMembers::setupButtons(not_null<GroupCall*> 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) {

View file

@ -37,6 +37,9 @@ public:
[[nodiscard]] int desiredHeight() const;
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
[[nodiscard]] rpl::producer<> addMembersRequests() const {
return _addMemberRequests.events();
}
private:
using ListWidget = PeerListContent;
@ -66,9 +69,8 @@ private:
object_ptr<Ui::FlatLabel> setupTitle(not_null<GroupCall*> call);
void setupList();
void setupButtons();
void setupButtons(not_null<GroupCall*> call);
void addMember();
void updateHeaderControlsGeometry(int newWidth);
const base::weak_ptr<GroupCall> _call;
@ -76,6 +78,7 @@ private:
std::unique_ptr<PeerListController> _listController;
object_ptr<Ui::RpWidget> _header = { nullptr };
ListWidget *_list = { nullptr };
rpl::event_stream<> _addMemberRequests;
Ui::RpWidget *_titleWrap = nullptr;
Ui::FlatLabel *_title = nullptr;

View file

@ -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 <QtGui/QWindow>
namespace Calls {
namespace {
class InviteController final : public ParticipantsBoxController {
public:
InviteController(
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> alreadyIn,
int fullInCount);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void itemDeselectedHook(not_null<PeerData*> peer) override;
std::variant<int, not_null<UserData*>> inviteSelectedUsers(
not_null<PeerListBox*> box,
not_null<GroupCall*> call) const;
private:
void updateTitle() const;
[[nodiscard]] int alreadyInCount() const;
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
[[nodiscard]] int fullCount() const;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const override;
const not_null<ChannelData*> _channel;
const base::flat_set<not_null<UserData*>> _alreadyIn;
const int _fullInCount = 0;
mutable base::flat_set<not_null<UserData*>> _skippedUsers;
};
InviteController::InviteController(
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> 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<PeerListRow*> row) {
delegate()->peerListSetRowChecked(row, !row->checked());
updateTitle();
}
base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
updateTitle();
}
int InviteController::alreadyInCount() const {
return std::max(_fullInCount, int(_alreadyIn.size()));
}
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
return _alreadyIn.contains(user);
}
int InviteController::fullCount() const {
return alreadyInCount() + delegate()->peerListSelectedRowsCount();
}
std::unique_ptr<PeerListRow> InviteController::createRow(
not_null<UserData*> user) const {
if (user->isSelf() || user->isBot()) {
if (_skippedUsers.emplace(user).second) {
updateTitle();
}
return nullptr;
}
auto result = std::make_unique<PeerListRow>(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<int, not_null<UserData*>> InviteController::inviteSelectedUsers(
not_null<PeerListBox*> box,
not_null<GroupCall*> call) const {
const auto rows = box->peerListCollectSelectedRows();
const auto users = ranges::view::all(
rows
) | ranges::view::transform([](not_null<PeerData*> peer) {
Expects(peer->isUser());
Expects(!peer->isSelf());
return not_null<UserData*>(peer->asUser());
}) | ranges::to_vector;
return call->inviteUsers(users);
}
} // namespace
void LeaveGroupCallBox(
not_null<Ui::GenericBox*> 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<InviteController>(
_channel,
std::move(alreadyIn),
real->fullCount());
const auto weak = base::make_weak(_call);
auto initBox = [=, controller = controller.get()](
not_null<PeerListBox*> 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<not_null<UserData*>>(&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<int>(&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<PeerListBox>(std::move(controller), initBox));
}
void GroupPanel::initLayout() {
initGeometry();

View file

@ -89,9 +89,9 @@ private:
void updateControlsGeometry();
void showControls();
void copyShareLink();
void hangup(bool discardCallChecked);
void addMembers();
[[nodiscard]] int computeMembersListTop() const;
[[nodiscard]] std::optional<QRect> computeTitleRect() const;
void refreshTitle();

View file

@ -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;
}

View file

@ -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<not_null<UserData*>> & {
static const base::flat_set<not_null<UserData*>> kEmpty;
const auto i = _invitedToCallUsers.find(callId);
return (i != _invitedToCallUsers.end()) ? i->second : kEmpty;
}
void Session::registerInvitedToCallUser(
uint64 callId,
not_null<ChannelData*> channel,
not_null<UserData*> 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<UserData*> 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) {

View file

@ -158,6 +158,14 @@ public:
void unregisterGroupCall(not_null<GroupCall*> call);
GroupCall *groupCall(uint64 callId) const;
[[nodiscard]] auto invitedToCallUsers(uint64 callId) const
-> const base::flat_set<not_null<UserData*>> &;
void registerInvitedToCallUser(
uint64 callId,
not_null<ChannelData*> channel,
not_null<UserData*> user);
void unregisterInvitedToCallUser(uint64 callId, not_null<UserData*> user);
void enumerateUsers(Fn<void(not_null<UserData*>)> action) const;
void enumerateGroups(Fn<void(not_null<PeerData*>)> action) const;
void enumerateChannels(Fn<void(not_null<ChannelData*>)> action) const;
@ -912,6 +920,7 @@ private:
base::flat_set<not_null<ViewElement*>> _heavyViewParts;
base::flat_map<uint64, not_null<GroupCall*>> _groupCalls;
base::flat_map<uint64, base::flat_set<not_null<UserData*>>> _invitedToCallUsers;
History *_topPromoted = nullptr;

View file

@ -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;
}

View file

@ -458,6 +458,14 @@ struct ActionGeoProximityReached {
bool toSelf = false;
};
struct ActionGroupCall {
int duration = 0;
};
struct ActionInviteToGroupCall {
std::vector<int32> 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(

View file

@ -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()) {

View file

@ -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)) {

View file

@ -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) };
});