Add creating of a scheduled group call.

This commit is contained in:
John Preston 2021-04-05 14:29:03 +04:00
parent e6587f2556
commit 15d17c8b0e
15 changed files with 427 additions and 206 deletions

View file

@ -2056,6 +2056,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_join_as_header" = "Join Voice Chat as..."; "lng_group_call_join_as_header" = "Join Voice Chat as...";
"lng_group_call_display_as_header" = "Display me as..."; "lng_group_call_display_as_header" = "Display me as...";
"lng_group_call_join_as_about" = "Choose whether you want to be displayed as your personal account or as your channel."; "lng_group_call_join_as_about" = "Choose whether you want to be displayed as your personal account or as your channel.";
"lng_group_call_or_schedule" = "Or you can {link}.";
"lng_group_call_schedule" = "Schedule Voice Chat";
"lng_group_call_schedule_title" = "Schedule Voice Chat";
"lng_group_call_schedule_notified_group" = "The members of the group will be notified that the voice chat will start in {duration}.";
"lng_group_call_schedule_notified_channel" = "The subscribers of the channel will be notified that the voice chat will start in {duration}.";
"lng_group_call_scheduled_status" = "Scheduled";
"lng_group_call_scheduled_title" = "Scheduled Voice Chat";
"lng_group_call_starts_short" = "Starts {when}";
"lng_group_call_starts" = "Voice Chat starts {when}";
"lng_group_call_starts_today" = "today at {time}";
"lng_group_call_starts_tomorrow" = "tomorrow at {time}";
"lng_group_call_starts_date" = "{date} at {time}";
"lng_group_call_starts_in" = "Starts in";
"lng_group_call_set_reminder" = "Set Reminder";
"lng_group_call_cancel_reminder" = "Cancel Reminder";
"lng_group_call_join_as_personal" = "personal account"; "lng_group_call_join_as_personal" = "personal account";
"lng_group_call_edit_title" = "Edit voice chat title"; "lng_group_call_edit_title" = "Edit voice chat title";
"lng_group_call_switch_done" = "Members of this voice chat will now see you as **{user}**"; "lng_group_call_switch_done" = "Members of this voice chat will now see you as **{user}**";

View file

