From 15d17c8b0e7c1a66aed0d2fc94772ccd8c56924b Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Apr 2021 14:29:03 +0400 Subject: [PATCH] Add creating of a scheduled group call. --- Telegram/Resources/langs/lang.strings | 15 + .../calls/calls_choose_join_as.cpp | 88 +++++- .../SourceFiles/calls/calls_group_call.cpp | 37 ++- Telegram/SourceFiles/calls/calls_group_call.h | 7 +- .../SourceFiles/calls/calls_group_common.h | 1 + .../SourceFiles/calls/calls_group_panel.cpp | 286 +++++++++--------- .../SourceFiles/calls/calls_group_panel.h | 9 +- Telegram/SourceFiles/data/data_group_call.cpp | 1 + Telegram/SourceFiles/data/data_group_call.h | 10 + .../view/history_view_group_call_tracker.cpp | 14 +- Telegram/SourceFiles/mainwidget.cpp | 1 + .../SourceFiles/ui/boxes/choose_date_time.cpp | 108 ++++--- .../SourceFiles/ui/boxes/choose_date_time.h | 4 +- .../SourceFiles/ui/chat/group_call_bar.cpp | 50 ++- Telegram/SourceFiles/ui/chat/group_call_bar.h | 2 + 15 files changed, 427 insertions(+), 206 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 168215d8f6..b34f39161a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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}**"; diff --git a/Telegram/SourceFiles/calls/calls_choose_join_as.cpp b/Telegram/SourceFiles/calls/calls_choose_join_as.cpp index aa02d788b6..fc57655151 100644 --- a/Telegram/SourceFiles/calls/calls_choose_join_as.cpp +++ b/Telegram/SourceFiles/calls/calls_choose_join_as.cpp @@ -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 ListController::selected() const { return _selected; } +void ScheduleGroupCallBox( + not_null box, + const JoinInfo &info, + Fn 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>(); + 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 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( 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( + 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(); diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index 8c0d7ff2a9..36192d0bc7 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -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()), 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())); diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h index 09781aab9f..337542fd82 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.h +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -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 _mySsrcs; mtpRequestId _createRequestId = 0; mtpRequestId _updateMuteRequestId = 0; diff --git a/Telegram/SourceFiles/calls/calls_group_common.h b/Telegram/SourceFiles/calls/calls_group_common.h index 371f290b03..0cfa4d9fe5 100644 --- a/Telegram/SourceFiles/calls/calls_group_common.h +++ b/Telegram/SourceFiles/calls/calls_group_common.h @@ -44,6 +44,7 @@ struct JoinInfo { not_null joinAs; std::vector> possibleJoinAs; QString joinHash; + TimeId scheduleDate = 0; }; } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp index f6e36c9919..c3b6b57d9d 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -259,7 +259,7 @@ Panel::Panel(not_null call) _window->body(), st::groupCallTitle)) #endif // !Q_OS_MAC -, _members(widget(), call) +, _scheduleDate(call->scheduleDate()) , _settings(widget(), st::groupCallSettings) , _mute(std::make_unique( widget(), @@ -286,30 +286,7 @@ Panel::Panel(not_null 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 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 real) { @@ -393,11 +370,9 @@ void Panel::initWindow() { } else if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) { if (static_cast(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 participantPeer) { - kickParticipant(participantPeer); - }, _callLifetime); - - const auto showBox = [=](object_ptr 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 participantPeer) { + kickParticipant(participantPeer); + }, _callLifetime); + + const auto showBox = [=](object_ptr 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 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 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 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 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> &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); diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h index bc966b6056..6d0c04f6c3 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -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 channel); void subscribeToPeerChanges(); - GroupCall *_call = nullptr; + const not_null _call; not_null _peer; const std::unique_ptr _window; @@ -118,8 +120,9 @@ private: object_ptr _menuToggle = { nullptr }; object_ptr _menu = { nullptr }; object_ptr _joinAsToggle = { nullptr }; - object_ptr _members; + object_ptr _members = { nullptr }; rpl::variable _titleText; + rpl::variable _scheduleDate; ChooseJoinAsProcess _joinAsProcess; object_ptr _settings; diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 0c9fd8947b..33ba9085e6 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -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()); } diff --git a/Telegram/SourceFiles/data/data_group_call.h b/Telegram/SourceFiles/data/data_group_call.h index 8826786104..71b77fda4a 100644 --- a/Telegram/SourceFiles/data/data_group_call.h +++ b/Telegram/SourceFiles/data/data_group_call.h @@ -63,6 +63,15 @@ public: [[nodiscard]] rpl::producer recordStartDateChanges() const { return _recordStartDate.changes(); } + [[nodiscard]] TimeId scheduleDate() const { + return _scheduleDate.current(); + } + [[nodiscard]] rpl::producer scheduleDateValue() const { + return _scheduleDate.value(); + } + [[nodiscard]] rpl::producer scheduleDateChanges() const { + return _scheduleDate.changes(); + } void setPeer(not_null peer); @@ -163,6 +172,7 @@ private: int _serverParticipantsCount = 0; rpl::variable _fullCount = 0; rpl::variable _recordStartDate = 0; + rpl::variable _scheduleDate = 0; base::flat_map _unknownSpokenSsrcs; base::flat_map _unknownSpokenPeerIds; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp index fe46bc4e83..27929ee530 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp @@ -330,10 +330,18 @@ rpl::producer 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); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 5e0de5420d..fd3b0f86a8 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -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) { diff --git a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp index c753c76375..5448f3eb2f 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp @@ -572,36 +572,49 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox( rpl::producer title, rpl::producer submit, Fn done, - TimeId time) { + TimeId time, + rpl::producer description) { + struct State { + rpl::variable date; + not_null day; + not_null time; + not_null at; + }; box->setTitle(std::move(title)); box->setWidth(st::boxWideWidth); - const auto date = CreateChild>( - box.get(), - base::unixtime::parse(time).date()); const auto content = box->addRow( object_ptr(box, st::scheduleHeight)); - const auto dayInput = CreateChild( - content, - st::scheduleDateField); - const auto timeInput = CreateChild( - content, - TimeString(time)); - const auto at = CreateChild( - content, - tr::lng_schedule_at(), - st::scheduleAtLabel); + if (description) { + box->addRow(object_ptr( + box, + std::move(description), + st::boxLabel)); + } + const auto state = box->lifetime().make_state(State{ + .date = base::unixtime::parse(time).date(), + .day = CreateChild( + content, + st::scheduleDateField), + .time = CreateChild( + content, + TimeString(time)), + .at = CreateChild( + 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 event) { if (event->type() == QEvent::Wheel) { const auto e = static_cast(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>(); - 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 box) { @@ -652,31 +665,28 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox( box->setMaxDate(maxDate); }; *calendar = box->getDelegate()->show(Box( - 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; diff --git a/Telegram/SourceFiles/ui/boxes/choose_date_time.h b/Telegram/SourceFiles/ui/boxes/choose_date_time.h index 87b42c28e5..6bfb72b7af 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_date_time.h +++ b/Telegram/SourceFiles/ui/boxes/choose_date_time.h @@ -16,6 +16,7 @@ class RoundButton; struct ChooseDateTimeBoxDescriptor { QPointer submit; Fn collect; + rpl::producer values; }; ChooseDateTimeBoxDescriptor ChooseDateTimeBox( @@ -23,6 +24,7 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox( rpl::producer title, rpl::producer submit, Fn done, - TimeId time); + TimeId time, + rpl::producer description = nullptr); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index b74142371d..b7a3150526 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -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))); diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.h b/Telegram/SourceFiles/ui/chat/group_call_bar.h index f085a2d043..e1074dac6c 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.h +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.h @@ -21,6 +21,8 @@ struct GroupCallUser; class GroupCallUserpics; struct GroupCallBarContent { + QString title; + TimeId scheduleDate = 0; int count = 0; bool shown = false; std::vector users;