From d7e90fec1a02ca9803e53f19e35bb2b1eeacaeed Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 6 Apr 2021 18:02:43 +0400 Subject: [PATCH] Add a nice countdown to scheduled voice chat panel. --- Telegram/Resources/langs/lang.strings | 9 +- Telegram/SourceFiles/calls/calls.style | 15 ++ .../SourceFiles/calls/calls_group_panel.cpp | 194 +++++++++++++++++- .../SourceFiles/calls/calls_group_panel.h | 3 + .../SourceFiles/history/history_service.cpp | 148 ++++++------- .../SourceFiles/history/history_service.h | 8 +- .../view/history_view_service_message.cpp | 19 +- .../SourceFiles/ui/chat/group_call_bar.cpp | 5 +- 8 files changed, 293 insertions(+), 108 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ddcf605b97..58fc9402e1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1098,8 +1098,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_invite_user_chat" = "the voice chat"; "lng_action_invite_users_and_one" = "{accumulated}, {user}"; "lng_action_invite_users_and_last" = "{accumulated} and {user}"; -"lng_action_group_call_started" = "{from} started {chat}"; -"lng_action_group_call_started_chat" = "a voice chat"; +"lng_action_group_call_started_group" = "{from} started a voice chat"; +"lng_action_group_call_started_channel" = "Voice chat started"; +"lng_action_group_call_scheduled_group" = "{from} scheduled a voice chat"; +"lng_action_group_call_scheduled_channel" = "Voice chat scheduled"; "lng_action_group_call_finished" = "Voice chat finished ({duration})"; "lng_action_add_user" = "{from} added {user}"; "lng_action_add_users_many" = "{from} added {users}"; @@ -2073,6 +2075,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_starts_short_today" = "Today, {time}"; +"lng_group_call_starts_short_tomorrow" = "Tomorrow, {time}"; +"lng_group_call_starts_short_date" = "{date}, {time}"; "lng_group_call_start_now" = "Start Now"; "lng_group_call_set_reminder" = "Set Reminder"; "lng_group_call_cancel_reminder" = "Cancel Reminder"; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index f768fb6f3f..49c52292dc 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -945,3 +945,18 @@ callTopBarMuteCrossLine: CrossLineAnimation { endPosition: point(26px, 23px); stroke: 2px; } + +groupCallStartsIn: FlatLabel(defaultFlatLabel) { + style: TextStyle(defaultTextStyle) { + font: font(20px semibold); + linkFont: font(20px semibold); + linkFontOver: font(20px semibold underline); + } + textFg: groupCallMembersFg; +} +groupCallScheduledBodyHeight: 200px; +groupCallStartsWhen: groupCallStartsIn; +groupCallStartsInTop: 10px; +groupCallStartsWhenTop: 160px; +groupCallCountdownFont: font(64px semibold); +groupCallCountdownTop: 52px; diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp index cd86348a1d..5f95e7c940 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/dropdown_menu.h" #include "ui/widgets/input_fields.h" +#include "ui/chat/group_call_bar.h" #include "ui/layers/layer_manager.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" @@ -41,6 +42,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_participants_box.h" #include "boxes/peer_lists_box.h" #include "boxes/confirm_box.h" +#include "base/unixtime.h" +#include "base/timer_rpl.h" #include "app.h" #include "apiwrap.h" // api().kickParticipant. #include "styles/style_calls.h" @@ -116,6 +119,122 @@ private: }; +[[nodiscard]] rpl::producer StartsWhenText( + rpl::producer date) { + return std::move( + date + ) | rpl::map([](TimeId date) -> rpl::producer { + const auto parsedDate = base::unixtime::parse(date); + const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0)); + const auto previousDay = QDateTime( + parsedDate.date().addDays(-1), + QTime(0, 0)); + const auto now = QDateTime::currentDateTime(); + const auto kDay = int64(24 * 60 * 60); + const auto tillTomorrow = int64(now.secsTo(previousDay)); + const auto tillToday = tillTomorrow + kDay; + const auto tillAfter = tillToday + kDay; + + const auto time = parsedDate.time().toString( + QLocale::system().timeFormat(QLocale::ShortFormat)); + auto exact = tr::lng_group_call_starts_short_date( + lt_date, + rpl::single(langDayOfMonthFull(dateDay.date())), + lt_time, + rpl::single(time)); + auto tomorrow = tr::lng_group_call_starts_short_tomorrow( + lt_time, + rpl::single(time)); + auto today = tr::lng_group_call_starts_short_today( + lt_time, + rpl::single(time)); + + auto todayAndAfter = rpl::single( + std::move(today) + ) | rpl::then(base::timer_once( + std::min(tillAfter, kDay) * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(exact); + })) | rpl::flatten_latest(); + + auto tomorrowAndAfter = rpl::single( + std::move(tomorrow) + ) | rpl::then(base::timer_once( + std::min(tillToday, kDay) * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(todayAndAfter); + })) | rpl::flatten_latest(); + + auto full = rpl::single( + rpl::duplicate(exact) + ) | rpl::then(base::timer_once( + tillTomorrow * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(tomorrowAndAfter); + })) | rpl::flatten_latest(); + + if (tillTomorrow > 0) { + return std::move(full); + } else if (tillToday > 0) { + return std::move(tomorrowAndAfter); + } else if (tillAfter > 0) { + return std::move(todayAndAfter); + } else { + return std::move(exact); + } + }) | rpl::flatten_latest(); +} + +[[nodiscard]] object_ptr CreateGradientLabel( + QWidget *parent, + rpl::producer text) { + struct State { + QBrush brush; + QPainterPath path; + }; + auto result = object_ptr(parent); + const auto raw = result.data(); + const auto state = raw->lifetime().make_state(); + + std::move( + text + ) | rpl::start_with_next([=](const QString &text) { + state->path = QPainterPath(); + const auto &font = st::groupCallCountdownFont; + state->path.addText(0, font->ascent, font->f, text); + const auto width = font->width(text); + raw->resize(width, font->height); + auto gradient = QLinearGradient(QPoint(width, 0), QPoint()); + gradient.setStops(QGradientStops{ + { 0.0, st::groupCallForceMutedBar1->c }, + { .7, st::groupCallForceMutedBar2->c }, + { 1.0, st::groupCallForceMutedBar3->c } + }); + state->brush = QBrush(std::move(gradient)); + raw->update(); + }, raw->lifetime()); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + const auto skip = st::groupCallWidth / 20; + const auto available = parent->width() - 2 * skip; + const auto full = raw->width(); + if (available > 0 && full > available) { + const auto scale = available / float64(full); + const auto shift = raw->rect().center(); + p.translate(shift); + p.scale(scale, scale); + p.translate(-shift); + } + p.setPen(Qt::NoPen); + p.setBrush(state->brush); + p.drawPath(state->path); + }, raw->lifetime()); + return result; +} + [[nodiscard]] object_ptr CreateSectionSubtitle( QWidget *parent, rpl::producer text) { @@ -451,18 +570,23 @@ void Panel::initControls() { }); _settings->setText(tr::lng_group_call_settings()); - const auto scheduled = (_call->scheduleDate() != 0); - _hangup->setText(scheduled + const auto scheduleDate = _call->scheduleDate(); + _hangup->setText(scheduleDate ? tr::lng_group_call_close() : tr::lng_group_call_leave()); - if (scheduled) { - _call->real( + if (scheduleDate) { + auto changes = _call->real( ) | rpl::map([=](not_null real) { return real->scheduleDateValue(); - }) | rpl::flatten_latest() | rpl::filter([](TimeId date) { + }) | rpl::flatten_latest(); + setupScheduledLabels(rpl::single( + scheduleDate + ) | rpl::then(rpl::duplicate(changes))); + std::move(changes) | rpl::filter([](TimeId date) { return (date == 0); }) | rpl::take(1) | rpl::start_with_next([=] { _hangup->setText(tr::lng_group_call_leave()); + setupMembers(); }, _callLifetime); } @@ -553,8 +677,66 @@ void Panel::setupRealMuteButtonState(not_null real) { }, _callLifetime); } +void Panel::setupScheduledLabels(rpl::producer date) { + using namespace rpl::mappers; + _startsIn.create( + widget(), + tr::lng_group_call_starts_in(), + st::groupCallStartsIn); + date = std::move(date) | rpl::take_while(_1 != 0); + _startsWhen.create( + widget(), + StartsWhenText(rpl::duplicate(date)), + st::groupCallStartsWhen); + _countdown = CreateGradientLabel(widget(), std::move( + date + ) | rpl::map([=](TimeId date) { + _countdownData = std::make_shared(date); + return _countdownData->text(); + }) | rpl::flatten_latest()); + + const auto top = [=] { + const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; + const auto membersTop = st::groupCallMembersTop; + const auto height = st::groupCallScheduledBodyHeight; + return (membersTop + (muteTop - membersTop - height) / 2); + }; + rpl::combine( + widget()->sizeValue(), + _startsIn->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _startsIn->move( + (size.width() - width) / 2, + top() + st::groupCallStartsInTop); + }, _startsIn->lifetime()); + + rpl::combine( + widget()->sizeValue(), + _startsWhen->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _startsWhen->move( + (size.width() - width) / 2, + top() + st::groupCallStartsWhenTop); + }, _startsWhen->lifetime()); + + rpl::combine( + widget()->sizeValue(), + _countdown->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _countdown->move( + (size.width() - width) / 2, + top() + st::groupCallCountdownTop); + }, _startsWhen->lifetime()); +} + void Panel::setupMembers() { - Expects(!_members); + if (_members) { + return; + } + + _startsIn.destroy(); + _countdown.destroy(); + _startsWhen.destroy(); _members.create(widget(), _call); diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h index 3f8b374783..586aa712b9 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -38,6 +38,7 @@ class Window; class ScrollArea; class GenericBox; class LayerManager; +class GroupCallScheduledLeft; namespace Platform { class TitleControls; } // namespace Platform @@ -75,6 +76,7 @@ private: void initControls(); void initLayout(); void initGeometry(); + void setupScheduledLabels(rpl::producer date); void setupMembers(); void setupJoinAsChangedToasts(); void setupTitleChangedToasts(); @@ -124,6 +126,7 @@ private: object_ptr _members = { nullptr }; object_ptr _startsIn = { nullptr }; object_ptr _countdown = { nullptr }; + std::shared_ptr _countdownData; object_ptr _startsWhen = { nullptr }; ChooseJoinAsProcess _joinAsProcess; diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index cc666053cd..ce923c8418 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -72,17 +72,6 @@ constexpr auto kPinnedMessageTextLimit = 16; ); } -[[nodiscard]] std::optional PeerHasThisCall( - not_null peer, - uint64 id) { - const auto call = peer->groupCall(); - return call - ? std::make_optional(call->id() == id) - : PeerCallKnown(peer) - ? std::make_optional(false) - : std::nullopt; -} - [[nodiscard]] uint64 CallIdFromInput(const MTPInputGroupCall &data) { return data.match([&](const MTPDinputGroupCall &data) { return data.vid().v; @@ -348,14 +337,30 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { auto prepareGroupCall = [this](const MTPDmessageActionGroupCall &action) { if (const auto duration = action.vduration()) { - return prepareDiscardedCallText(duration->v); + const auto seconds = duration->v; + const auto days = seconds / 86400; + const auto hours = seconds / 3600; + const auto minutes = seconds / 60; + auto text = (days > 1) + ? tr::lng_group_call_duration_days(tr::now, lt_count, days) + : (hours > 1) + ? tr::lng_group_call_duration_hours(tr::now, lt_count, hours) + : (minutes > 1) + ? tr::lng_group_call_duration_minutes(tr::now, lt_count, minutes) + : tr::lng_group_call_duration_seconds(tr::now, lt_count, seconds); + return PreparedText{ tr::lng_action_group_call_finished(tr::now, lt_duration, text) }; } - const auto callId = CallIdFromInput(action.vcall()); - const auto peer = history()->peer; - const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false) - ? callId - : 0; - return prepareStartedCallText(linkCallId); + auto result = PreparedText{}; + if (history()->peer->isBroadcast()) { + result.text = tr::lng_action_group_call_started_channel(tr::now); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_group_call_started_group( + tr::now, + lt_from, + fromLinkText()); + } + return result; }; auto prepareInviteToGroupCall = [this](const MTPDmessageActionInviteToGroupCall &action) { @@ -406,12 +411,17 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { }; auto prepareGroupCallScheduled = [this](const MTPDmessageActionGroupCallScheduled &action) { - const auto callId = CallIdFromInput(action.vcall()); - const auto peer = history()->peer; - const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false) - ? callId - : 0; - return prepareStartedCallText(linkCallId); + auto result = PreparedText{}; + if (history()->peer->isBroadcast()) { + result.text = tr::lng_action_group_call_scheduled_channel(tr::now); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_group_call_scheduled_group( + tr::now, + lt_from, + fromLinkText()); + } + return result; }; const auto messageText = action.match([&]( @@ -577,41 +587,6 @@ bool HistoryService::updateDependent(bool force) { return (dependent->msg || !dependent->msgId); } -HistoryService::PreparedText HistoryService::prepareDiscardedCallText( - int duration) { - const auto seconds = duration; - const auto days = seconds / 86400; - const auto hours = seconds / 3600; - const auto minutes = seconds / 60; - auto text = (days > 1) - ? tr::lng_group_call_duration_days(tr::now, lt_count, days) - : (hours > 1) - ? tr::lng_group_call_duration_hours(tr::now, lt_count, hours) - : (minutes > 1) - ? tr::lng_group_call_duration_minutes(tr::now, lt_count, minutes) - : tr::lng_group_call_duration_seconds(tr::now, lt_count, seconds); - return PreparedText{ tr::lng_action_group_call_finished(tr::now, lt_duration, text) }; -} - -HistoryService::PreparedText HistoryService::prepareStartedCallText( - uint64 linkCallId) { - auto result = PreparedText{}; - result.links.push_back(fromLink()); - auto chatText = tr::lng_action_group_call_started_chat(tr::now); - if (linkCallId) { - const auto peer = history()->peer; - result.links.push_back(GroupCallClickHandler(peer, linkCallId)); - chatText = textcmdLink(2, chatText); - } - result.text = tr::lng_action_group_call_started( - tr::now, - lt_from, - fromLinkText(), - lt_chat, - chatText); - return result; -} - HistoryService::PreparedText HistoryService::prepareInvitedToCallText( const QVector &users, uint64 linkCallId) { @@ -973,11 +948,12 @@ void HistoryService::createFromMtp(const MTPDmessage &message) { } void HistoryService::createFromMtp(const MTPDmessageService &message) { - if (message.vaction().type() == mtpc_messageActionGameScore) { + const auto type = message.vaction().type(); + if (type == mtpc_messageActionGameScore) { const auto &data = message.vaction().c_messageActionGameScore(); UpdateComponents(HistoryServiceGameScore::Bit()); Get()->score = data.vscore().v; - } else if (message.vaction().type() == mtpc_messageActionPaymentSent) { + } else if (type == mtpc_messageActionPaymentSent) { const auto &data = message.vaction().c_messageActionPaymentSent(); UpdateComponents(HistoryServicePayment::Bit()); const auto amount = data.vtotal_amount().v; @@ -998,36 +974,24 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { crl::guard(weak, [=] { weak->window().activate(); })); } }); - } else if (message.vaction().type() == mtpc_messageActionGroupCall) { - const auto &data = message.vaction().c_messageActionGroupCall(); - if (data.vduration()) { + } else if (type == mtpc_messageActionGroupCall + || type == mtpc_messageActionGroupCallScheduled) { + const auto started = (type == mtpc_messageActionGroupCall); + const auto &callData = started + ? message.vaction().c_messageActionGroupCall().vcall() + : message.vaction().c_messageActionGroupCallScheduled().vcall(); + const auto duration = started + ? message.vaction().c_messageActionGroupCall().vduration() + : tl::conditional(); + if (duration) { RemoveComponents(HistoryServiceOngoingCall::Bit()); } else { UpdateComponents(HistoryServiceOngoingCall::Bit()); const auto call = Get(); - const auto id = CallIdFromInput(data.vcall()); - call->lifetime.destroy(); - - const auto peer = history()->peer; - const auto has = PeerHasThisCall(peer, id); - if (!has.has_value()) { - PeerHasThisCallValue( - peer, - id - ) | rpl::start_with_next([=](bool has) { - updateText(prepareStartedCallText(has ? id : 0)); - }, call->lifetime); - } else if (*has) { - PeerHasThisCallValue( - peer, - id - ) | rpl::skip(1) | rpl::start_with_next([=](bool has) { - Assert(!has); - updateText(prepareStartedCallText(0)); - }, call->lifetime); - } + call->id = CallIdFromInput(callData); + call->link = GroupCallClickHandler(history()->peer, call->id); } - } else if (message.vaction().type() == mtpc_messageActionInviteToGroupCall) { + } else if (type == mtpc_messageActionInviteToGroupCall) { const auto &data = message.vaction().c_messageActionInviteToGroupCall(); const auto id = CallIdFromInput(data.vcall()); const auto peer = history()->peer; @@ -1044,6 +1008,7 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { } else { UpdateComponents(HistoryServiceOngoingCall::Bit()); const auto call = Get(); + call->id = id; call->lifetime.destroy(); const auto users = data.vusers().v; @@ -1207,3 +1172,14 @@ not_null GenerateJoinedMessage( GenerateJoinedText(history, inviter), flags); } + +std::optional PeerHasThisCall( + not_null peer, + uint64 id) { + const auto call = peer->groupCall(); + return call + ? std::make_optional(call->id() == id) + : PeerCallKnown(peer) + ? std::make_optional(false) + : std::nullopt; +} diff --git a/Telegram/SourceFiles/history/history_service.h b/Telegram/SourceFiles/history/history_service.h index e7bb8ee706..88dd3b7959 100644 --- a/Telegram/SourceFiles/history/history_service.h +++ b/Telegram/SourceFiles/history/history_service.h @@ -52,6 +52,7 @@ struct HistoryServiceSelfDestruct struct HistoryServiceOngoingCall : public RuntimeComponent { uint64 id = 0; + ClickHandlerPtr link; rpl::lifetime lifetime; }; @@ -160,8 +161,6 @@ private: PreparedText preparePinnedText(); PreparedText prepareGameScoreText(); PreparedText preparePaymentSentText(); - PreparedText prepareDiscardedCallText(int duration); - PreparedText prepareStartedCallText(uint64 linkCallId); PreparedText prepareInvitedToCallText( const QVector &users, uint64 linkCallId); @@ -170,8 +169,11 @@ private: }; -not_null GenerateJoinedMessage( +[[nodiscard]] not_null GenerateJoinedMessage( not_null history, TimeId inviteDate, not_null inviter, MTPDmessage::Flags flags); +[[nodiscard]] std::optional PeerHasThisCall( + not_null peer, + uint64 id); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 693808180d..920f21f37d 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -513,17 +513,18 @@ TextState Service::textState(QPoint point, StateRequest request) const { point - trect.topLeft(), trect.width(), textRequest)); - if (auto gamescore = item->Get()) { - if (!result.link - && result.cursor == CursorState::Text - && g.contains(point)) { + if (!result.link + && result.cursor == CursorState::Text + && g.contains(point)) { + if (const auto gamescore = item->Get()) { result.link = gamescore->lnk; - } - } else if (auto payment = item->Get()) { - if (!result.link - && result.cursor == CursorState::Text - && g.contains(point)) { + } else if (const auto payment = item->Get()) { result.link = payment->invoiceLink; + } else if (const auto call = item->Get()) { + const auto peer = history()->peer; + if (PeerHasThisCall(peer, call->id).value_or(false)) { + result.link = call->link; + } } } } else if (media) { diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index f405a06b59..5869750235 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -152,10 +152,10 @@ void GroupCallBar::refreshOpenBrush() { if (_openBrushForWidth == width) { return; } - auto gradient = QLinearGradient(QPoint(width, 0), QPoint(-width, 0)); + auto gradient = QLinearGradient(QPoint(width, 0), QPoint(0, 0)); gradient.setStops(QGradientStops{ { 0.0, st::groupCallForceMutedBar1->c }, - { .35, st::groupCallForceMutedBar2->c }, + { .7, st::groupCallForceMutedBar2->c }, { 1.0, st::groupCallForceMutedBar3->c } }); _openBrushOverride = QBrush(std::move(gradient)); @@ -169,6 +169,7 @@ void GroupCallBar::refreshScheduledProcess() { if (_scheduledProcess) { _scheduledProcess = nullptr; _open = nullptr; + _openBrushForWidth = 0; _join = std::make_unique( _inner.get(), tr::lng_group_call_join(),