@ -18,15 +18,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "boxes/peer_list_box.h" #include "boxes/peer_list_box.h"
#include "boxes/confirm_box.h" #include "boxes/confirm_box.h"
#include "base/unixtime.h"
#include "base/timer_rpl.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_calls.h" #include "styles/style_calls.h"
namespace Calls::Group { namespace Calls::Group {
namespace { namespace {
constexpr auto kDefaultScheduleDuration = 60 * TimeId(60);
constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
using Context = ChooseJoinAsProcess::Context; using Context = ChooseJoinAsProcess::Context;
class ListController : public PeerListController { class ListController : public PeerListController {
@ -109,6 +115,60 @@ not_null<PeerData*> ListController::selected() const {
return _selected; return _selected;
} }
void ScheduleGroupCallBox(
not_null<Ui::GenericBox*> box,
const JoinInfo &info,
Fn<void(JoinInfo)> done) {
const auto send = [=](TimeId date) {
box->closeBox();
auto copy = info;
copy.scheduleDate = date;
done(std::move(copy));
};
const auto duration = box->lifetime().make_state<
rpl::variable<QString>>();
auto description = (info.peer->isBroadcast()
? tr::lng_group_call_schedule_notified_channel
: tr::lng_group_call_schedule_notified_group)(
lt_duration,
duration->value());
auto descriptor = Ui::ChooseDateTimeBox(
box,
tr::lng_group_call_schedule_title(),
tr::lng_schedule_button(),
send,
base::unixtime::now() + kDefaultScheduleDuration,
std::move(description));
using namespace rpl::mappers;
*duration = rpl::combine(
rpl::single(
rpl::empty_value()
) | rpl::then(base::timer_each(kLabelRefreshInterval)),
std::move(descriptor.values) | rpl::filter(_1 != 0),
_2
) | rpl::map([](TimeId date) {
const auto now = base::unixtime::now();
const auto duration = (date - now);
if (duration >= 24 * 60 * 60) {
return tr::lng_signin_reset_days(
tr::now,
lt_count,
duration / (24 * 60 * 60));
} else if (duration >= 60 * 60) {
return tr::lng_signin_reset_hours(
tr::now,
lt_count,
duration / (60 * 60));
}
return tr::lng_signin_reset_minutes(
tr::now,
lt_count,
std::max(duration / 60, 1));
});
}
void ChooseJoinAsBox( void ChooseJoinAsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
Context context, Context context,
@ -124,12 +184,13 @@ void ChooseJoinAsBox(
} }
Unexpected("Context in ChooseJoinAsBox."); Unexpected("Context in ChooseJoinAsBox.");
}()); }());
const auto &labelSt = (context == Context::Switch)
? st::groupCallJoinAsLabel
: st::confirmPhoneAboutLabel;
box->addRow(object_ptr<Ui::FlatLabel>( box->addRow(object_ptr<Ui::FlatLabel>(
box, box,
tr::lng_group_call_join_as_about(), tr::lng_group_call_join_as_about(),
(context == Context::Switch labelSt));
? st::groupCallJoinAsLabel
: st::confirmPhoneAboutLabel)));
auto &lifetime = box->lifetime(); auto &lifetime = box->lifetime();
const auto delegate = lifetime.make_state< const auto delegate = lifetime.make_state<
@ -155,6 +216,27 @@ void ChooseJoinAsBox(
auto next = (context == Context::Switch) auto next = (context == Context::Switch)
? tr::lng_settings_save() ? tr::lng_settings_save()
: tr::lng_continue(); : tr::lng_continue();
if (context == Context::Create) {
const auto makeLink = [](const QString &text) {
return Ui::Text::Link(text);
};
const auto label = box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_group_call_or_schedule(
lt_link,
tr::lng_group_call_schedule(makeLink),
Ui::Text::WithEntities),
labelSt));
label->setClickHandlerFilter([=](const auto&...) {
auto withJoinAs = info;
withJoinAs.joinAs = controller->selected();
box->getDelegate()->show(Box(
ScheduleGroupCallBox,
withJoinAs,
done));
return false;
});
}
box->addButton(std::move(next), [=] { box->addButton(std::move(next), [=] {
auto copy = info; auto copy = info;
copy.joinAs = controller->selected(); copy.joinAs = controller->selected();

View file

@ -184,6 +184,7 @@ GroupCall::GroupCall(
, _joinAs(info.joinAs) , _joinAs(info.joinAs)
, _possibleJoinAs(std::move(info.possibleJoinAs)) , _possibleJoinAs(std::move(info.possibleJoinAs))
, _joinHash(info.joinHash) , _joinHash(info.joinHash)
, _scheduleDate(info.scheduleDate)
, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) , _lastSpokeCheckTimer([=] { checkLastSpoke(); })
, _checkJoinedTimer([=] { checkJoined(); }) , _checkJoinedTimer([=] { checkJoined(); })
, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) , _pushToTalkCancelTimer([=] { pushToTalkCancel(); })
@ -218,14 +219,14 @@ GroupCall::GroupCall(
const auto id = inputCall.c_inputGroupCall().vid().v; const auto id = inputCall.c_inputGroupCall().vid().v;
if (id) { if (id) {
if (const auto call = _peer->groupCall(); call && call->id() == id) { if (const auto call = _peer->groupCall(); call && call->id() == id) {
_scheduleDate = call->scheduleDate();
if (!_peer->canManageGroupCall() && call->joinMuted()) { if (!_peer->canManageGroupCall() && call->joinMuted()) {
_muted = MuteState::ForceMuted; _muted = MuteState::ForceMuted;
} }
} }
_state = State::Joining;
join(inputCall); join(inputCall);
} else { } else {
start(); start(info.scheduleDate);
} }
_mediaDevices->audioInputId( _mediaDevices->audioInputId(
@ -326,13 +327,14 @@ bool GroupCall::showChooseJoinAs() const {
&& !_possibleJoinAs.front()->isSelf()); && !_possibleJoinAs.front()->isSelf());
} }
void GroupCall::start() { void GroupCall::start(TimeId scheduleDate) {
using Flag = MTPphone_CreateGroupCall::Flag;
_createRequestId = _api.request(MTPphone_CreateGroupCall( _createRequestId = _api.request(MTPphone_CreateGroupCall(
MTP_flags(0), MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag(0)),
_peer->input, _peer->input,
MTP_int(openssl::RandomValue<int32>()), MTP_int(openssl::RandomValue<int32>()),
MTPstring(), // title MTPstring(), // title
MTPint() // schedule_date MTP_int(scheduleDate)
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
_acceptFields = true; _acceptFields = true;
_peer->session().api().applyUpdates(result); _peer->session().api().applyUpdates(result);
@ -350,6 +352,15 @@ void GroupCall::start() {
} }
void GroupCall::join(const MTPInputGroupCall &inputCall) { void GroupCall::join(const MTPInputGroupCall &inputCall) {
inputCall.match([&](const MTPDinputGroupCall &data) {
_id = data.vid().v;
_accessHash = data.vaccess_hash().v;
});
if (_scheduleDate) {
setState(State::Waiting);
return;
}
setState(State::Joining); setState(State::Joining);
if (const auto chat = _peer->asChat()) { if (const auto chat = _peer->asChat()) {
chat->setGroupCall(inputCall); chat->setGroupCall(inputCall);
@ -358,12 +369,7 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) {
} else { } else {
Unexpected("Peer type in GroupCall::join."); Unexpected("Peer type in GroupCall::join.");
} }
rejoin();
inputCall.match([&](const MTPDinputGroupCall &data) {
_id = data.vid().v;
_accessHash = data.vaccess_hash().v;
rejoin();
});
using Update = Data::GroupCall::ParticipantUpdate; using Update = Data::GroupCall::ParticipantUpdate;
_peer->groupCall()->participantUpdated( _peer->groupCall()->participantUpdated(
@ -646,8 +652,10 @@ void GroupCall::rejoinAs(Group::JoinInfo info) {
.wasJoinAs = _joinAs, .wasJoinAs = _joinAs,
.nowJoinAs = info.joinAs, .nowJoinAs = info.joinAs,
}; };
setState(State::Joining); if (!_scheduleDate) {
rejoin(info.joinAs); setState(State::Joining);
rejoin(info.joinAs);
}
_rejoinEvents.fire_copy(event); _rejoinEvents.fire_copy(event);
} }
@ -734,6 +742,9 @@ void GroupCall::handlePossibleCreateOrJoinResponse(
void GroupCall::handlePossibleCreateOrJoinResponse( void GroupCall::handlePossibleCreateOrJoinResponse(
const MTPDgroupCall &data) { const MTPDgroupCall &data) {
if (const auto date = data.vschedule_date()) {
_scheduleDate = date->v;
}
if (_acceptFields) { if (_acceptFields) {
if (!_instance && !_id) { if (!_instance && !_id) {
join(MTP_inputGroupCall(data.vid(), data.vaccess_hash())); join(MTP_inputGroupCall(data.vid(), data.vaccess_hash()));

View file

@ -109,8 +109,11 @@ public:
return _joinAs; return _joinAs;
} }
[[nodiscard]] bool showChooseJoinAs() const; [[nodiscard]] bool showChooseJoinAs() const;
[[nodiscard]] TimeId scheduleDate() const {
return _scheduleDate;
}
void start(); void start(TimeId scheduleDate);
void hangup(); void hangup();
void discard(); void discard();
void rejoinAs(Group::JoinInfo info); void rejoinAs(Group::JoinInfo info);
@ -138,6 +141,7 @@ public:
enum State { enum State {
Creating, Creating,
Waiting,
Joining, Joining,
Connecting, Connecting,
Joined, Joined,
@ -310,6 +314,7 @@ private:
uint64 _id = 0; uint64 _id = 0;
uint64 _accessHash = 0; uint64 _accessHash = 0;
uint32 _mySsrc = 0; uint32 _mySsrc = 0;
TimeId _scheduleDate = 0;
base::flat_set<uint32> _mySsrcs; base::flat_set<uint32> _mySsrcs;
mtpRequestId _createRequestId = 0; mtpRequestId _createRequestId = 0;
mtpRequestId _updateMuteRequestId = 0; mtpRequestId _updateMuteRequestId = 0;

View file

@ -44,6 +44,7 @@ struct JoinInfo {
not_null<PeerData*> joinAs; not_null<PeerData*> joinAs;
std::vector<not_null<PeerData*>> possibleJoinAs; std::vector<not_null<PeerData*>> possibleJoinAs;
QString joinHash; QString joinHash;
TimeId scheduleDate = 0;
}; };
} // namespace Calls::Group } // namespace Calls::Group

View file

@ -259,7 +259,7 @@ Panel::Panel(not_null<GroupCall*> call)
_window->body(), _window->body(),
st::groupCallTitle)) st::groupCallTitle))
#endif // !Q_OS_MAC #endif // !Q_OS_MAC
, _members(widget(), call) , _scheduleDate(call->scheduleDate())
, _settings(widget(), st::groupCallSettings) , _settings(widget(), st::groupCallSettings)
, _mute(std::make_unique<Ui::CallMuteButton>( , _mute(std::make_unique<Ui::CallMuteButton>(
widget(), widget(),
@ -286,30 +286,7 @@ Panel::Panel(not_null<GroupCall*> call)
showAndActivate(); showAndActivate();
setupJoinAsChangedToasts(); setupJoinAsChangedToasts();
setupTitleChangedToasts(); setupTitleChangedToasts();
setupAllowedToSpeakToasts();
call->allowedToSpeakNotifications(
) | rpl::start_with_next([=] {
if (isActive()) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_can_speak_here(tr::now) },
});
} else {
const auto real = _peer->groupCall();
const auto name = (real
&& (real->id() == call->id())
&& !real->title().isEmpty())
? real->title()
: _peer->name;
Ui::ShowMultilineToast({
.text = tr::lng_group_call_can_speak(
tr::now,
lt_chat,
Ui::Text::Bold(name),
Ui::Text::WithEntities),
});
}
}, widget()->lifetime());
} }
Panel::~Panel() { Panel::~Panel() {
@ -326,7 +303,7 @@ void Panel::setupRealCallViewers(not_null<GroupCall*> call) {
) | rpl::map([=] { ) | rpl::map([=] {
return peer->groupCall(); return peer->groupCall();
}) | rpl::filter([=](Data::GroupCall *real) { }) | rpl::filter([=](Data::GroupCall *real) {
return _call && real && (real->id() == _call->id()); return real && (real->id() == _call->id());
}) | rpl::take( }) | rpl::take(
1 1
) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) { ) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
@ -393,11 +370,9 @@ void Panel::initWindow() {
} else if (e->type() == QEvent::KeyPress } else if (e->type() == QEvent::KeyPress
|| e->type() == QEvent::KeyRelease) { || e->type() == QEvent::KeyRelease) {
if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) { if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) {
if (_call) { _call->pushToTalk(
_call->pushToTalk( e->type() == QEvent::KeyPress,
e->type() == QEvent::KeyPress, kSpacePushToTalkDelay);
kSpacePushToTalkDelay);
}
} }
} }
return base::EventFilterResult::Continue; return base::EventFilterResult::Continue;
@ -439,9 +414,7 @@ void Panel::initWidget() {
} }
void Panel::endCall() { void Panel::endCall() {
if (!_call) { if (!_call->peer()->canManageGroupCall()) {
return;
} else if (!_call->peer()->canManageGroupCall()) {
_call->hangup(); _call->hangup();
return; return;
} }
@ -455,7 +428,7 @@ void Panel::endCall() {
void Panel::initControls() { void Panel::initControls() {
_mute->clicks( _mute->clicks(
) | rpl::filter([=](Qt::MouseButton button) { ) | rpl::filter([=](Qt::MouseButton button) {
return (button == Qt::LeftButton) && (_call != nullptr); return (button == Qt::LeftButton);
}) | rpl::start_with_next([=] { }) | rpl::start_with_next([=] {
const auto oldState = _call->muted(); const auto oldState = _call->muted();
const auto newState = (oldState == MuteState::ForceMuted) const auto newState = (oldState == MuteState::ForceMuted)
@ -470,32 +443,17 @@ void Panel::initControls() {
_hangup->setClickedCallback([=] { endCall(); }); _hangup->setClickedCallback([=] { endCall(); });
_settings->setClickedCallback([=] { _settings->setClickedCallback([=] {
if (_call) { _layerBg->showBox(Box(SettingsBox, _call));
_layerBg->showBox(Box(SettingsBox, _call));
}
}); });
_settings->setText(tr::lng_group_call_settings()); _settings->setText(tr::lng_group_call_settings());
_hangup->setText(tr::lng_group_call_leave()); _hangup->setText(tr::lng_group_call_leave());
_members->desiredHeightValue( if (!_call->scheduleDate()) {
) | rpl::start_with_next([=] { setupMembers();
updateControlsGeometry();
}, _members->lifetime());
initWithCall(_call);
}
void Panel::initWithCall(GroupCall *call) {
_callLifetime.destroy();
_call = call;
if (!_call) {
return;
} }
_peer = _call->peer(); _call->stateValue(
call->stateValue(
) | rpl::filter([](State state) { ) | rpl::filter([](State state) {
return (state == State::HangingUp) return (state == State::HangingUp)
|| (state == State::Ended) || (state == State::Ended)
@ -505,59 +463,13 @@ void Panel::initWithCall(GroupCall *call) {
closeBeforeDestroy(); closeBeforeDestroy();
}, _callLifetime); }, _callLifetime);
call->levelUpdates( _call->levelUpdates(
) | rpl::filter([=](const LevelUpdate &update) { ) | rpl::filter([=](const LevelUpdate &update) {
return update.me; return update.me;
}) | rpl::start_with_next([=](const LevelUpdate &update) { }) | rpl::start_with_next([=](const LevelUpdate &update) {
_mute->setLevel(update.value); _mute->setLevel(update.value);
}, _callLifetime); }, _callLifetime);
_members->toggleMuteRequests(
) | rpl::start_with_next([=](MuteRequest request) {
if (_call) {
_call->toggleMute(request);
}
}, _callLifetime);
_members->changeVolumeRequests(
) | rpl::start_with_next([=](VolumeRequest request) {
if (_call) {
_call->changeVolume(request);
}
}, _callLifetime);
_members->kickParticipantRequests(
) | rpl::start_with_next([=](not_null<PeerData*> participantPeer) {
kickParticipant(participantPeer);
}, _callLifetime);
const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
_peer,
showBox,
showToast);
auto shareLink = std::move(shareLinkCallback);
_members->lifetime().add(std::move(shareLinkLifetime));
_members->addMembersRequests(
) | rpl::start_with_next([=] {
if (_call) {
if (_peer->isBroadcast() && _peer->asChannel()->hasUsername()) {
shareLink();
} else {
addMembers();
}
}
}, _callLifetime);
using namespace rpl::mappers; using namespace rpl::mappers;
rpl::combine( rpl::combine(
_call->mutedValue() | MapPushToTalkToActive(), _call->mutedValue() | MapPushToTalkToActive(),
@ -600,6 +512,61 @@ void Panel::initWithCall(GroupCall *call) {
}, _callLifetime); }, _callLifetime);
} }
void Panel::setupMembers() {
Expects(!_members);
_members.create(widget(), _call);
_members->desiredHeightValue(
) | rpl::start_with_next([=] {
updateMembersGeometry();
}, _members->lifetime());
_members->toggleMuteRequests(
) | rpl::start_with_next([=](MuteRequest request) {
if (_call) {
_call->toggleMute(request);
}
}, _callLifetime);
_members->changeVolumeRequests(
) | rpl::start_with_next([=](VolumeRequest request) {
if (_call) {
_call->changeVolume(request);
}
}, _callLifetime);
_members->kickParticipantRequests(
) | rpl::start_with_next([=](not_null<PeerData*> participantPeer) {
kickParticipant(participantPeer);
}, _callLifetime);
const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
_peer,
showBox,
showToast);
auto shareLink = std::move(shareLinkCallback);
_members->lifetime().add(std::move(shareLinkLifetime));
_members->addMembersRequests(
) | rpl::start_with_next([=] {
if (_peer->isBroadcast() && _peer->asChannel()->hasUsername()) {
shareLink();
} else {
addMembers();
}
}, _callLifetime);
}
void Panel::setupJoinAsChangedToasts() { void Panel::setupJoinAsChangedToasts() {
_call->rejoinEvents( _call->rejoinEvents(
) | rpl::filter([](RejoinEvent event) { ) | rpl::filter([](RejoinEvent event) {
@ -623,7 +590,8 @@ void Panel::setupJoinAsChangedToasts() {
void Panel::setupTitleChangedToasts() { void Panel::setupTitleChangedToasts() {
_call->titleChanged( _call->titleChanged(
) | rpl::filter([=] { ) | rpl::filter([=] {
return _peer->groupCall() && _peer->groupCall()->id() == _call->id(); const auto real = _peer->groupCall();
return real && (real->id() == _call->id());
}) | rpl::map([=] { }) | rpl::map([=] {
return _peer->groupCall()->title().isEmpty() return _peer->groupCall()->title().isEmpty()
? _peer->name ? _peer->name
@ -640,8 +608,44 @@ void Panel::setupTitleChangedToasts() {
}, widget()->lifetime()); }, widget()->lifetime());
} }
void Panel::setupAllowedToSpeakToasts() {
_call->allowedToSpeakNotifications(
) | rpl::start_with_next([=] {
if (isActive()) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_can_speak_here(tr::now) },
});
} else {
const auto real = _peer->groupCall();
const auto name = (real
&& (real->id() == _call->id())
&& !real->title().isEmpty())
? real->title()
: _peer->name;
Ui::ShowMultilineToast({
.text = tr::lng_group_call_can_speak(
tr::now,
lt_chat,
Ui::Text::Bold(name),
Ui::Text::WithEntities),
});
}
}, widget()->lifetime());
}
void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) { void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
if (!_members) {
real->scheduleDateValue(
) | rpl::filter([=](TimeId scheduleDate) {
return !scheduleDate;
}) | rpl::take(1) | rpl::start_with_next([=] {
setupMembers();
}, _callLifetime);
}
_titleText = real->titleValue(); _titleText = real->titleValue();
_scheduleDate = real->scheduleDateValue();
const auto validateRecordingMark = [=](bool recording) { const auto validateRecordingMark = [=](bool recording) {
if (!recording && _recordingMark) { if (!recording && _recordingMark) {
@ -702,7 +706,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
.parentOverride = widget(), .parentOverride = widget(),
.text = (recorded .text = (recorded
? tr::lng_group_call_recording_started ? tr::lng_group_call_recording_started
: (_call && _call->recordingStoppedByMe()) : _call->recordingStoppedByMe()
? tr::lng_group_call_recording_saved ? tr::lng_group_call_recording_saved
: tr::lng_group_call_recording_stopped)( : tr::lng_group_call_recording_stopped)(
tr::now, tr::now,
@ -751,9 +755,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
void Panel::chooseJoinAs() { void Panel::chooseJoinAs() {
const auto context = ChooseJoinAsProcess::Context::Switch; const auto context = ChooseJoinAsProcess::Context::Switch;
const auto callback = [=](JoinInfo info) { const auto callback = [=](JoinInfo info) {
if (_call) { _call->rejoinAs(info);
_call->rejoinAs(info);
}
}; };
const auto showBox = [=](object_ptr<Ui::BoxContent> next) { const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next)); _layerBg->showBox(std::move(next));
@ -774,7 +776,7 @@ void Panel::chooseJoinAs() {
} }
void Panel::showMainMenu() { void Panel::showMainMenu() {
if (_menu || !_call) { if (_menu) {
return; return;
} }
_menu.create(widget(), st::groupCallDropdownMenu); _menu.create(widget(), st::groupCallDropdownMenu);
@ -822,7 +824,7 @@ void Panel::showMainMenu() {
void Panel::addMembers() { void Panel::addMembers() {
const auto real = _peer->groupCall(); const auto real = _peer->groupCall();
if (!_call || !real || real->id() != _call->id()) { if (!real || real->id() != _call->id()) {
return; return;
} }
auto alreadyIn = _peer->owner().invitedToCallUsers(real->id()); auto alreadyIn = _peer->owner().invitedToCallUsers(real->id());
@ -848,7 +850,7 @@ void Panel::addMembers() {
&st::groupCallInviteMembersList, &st::groupCallInviteMembersList,
&st::groupCallMultiSelect); &st::groupCallMultiSelect);
const auto weak = base::make_weak(_call); const auto weak = base::make_weak(_call.get());
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) {
@ -1031,7 +1033,7 @@ void Panel::showControls() {
void Panel::closeBeforeDestroy() { void Panel::closeBeforeDestroy() {
_window->close(); _window->close();
initWithCall(nullptr); _callLifetime.destroy();
} }
void Panel::initGeometry() { void Panel::initGeometry() {
@ -1066,28 +1068,8 @@ void Panel::updateControlsGeometry() {
if (widget()->size().isEmpty()) { if (widget()->size().isEmpty()) {
return; return;
} }
const auto desiredHeight = _members->desiredHeight();
const auto membersWidthAvailable = widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidthMin = st::groupCallWidth
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidth = std::clamp(
membersWidthAvailable,
membersWidthMin,
st::groupCallMembersWidthMax);
const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;
const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip; const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip;
const auto membersTop = st::groupCallMembersTop;
const auto availableHeight = muteTop
- membersTop
- st::groupCallMembersMargin.bottom();
_members->setGeometry(
(widget()->width() - membersWidth) / 2,
membersTop,
membersWidth,
std::min(desiredHeight, availableHeight));
const auto muteSize = _mute->innerSize().width(); const auto muteSize = _mute->innerSize().width();
const auto fullWidth = muteSize const auto fullWidth = muteSize
+ 2 * _settings->width() + 2 * _settings->width()
@ -1095,6 +1077,8 @@ void Panel::updateControlsGeometry() {
_mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop }); _mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop });
_settings->moveToLeft((widget()->width() - fullWidth) / 2, buttonsTop); _settings->moveToLeft((widget()->width() - fullWidth) / 2, buttonsTop);
_hangup->moveToRight((widget()->width() - fullWidth) / 2, buttonsTop); _hangup->moveToRight((widget()->width() - fullWidth) / 2, buttonsTop);
updateMembersGeometry();
refreshTitle(); refreshTitle();
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
@ -1120,6 +1104,33 @@ void Panel::updateControlsGeometry() {
} }
} }
void Panel::updateMembersGeometry() {
if (!_members) {
return;
}
const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;
const auto membersTop = st::groupCallMembersTop;
const auto availableHeight = muteTop
- membersTop
- st::groupCallMembersMargin.bottom();
const auto desiredHeight = _members->desiredHeight();
const auto membersWidthAvailable = widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidthMin = st::groupCallWidth
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidth = std::clamp(
membersWidthAvailable,
membersWidthMin,
st::groupCallMembersWidthMax);
_members->setGeometry(
(widget()->width() - membersWidth) / 2,
membersTop,
membersWidth,
std::min(desiredHeight, availableHeight));
}
void Panel::refreshTitle() { void Panel::refreshTitle() {
if (!_title) { if (!_title) {
auto text = rpl::combine( auto text = rpl::combine(
@ -1143,11 +1154,16 @@ void Panel::refreshTitle() {
if (!_subtitle) { if (!_subtitle) {
_subtitle.create( _subtitle.create(
widget(), widget(),
tr::lng_group_call_members( _scheduleDate.value(
lt_count_decimal, ) | rpl::map([=](TimeId scheduleDate) {
_members->fullCountValue() | rpl::map([](int value) { return scheduleDate
return (value > 0) ? float64(value) : 1.; ? tr::lng_group_call_scheduled_status()
})), : tr::lng_group_call_members(
lt_count_decimal,
_members->fullCountValue() | rpl::map([](int value) {
return (value > 0) ? float64(value) : 1.;
}));
}) | rpl::flatten_latest(),
st::groupCallSubtitleLabel); st::groupCallSubtitleLabel);
_subtitle->show(); _subtitle->show();
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);

View file

@ -73,15 +73,17 @@ private:
void initWindow(); void initWindow();
void initWidget(); void initWidget();
void initControls(); void initControls();
void initWithCall(GroupCall *call);
void initLayout(); void initLayout();
void initGeometry(); void initGeometry();
void setupMembers();
void setupJoinAsChangedToasts(); void setupJoinAsChangedToasts();
void setupTitleChangedToasts(); void setupTitleChangedToasts();
void setupAllowedToSpeakToasts();
bool handleClose(); bool handleClose();
void updateControlsGeometry(); void updateControlsGeometry();
void updateMembersGeometry();
void showControls(); void showControls();
void endCall(); void endCall();
@ -100,7 +102,7 @@ private:
void migrate(not_null<ChannelData*> channel); void migrate(not_null<ChannelData*> channel);
void subscribeToPeerChanges(); void subscribeToPeerChanges();
GroupCall *_call = nullptr; const not_null<GroupCall*> _call;
not_null<PeerData*> _peer; not_null<PeerData*> _peer;
const std::unique_ptr<Ui::Window> _window; const std::unique_ptr<Ui::Window> _window;
@ -118,8 +120,9 @@ private:
object_ptr<Ui::IconButton> _menuToggle = { nullptr }; object_ptr<Ui::IconButton> _menuToggle = { nullptr };
object_ptr<Ui::DropdownMenu> _menu = { nullptr }; object_ptr<Ui::DropdownMenu> _menu = { nullptr };
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr }; object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
object_ptr<Members> _members; object_ptr<Members> _members = { nullptr };
rpl::variable<QString> _titleText; rpl::variable<QString> _titleText;
rpl::variable<TimeId> _scheduleDate;
ChooseJoinAsProcess _joinAsProcess; ChooseJoinAsProcess _joinAsProcess;
object_ptr<Ui::CallButton> _settings; object_ptr<Ui::CallButton> _settings;

View file

@ -329,6 +329,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) {
changePeerEmptyCallFlag(); changePeerEmptyCallFlag();
_title = qs(data.vtitle().value_or_empty()); _title = qs(data.vtitle().value_or_empty());
_recordStartDate = data.vrecord_start_date().value_or_empty(); _recordStartDate = data.vrecord_start_date().value_or_empty();
_scheduleDate = data.vschedule_date().value_or_empty();
_allParticipantsLoaded _allParticipantsLoaded
= (_serverParticipantsCount == _participants.size()); = (_serverParticipantsCount == _participants.size());
} }

View file

@ -63,6 +63,15 @@ public:
[[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const { [[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const {
return _recordStartDate.changes(); return _recordStartDate.changes();
} }
[[nodiscard]] TimeId scheduleDate() const {
return _scheduleDate.current();
}
[[nodiscard]] rpl::producer<TimeId> scheduleDateValue() const {
return _scheduleDate.value();
}
[[nodiscard]] rpl::producer<TimeId> scheduleDateChanges() const {
return _scheduleDate.changes();
}
void setPeer(not_null<PeerData*> peer); void setPeer(not_null<PeerData*> peer);
@ -163,6 +172,7 @@ private:
int _serverParticipantsCount = 0; int _serverParticipantsCount = 0;
rpl::variable<int> _fullCount = 0; rpl::variable<int> _fullCount = 0;
rpl::variable<TimeId> _recordStartDate = 0; rpl::variable<TimeId> _recordStartDate = 0;
rpl::variable<TimeId> _scheduleDate = 0;
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs; base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds; base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds;

View file

@ -330,10 +330,18 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
RegenerateUserpics(state, call, userpicSize); RegenerateUserpics(state, call, userpicSize);
call->fullCountValue( rpl::combine(
) | rpl::start_with_next([=](int count) { call->titleValue(),
call->scheduleDateValue(),
call->fullCountValue()
) | rpl::start_with_next([=](
const QString &title,
TimeId scheduleDate,
int count) {
state->current.title = title;
state->current.scheduleDate = scheduleDate;
state->current.count = count; state->current.count = count;
state->current.shown = (count > 0); state->current.shown = (count > 0) || (scheduleDate != 0);
consumer.put_next_copy(state->current); consumer.put_next_copy(state->current);
}, lifetime); }, lifetime);

View file

@ -994,6 +994,7 @@ void MainWidget::setCurrentGroupCall(Calls::GroupCall *call) {
) | rpl::start_with_next([=](Calls::GroupCall::State state) { ) | rpl::start_with_next([=](Calls::GroupCall::State state) {
using State = Calls::GroupCall::State; using State = Calls::GroupCall::State;
if (state != State::Creating if (state != State::Creating
&& state != State::Waiting
&& state != State::Joining && state != State::Joining
&& state != State::Joined && state != State::Joined
&& state != State::Connecting) { && state != State::Connecting) {

View file

@ -572,36 +572,49 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
rpl::producer<QString> title, rpl::producer<QString> title,
rpl::producer<QString> submit, rpl::producer<QString> submit,
Fn<void(TimeId)> done, Fn<void(TimeId)> done,
TimeId time) { TimeId time,
rpl::producer<QString> description) {
struct State {
rpl::variable<QDate> date;
not_null<InputField*> day;
not_null<TimeInput*> time;
not_null<FlatLabel*> at;
};
box->setTitle(std::move(title)); box->setTitle(std::move(title));
box->setWidth(st::boxWideWidth); box->setWidth(st::boxWideWidth);
const auto date = CreateChild<rpl::variable<QDate>>(
box.get(),
base::unixtime::parse(time).date());
const auto content = box->addRow( const auto content = box->addRow(
object_ptr<FixedHeightWidget>(box, st::scheduleHeight)); object_ptr<FixedHeightWidget>(box, st::scheduleHeight));
const auto dayInput = CreateChild<InputField>( if (description) {
content, box->addRow(object_ptr<FlatLabel>(
st::scheduleDateField); box,
const auto timeInput = CreateChild<TimeInput>( std::move(description),
content, st::boxLabel));
TimeString(time)); }
const auto at = CreateChild<FlatLabel>( const auto state = box->lifetime().make_state<State>(State{
content, .date = base::unixtime::parse(time).date(),
tr::lng_schedule_at(), .day = CreateChild<InputField>(
st::scheduleAtLabel); content,
st::scheduleDateField),
.time = CreateChild<TimeInput>(
content,
TimeString(time)),
.at = CreateChild<FlatLabel>(
content,
tr::lng_schedule_at(),
st::scheduleAtLabel),
});
date->value( state->date.value(
) | rpl::start_with_next([=](QDate date) { ) | rpl::start_with_next([=](QDate date) {
dayInput->setText(DayString(date)); state->day->setText(DayString(date));
timeInput->setFocusFast(); state->time->setFocusFast();
}, dayInput->lifetime()); }, state->day->lifetime());
const auto minDate = QDate::currentDate(); const auto minDate = QDate::currentDate();
const auto maxDate = minDate.addYears(1).addDays(-1); const auto maxDate = minDate.addYears(1).addDays(-1);
const auto &dayViewport = dayInput->rawTextEdit()->viewport(); const auto &dayViewport = state->day->rawTextEdit()->viewport();
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) { base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) { if (event->type() == QEvent::Wheel) {
const auto e = static_cast<QWheelEvent*>(event.get()); const auto e = static_cast<QWheelEvent*>(event.get());
@ -609,8 +622,8 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
if (!direction) { if (!direction) {
return base::EventFilterResult::Continue; return base::EventFilterResult::Continue;
} }
const auto d = date->current().addDays(direction); const auto d = state->date.current().addDays(direction);
*date = std::clamp(d, minDate, maxDate); state->date = std::clamp(d, minDate, maxDate);
return base::EventFilterResult::Cancel; return base::EventFilterResult::Cancel;
} }
return base::EventFilterResult::Continue; return base::EventFilterResult::Continue;
@ -619,19 +632,19 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
content->widthValue( content->widthValue(
) | rpl::start_with_next([=](int width) { ) | rpl::start_with_next([=](int width) {
const auto paddings = width const auto paddings = width
- at->width() - state->at->width()
- 2 * st::scheduleAtSkip - 2 * st::scheduleAtSkip
- st::scheduleDateWidth - st::scheduleDateWidth
- st::scheduleTimeWidth; - st::scheduleTimeWidth;
const auto left = paddings / 2; const auto left = paddings / 2;
dayInput->resizeToWidth(st::scheduleDateWidth); state->day->resizeToWidth(st::scheduleDateWidth);
dayInput->moveToLeft(left, st::scheduleDateTop, width); state->day->moveToLeft(left, st::scheduleDateTop, width);
at->moveToLeft( state->at->moveToLeft(
left + st::scheduleDateWidth + st::scheduleAtSkip, left + st::scheduleDateWidth + st::scheduleAtSkip,
st::scheduleAtTop, st::scheduleAtTop,
width); width);
timeInput->resizeToWidth(st::scheduleTimeWidth); state->time->resizeToWidth(st::scheduleTimeWidth);
timeInput->moveToLeft( state->time->moveToLeft(
width - left - st::scheduleTimeWidth, width - left - st::scheduleTimeWidth,
st::scheduleDateTop, st::scheduleDateTop,
width); width);
@ -639,12 +652,12 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
const auto calendar = const auto calendar =
content->lifetime().make_state<QPointer<CalendarBox>>(); content->lifetime().make_state<QPointer<CalendarBox>>();
QObject::connect(dayInput, &InputField::focused, [=] { QObject::connect(state->day, &InputField::focused, [=] {
if (*calendar) { if (*calendar) {
return; return;
} }
const auto chosen = [=](QDate chosen) { const auto chosen = [=](QDate chosen) {
*date = chosen; state->date = chosen;
(*calendar)->closeBox(); (*calendar)->closeBox();
}; };
const auto finalize = [=](not_null<CalendarBox*> box) { const auto finalize = [=](not_null<CalendarBox*> box) {
@ -652,31 +665,28 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
box->setMaxDate(maxDate); box->setMaxDate(maxDate);
}; };
*calendar = box->getDelegate()->show(Box<CalendarBox>( *calendar = box->getDelegate()->show(Box<CalendarBox>(
date->current(), state->date.current(),
date->current(), state->date.current(),
crl::guard(box, chosen), crl::guard(box, chosen),
finalize)); finalize));
(*calendar)->boxClosing( (*calendar)->boxClosing(
) | rpl::start_with_next(crl::guard(timeInput, [=] { ) | rpl::start_with_next(crl::guard(state->time, [=] {
timeInput->setFocusFast(); state->time->setFocusFast();
}), (*calendar)->lifetime()); }), (*calendar)->lifetime());
}); });
const auto collect = [=] { const auto collect = [=] {
const auto timeValue = timeInput->valueCurrent().split(':'); const auto timeValue = state->time->valueCurrent().split(':');
if (timeValue.size() != 2) { if (timeValue.size() != 2) {
timeInput->showError();
return 0; return 0;
} }
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt()); const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
if (!time.isValid()) { if (!time.isValid()) {
timeInput->showError();
return 0; return 0;
} }
const auto result = base::unixtime::serialize( const auto result = base::unixtime::serialize(
QDateTime(date->current(), time)); QDateTime(state->date.current(), time));
if (result <= base::unixtime::now() + kMinimalSchedule) { if (result <= base::unixtime::now() + kMinimalSchedule) {
timeInput->showError();
return 0; return 0;
} }
return result; return result;
@ -684,17 +694,27 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
const auto save = [=] { const auto save = [=] {
if (const auto result = collect()) { if (const auto result = collect()) {
done(result); done(result);
} else {
state->time->showError();
} }
}; };
timeInput->submitRequests( state->time->submitRequests(
) | rpl::start_with_next( ) | rpl::start_with_next(save, state->time->lifetime());
save,
timeInput->lifetime());
auto result = ChooseDateTimeBoxDescriptor(); auto result = ChooseDateTimeBoxDescriptor();
box->setFocusCallback([=] { timeInput->setFocusFast(); }); box->setFocusCallback([=] { state->time->setFocusFast(); });
result.submit = box->addButton(std::move(submit), save); result.submit = box->addButton(std::move(submit), save);
result.collect = collect; result.collect = [=] {
if (const auto result = collect()) {
return result;
}
state->time->showError();
return 0;
};
result.values = rpl::combine(
state->date.value(),
state->time->value()
) | rpl::map(collect);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
return result; return result;

View file

@ -16,6 +16,7 @@ class RoundButton;
struct ChooseDateTimeBoxDescriptor { struct ChooseDateTimeBoxDescriptor {
QPointer<RoundButton> submit; QPointer<RoundButton> submit;
Fn<TimeId()> collect; Fn<TimeId()> collect;
rpl::producer<TimeId> values;
}; };
ChooseDateTimeBoxDescriptor ChooseDateTimeBox( ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
@ -23,6 +24,7 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
rpl::producer<QString> title, rpl::producer<QString> title,
rpl::producer<QString> submit, rpl::producer<QString> submit,
Fn<void(TimeId)> done, Fn<void(TimeId)> done,
TimeId time); TimeId time,
rpl::producer<QString> description = nullptr);
} // namespace Ui } // namespace Ui

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/shadow.h" #include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_calls.h" #include "styles/style_calls.h"
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget. #include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
@ -126,16 +127,59 @@ void GroupCallBar::paint(Painter &p) {
const auto titleTop = st::msgReplyPadding.top(); const auto titleTop = st::msgReplyPadding.top();
const auto textTop = titleTop + st::msgServiceNameFont->height; const auto textTop = titleTop + st::msgServiceNameFont->height;
const auto width = _inner->width(); const auto width = _inner->width();
const auto &font = st::defaultMessageBar.title.font;
p.setPen(st::defaultMessageBar.textFg); p.setPen(st::defaultMessageBar.textFg);
p.setFont(st::defaultMessageBar.title.font); p.setFont(font);
p.drawTextLeft(left, titleTop, width, tr::lng_group_call_title(tr::now));
const auto available = _join->x() - left;
const auto titleWidth = font->width(_content.title);
p.drawTextLeft(
left,
titleTop,
width,
(!_content.scheduleDate
? tr::lng_group_call_title(tr::now)
: _content.title.isEmpty()
? tr::lng_group_call_scheduled_title(tr::now)
: (titleWidth > available)
? font->elided(_content.title, available)
: _content.title));
p.setPen(st::historyStatusFg); p.setPen(st::historyStatusFg);
p.setFont(st::defaultMessageBar.text.font); p.setFont(st::defaultMessageBar.text.font);
const auto when = [&] {
if (!_content.scheduleDate) {
return QString();
}
const auto parsed = base::unixtime::parse(_content.scheduleDate);
const auto date = parsed.date();
const auto time = parsed.time().toString(
QLocale::system().timeFormat(QLocale::ShortFormat));
const auto today = QDate::currentDate();
if (date == today) {
return tr::lng_group_call_starts_today(tr::now, lt_time, time);
} else if (date == today.addDays(1)) {
return tr::lng_group_call_starts_tomorrow(
tr::now,
lt_time,
time);
} else {
return tr::lng_group_call_starts_date(
tr::now,
lt_date,
langDayOfMonthFull(date),
lt_time,
time);
}
}();
p.drawTextLeft( p.drawTextLeft(
left, left,
textTop, textTop,
width, width,
(_content.count > 0 (_content.scheduleDate
? (_content.title.isEmpty()
? tr::lng_group_call_starts_short
: tr::lng_group_call_starts)(tr::now, lt_when, when)
: _content.count > 0
? tr::lng_group_call_members(tr::now, lt_count, _content.count) ? tr::lng_group_call_members(tr::now, lt_count, _content.count)
: tr::lng_group_call_no_members(tr::now))); : tr::lng_group_call_no_members(tr::now)));

View file

@ -21,6 +21,8 @@ struct GroupCallUser;
class GroupCallUserpics; class GroupCallUserpics;
struct GroupCallBarContent { struct GroupCallBarContent {
QString title;
TimeId scheduleDate = 0;
int count = 0; int count = 0;
bool shown = false; bool shown = false;
std::vector<GroupCallUser> users; std::vector<GroupCallUser> users;