Add a nice countdown to scheduled voice chat panel.

This commit is contained in:
John Preston 2021-04-06 18:02:43 +04:00
parent 66e7f05df1
commit d7e90fec1a
8 changed files with 293 additions and 108 deletions

View File

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

View File

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

View File

@ -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<QString> StartsWhenText(
rpl::producer<TimeId> date) {
return std::move(
date
) | rpl::map([](TimeId date) -> rpl::producer<QString> {
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<Ui::RpWidget> CreateGradientLabel(
QWidget *parent,
rpl::producer<QString> text) {
struct State {
QBrush brush;
QPainterPath path;
};
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto state = raw->lifetime().make_state<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<Ui::RpWidget> CreateSectionSubtitle(
QWidget *parent,
rpl::producer<QString> 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<Data::GroupCall*> 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<Data::GroupCall*> real) {
}, _callLifetime);
}
void Panel::setupScheduledLabels(rpl::producer<TimeId> 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<Ui::GroupCallScheduledLeft>(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);

View File

@ -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<TimeId> date);
void setupMembers();
void setupJoinAsChangedToasts();
void setupTitleChangedToasts();
@ -124,6 +126,7 @@ private:
object_ptr<Members> _members = { nullptr };
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
object_ptr<Ui::RpWidget> _countdown = { nullptr };
std::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
ChooseJoinAsProcess _joinAsProcess;

View File

@ -72,17 +72,6 @@ constexpr auto kPinnedMessageTextLimit = 16;
);
}
[[nodiscard]] std::optional<bool> PeerHasThisCall(
not_null<PeerData*> 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<MTPint> &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<HistoryServiceGameScore>()->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<MTPint>();
if (duration) {
RemoveComponents(HistoryServiceOngoingCall::Bit());
} else {
UpdateComponents(HistoryServiceOngoingCall::Bit());
const auto call = Get<HistoryServiceOngoingCall>();
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<HistoryServiceOngoingCall>();
call->id = id;
call->lifetime.destroy();
const auto users = data.vusers().v;
@ -1207,3 +1172,14 @@ not_null<HistoryService*> GenerateJoinedMessage(
GenerateJoinedText(history, inviter),
flags);
}
std::optional<bool> PeerHasThisCall(
not_null<PeerData*> peer,
uint64 id) {
const auto call = peer->groupCall();
return call
? std::make_optional(call->id() == id)
: PeerCallKnown(peer)
? std::make_optional(false)
: std::nullopt;
}

View File

@ -52,6 +52,7 @@ struct HistoryServiceSelfDestruct
struct HistoryServiceOngoingCall
: public RuntimeComponent<HistoryServiceOngoingCall, HistoryItem> {
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<MTPint> &users,
uint64 linkCallId);
@ -170,8 +169,11 @@ private:
};
not_null<HistoryService*> GenerateJoinedMessage(
[[nodiscard]] not_null<HistoryService*> GenerateJoinedMessage(
not_null<History*> history,
TimeId inviteDate,
not_null<UserData*> inviter,
MTPDmessage::Flags flags);
[[nodiscard]] std::optional<bool> PeerHasThisCall(
not_null<PeerData*> peer,
uint64 id);

View File

@ -513,17 +513,18 @@ TextState Service::textState(QPoint point, StateRequest request) const {
point - trect.topLeft(),
trect.width(),
textRequest));
if (auto gamescore = item->Get<HistoryServiceGameScore>()) {
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<HistoryServiceGameScore>()) {
result.link = gamescore->lnk;
}
} else if (auto payment = item->Get<HistoryServicePayment>()) {
if (!result.link
&& result.cursor == CursorState::Text
&& g.contains(point)) {
} else if (const auto payment = item->Get<HistoryServicePayment>()) {
result.link = payment->invoiceLink;
} else if (const auto call = item->Get<HistoryServiceOngoingCall>()) {
const auto peer = history()->peer;
if (PeerHasThisCall(peer, call->id).value_or(false)) {
result.link = call->link;
}
}
}
} else if (media) {

View File

@ -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<RoundButton>(
_inner.get(),
tr::lng_group_call_join(),