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_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_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_edit_title" = "Edit voice chat title";
"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 "apiwrap.h"
#include "ui/layers/generic_box.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/text/text_utilities.h"
#include "boxes/peer_list_box.h"
#include "boxes/confirm_box.h"
#include "base/unixtime.h"
#include "base/timer_rpl.h"
#include "styles/style_boxes.h"
#include "styles/style_calls.h"
namespace Calls::Group {
namespace {
constexpr auto kDefaultScheduleDuration = 60 * TimeId(60);
constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
using Context = ChooseJoinAsProcess::Context;
class ListController : public PeerListController {
@ -109,6 +115,60 @@ not_null<PeerData*> ListController::selected() const {
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(
not_null<Ui::GenericBox*> box,
Context context,
@ -124,12 +184,13 @@ void ChooseJoinAsBox(
}
Unexpected("Context in ChooseJoinAsBox.");
}());
const auto &labelSt = (context == Context::Switch)
? st::groupCallJoinAsLabel
: st::confirmPhoneAboutLabel;
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_group_call_join_as_about(),
(context == Context::Switch
? st::groupCallJoinAsLabel
: st::confirmPhoneAboutLabel)));
labelSt));
auto &lifetime = box->lifetime();
const auto delegate = lifetime.make_state<
@ -155,6 +216,27 @@ void ChooseJoinAsBox(
auto next = (context == Context::Switch)
? tr::lng_settings_save()
: 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), [=] {
auto copy = info;
copy.joinAs = controller->selected();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,6 +63,15 @@ public:
[[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const {
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);
@ -163,6 +172,7 @@ private:
int _serverParticipantsCount = 0;
rpl::variable<int> _fullCount = 0;
rpl::variable<TimeId> _recordStartDate = 0;
rpl::variable<TimeId> _scheduleDate = 0;
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds;

View File

@ -330,10 +330,18 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
RegenerateUserpics(state, call, userpicSize);
call->fullCountValue(
) | rpl::start_with_next([=](int count) {
rpl::combine(
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.shown = (count > 0);
state->current.shown = (count > 0) || (scheduleDate != 0);
consumer.put_next_copy(state->current);
}, lifetime);

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "styles/style_chat.h"
#include "styles/style_calls.h"
#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 textTop = titleTop + st::msgServiceNameFont->height;
const auto width = _inner->width();
const auto &font = st::defaultMessageBar.title.font;
p.setPen(st::defaultMessageBar.textFg);
p.setFont(st::defaultMessageBar.title.font);
p.drawTextLeft(left, titleTop, width, tr::lng_group_call_title(tr::now));
p.setFont(font);
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.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(
left,
textTop,
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_no_members(tr::now)));

View File

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