Improve scheduled voice chat top bar design.

This commit is contained in:
John Preston 2021-04-06 13:59:14 +04:00
parent 088fda4ed8
commit 66e7f05df1
12 changed files with 260 additions and 55 deletions

View File

@ -1992,6 +1992,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_leave" = "Leave";
"lng_group_call_leave_title" = "Leave voice chat";
"lng_group_call_leave_sure" = "Are you sure you want to leave this voice chat?";
"lng_group_call_close" = "Close";
"lng_group_call_close_sure" = "Voice chat is scheduled. You can cancel it or just close this panel.";
"lng_group_call_also_cancel" = "Cancel voice chat";
"lng_group_call_leave_to_other_sure" = "Do you want to leave your active voice chat and join a voice chat in this group?";
"lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?";
"lng_group_call_create_sure_channel" = "Are you sure you want to start a voice chat in this channel as your personal account?";
@ -2022,6 +2025,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_copy_speaker_link" = "Copy Speaker Link";
"lng_group_call_copy_listener_link" = "Copy Listener Link";
"lng_group_call_end" = "End Voice Chat";
"lng_group_call_cancel" = "Cancel Voice Chat";
"lng_group_call_join" = "Join";
"lng_group_call_join_confirm" = "Do you want to join the voice chat {chat}?";
"lng_group_call_invite_done_user" = "You invited {user} to the voice chat.";
@ -2069,6 +2073,7 @@ 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_start_now" = "Start Now";
"lng_group_call_set_reminder" = "Set Reminder";
"lng_group_call_cancel_reminder" = "Cancel Reminder";
"lng_group_call_join_as_personal" = "personal account";

View File

@ -720,6 +720,11 @@ groupCallTopBarJoin: RoundButton(defaultActiveButton) {
height: 26px;
textTop: 4px;
}
groupCallTopBarOpen: RoundButton(groupCallTopBarJoin) {
ripple: RippleAnimation(defaultRippleAnimation) {
color: shadowFg;
}
}
groupCallBox: Box(defaultBox) {
button: RoundButton(defaultBoxButton) {
textFg: groupCallActiveFg;

View File

@ -152,17 +152,17 @@ void ScheduleGroupCallBox(
const auto now = base::unixtime::now();
const auto duration = (date - now);
if (duration >= 24 * 60 * 60) {
return tr::lng_signin_reset_days(
return tr::lng_group_call_duration_days(
tr::now,
lt_count,
duration / (24 * 60 * 60));
} else if (duration >= 60 * 60) {
return tr::lng_signin_reset_hours(
return tr::lng_group_call_duration_hours(
tr::now,
lt_count,
duration / (60 * 60));
}
return tr::lng_signin_reset_minutes(
return tr::lng_group_call_duration_minutes(
tr::now,
lt_count,
std::max(duration / 60, 1));

View File

@ -234,9 +234,9 @@ GroupCall::GroupCall(
return not_null{ real };
}) | rpl::take(
1
) | rpl::start_with_next([=](not_null<Data::GroupCall*> call) {
) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
subscribeToReal(real);
_realChanges.fire_copy(call);
_realChanges.fire_copy(real);
}, _lifetime);
}
if (_id) {

View File

@ -514,15 +514,6 @@ base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
std::move(callback));
}
base::unique_qptr<Ui::Menu::ItemBase> MakeFinishAction(
not_null<Ui::Menu::Menu*> menu,
Fn<void()> callback) {
return MakeAttentionAction(
menu,
tr::lng_group_call_end(tr::now),
std::move(callback));
}
} // namespace
void LeaveBox(
@ -530,16 +521,25 @@ void LeaveBox(
not_null<GroupCall*> call,
bool discardChecked,
BoxContext context) {
const auto scheduled = (call->scheduleDate() != 0);
if (!scheduled) {
box->setTitle(tr::lng_group_call_leave_title());
}
const auto inCall = (context == BoxContext::GroupCallPanel);
box->addRow(object_ptr<Ui::FlatLabel>(
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_group_call_leave_sure(),
(inCall ? st::groupCallBoxLabel : st::boxLabel)));
(scheduled
? tr::lng_group_call_close_sure()
: tr::lng_group_call_leave_sure()),
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
scheduled ? st::boxPadding : st::boxRowPadding);
const auto discard = call->peer()->canManageGroupCall()
? box->addRow(object_ptr<Ui::Checkbox>(
box.get(),
tr::lng_group_call_end(),
(scheduled
? tr::lng_group_call_also_cancel()
: tr::lng_group_call_also_end()),
discardChecked,
(inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),
(inCall ? st::groupCallCheck : st::defaultCheck)),
@ -550,7 +550,10 @@ void LeaveBox(
st::boxRowPadding.bottom()))
: nullptr;
const auto weak = base::make_weak(call.get());
box->addButton(tr::lng_group_call_leave(), [=] {
auto label = scheduled
? tr::lng_group_call_close()
: tr::lng_group_call_leave();
box->addButton(std::move(label), [=] {
const auto discardCall = (discard && discard->checked());
box->closeBox();
@ -603,7 +606,8 @@ void FillMenu(
const auto addEditJoinAs = call->showChooseJoinAs();
const auto addEditTitle = peer->canManageGroupCall();
const auto addEditRecording = peer->canManageGroupCall();
const auto addEditRecording = peer->canManageGroupCall()
&& !real->scheduleDate();
if (addEditJoinAs) {
menu->addAction(MakeJoinAsAction(
menu->menu(),
@ -660,7 +664,7 @@ void FillMenu(
showBox(Box(SettingsBox, strong));
}
});
menu->addAction(MakeFinishAction(menu->menu(), [=] {
const auto finish = [=] {
if (const auto strong = weak.get()) {
showBox(Box(
LeaveBox,
@ -668,7 +672,13 @@ void FillMenu(
true,
BoxContext::GroupCallPanel));
}
}));
};
menu->addAction(MakeAttentionAction(
menu->menu(),
(real->scheduleDate()
? tr::lng_group_call_cancel(tr::now)
: tr::lng_group_call_end(tr::now)),
finish));
}
base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(

View File

@ -266,7 +266,7 @@ Panel::Panel(not_null<GroupCall*> call)
Core::App().appDeactivatedValue(),
Ui::CallMuteButtonState{
.text = (_call->scheduleDate()
? "Start Now" // #TODO voice chats
? tr::lng_group_call_start_now(tr::now)
: tr::lng_group_call_connecting(tr::now)),
.type = (_call->scheduleDate()
? Ui::CallMuteButtonType::ScheduledCanStart
@ -451,7 +451,20 @@ void Panel::initControls() {
});
_settings->setText(tr::lng_group_call_settings());
const auto scheduled = (_call->scheduleDate() != 0);
_hangup->setText(scheduled
? tr::lng_group_call_close()
: tr::lng_group_call_leave());
if (scheduled) {
_call->real(
) | rpl::map([=](not_null<Data::GroupCall*> real) {
return real->scheduleDateValue();
}) | rpl::flatten_latest() | rpl::filter([](TimeId date) {
return (date == 0);
}) | rpl::take(1) | rpl::start_with_next([=] {
_hangup->setText(tr::lng_group_call_leave());
}, _callLifetime);
}
_call->stateValue(
) | rpl::filter([](State state) {
@ -497,10 +510,10 @@ void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
_mute->setState(Ui::CallMuteButtonState{
.text = (scheduleDate
? (canManage
? "Start Now" // #TODO voice chats
? tr::lng_group_call_start_now(tr::now)
: scheduleStartSubscribed
? "Cancel Reminder"
: "Set Reminder")
? tr::lng_group_call_cancel_reminder(tr::now)
: tr::lng_group_call_set_reminder(tr::now))
: state == GroupCall::InstanceState::Disconnected
? tr::lng_group_call_connecting(tr::now)
: mute == MuteState::ForceMuted

View File

@ -122,6 +122,9 @@ private:
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
object_ptr<Members> _members = { nullptr };
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
object_ptr<Ui::RpWidget> _countdown = { nullptr };
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
ChooseJoinAsProcess _joinAsProcess;
object_ptr<Ui::CallButton> _settings;

View File

@ -57,8 +57,13 @@ constexpr auto kHideBlobsDuration = crl::time(500);
constexpr auto kBlobLevelDuration = crl::time(250);
constexpr auto kBlobUpdateInterval = crl::time(100);
auto BarStateFromMuteState(MuteState state, GroupCall::InstanceState instanceState) {
return (instanceState == GroupCall::InstanceState::Disconnected)
auto BarStateFromMuteState(
MuteState state,
GroupCall::InstanceState instanceState,
TimeId scheduledDate) {
return scheduledDate
? BarState::ForceMuted
: (instanceState == GroupCall::InstanceState::Disconnected)
? BarState::Connecting
: (state == MuteState::ForceMuted || state == MuteState::RaisedHand)
? BarState::ForceMuted
@ -293,19 +298,27 @@ void TopBar::initControls() {
_call
? mapToState(_call->muted())
: _groupCall->muted(),
GroupCall::InstanceState::Connected));
GroupCall::InstanceState::Connected,
_call ? TimeId(0) : _groupCall->scheduleDate()));
using namespace rpl::mappers;
auto muted = _call
? rpl::combine(
_call->mutedValue() | rpl::map(mapToState),
rpl::single(GroupCall::InstanceState::Connected)
rpl::single(GroupCall::InstanceState::Connected),
rpl::single(TimeId(0))
) | rpl::type_erased()
: rpl::combine(
(_groupCall->mutedValue()
| MapPushToTalkToActive()
| rpl::distinct_until_changed()
| rpl::type_erased()),
_groupCall->instanceStateValue()
_groupCall->instanceStateValue(),
rpl::single(
_groupCall->scheduleDate()
) | rpl::then(_groupCall->real(
) | rpl::map([](not_null<Data::GroupCall*> call) {
return call->scheduleDateValue();
}) | rpl::flatten_latest())
) | rpl::filter(_2 != GroupCall::InstanceState::TransitionToRtc);
std::move(
muted

View File

@ -1025,12 +1025,15 @@ void History::applyServiceChanges(
}
} break;
case mtpc_messageActionGroupCall: {
const auto &d = action.c_messageActionGroupCall();
case mtpc_messageActionGroupCall:
case mtpc_messageActionGroupCallScheduled: {
const auto &call = (action.type() == mtpc_messageActionGroupCall)
? action.c_messageActionGroupCall().vcall()
: action.c_messageActionGroupCallScheduled().vcall();
if (const auto channel = peer->asChannel()) {
channel->setGroupCall(d.vcall());
channel->setGroupCall(call);
} else if (const auto chat = peer->asChat()) {
chat->setGroupCall(d.vcall());
chat->setGroupCall(call);
}
} break;
}

View File

@ -21,16 +21,84 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
GroupCallScheduledLeft::GroupCallScheduledLeft(TimeId date)
: _date(date)
, _datePrecise(computePreciseDate())
, _timer([=] { update(); }) {
update();
base::unixtime::updates(
) | rpl::start_with_next([=] {
restart();
}, _lifetime);
}
crl::time GroupCallScheduledLeft::computePreciseDate() const {
return crl::now() + (_date - base::unixtime::now()) * crl::time(1000);
}
void GroupCallScheduledLeft::setDate(TimeId date) {
if (_date == date) {
return;
}
_date = date;
restart();
}
void GroupCallScheduledLeft::restart() {
_datePrecise = computePreciseDate();
_timer.cancel();
update();
}
rpl::producer<QString> GroupCallScheduledLeft::text() const {
return _text.value();
}
void GroupCallScheduledLeft::update() {
const auto now = crl::now();
const auto duration = (_datePrecise - now);
const auto left = crl::time(std::round(std::abs(duration) / 1000.));
constexpr auto kDay = 24 * 60 * 60;
if (left >= kDay) {
const auto days = ((left / kDay) + 1);
_text = tr::lng_group_call_duration_days(
tr::now,
lt_count,
(duration < 0) ? (-days) : days);
} else {
const auto hours = left / (60 * 60);
const auto minutes = (left % (60 * 60)) / 60;
const auto seconds = (left % 60);
if (hours > 0) {
_text = (duration < 0 ? u"\x2212%1:%2:%3"_q : u"%1:%2:%3"_q)
.arg(hours, 2, 10, QChar('0'))
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0'));
} else {
_text = (duration < 0 && left > 0 ? u"\x2212%1:%2"_q : u"%1:%2"_q)
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0'));
}
}
if (left >= kDay) {
_timer.callOnce((left % kDay) * crl::time(1000));
} else {
const auto fraction = (std::abs(duration) + 500) % 1000;
if (fraction < 400 || fraction > 600) {
const auto next = std::abs(duration) % 1000;
_timer.callOnce((duration < 0) ? (1000 - next) : next);
} else if (!_timer.isActive()) {
_timer.callEach(1000);
}
}
}
GroupCallBar::GroupCallBar(
not_null<QWidget*> parent,
rpl::producer<GroupCallBarContent> content,
rpl::producer<bool> &&hideBlobs)
: _wrap(parent, object_ptr<RpWidget>(parent))
, _inner(_wrap.entity())
, _join(std::make_unique<RoundButton>(
_inner.get(),
tr::lng_group_call_join(),
st::groupCallTopBarJoin))
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
, _userpics(std::make_unique<GroupCallUserpics>(
st::historyGroupCallUserpics,
@ -55,6 +123,7 @@ GroupCallBar::GroupCallBar(
_content = content;
_userpics->update(_content.users, !_wrap.isHidden());
_inner->update();
refreshScheduledProcess();
}, lifetime());
std::move(
@ -76,6 +145,54 @@ GroupCallBar::GroupCallBar(
GroupCallBar::~GroupCallBar() = default;
void GroupCallBar::refreshOpenBrush() {
Expects(_open != nullptr);
const auto width = _open->width();
if (_openBrushForWidth == width) {
return;
}
auto gradient = QLinearGradient(QPoint(width, 0), QPoint(-width, 0));
gradient.setStops(QGradientStops{
{ 0.0, st::groupCallForceMutedBar1->c },
{ .35, st::groupCallForceMutedBar2->c },
{ 1.0, st::groupCallForceMutedBar3->c }
});
_openBrushOverride = QBrush(std::move(gradient));
_openBrushForWidth = width;
_open->setBrushOverride(_openBrushOverride);
}
void GroupCallBar::refreshScheduledProcess() {
const auto date = _content.scheduleDate;
if (!date) {
if (_scheduledProcess) {
_scheduledProcess = nullptr;
_open = nullptr;
_join = std::make_unique<RoundButton>(
_inner.get(),
tr::lng_group_call_join(),
st::groupCallTopBarJoin);
setupRightButton(_join.get());
}
return;
} else if (!_scheduledProcess) {
_scheduledProcess = std::make_unique<GroupCallScheduledLeft>(date);
_join = nullptr;
_open = std::make_unique<RoundButton>(
_inner.get(),
_scheduledProcess->text(),
st::groupCallTopBarOpen);
setupRightButton(_open.get());
_open->widthValue(
) | rpl::start_with_next([=] {
refreshOpenBrush();
}, _open->lifetime());
} else {
_scheduledProcess->setDate(date);
}
}
void GroupCallBar::setupInner() {
_inner->resize(0, st::historyReplyHeight);
_inner->paintRequest(
@ -102,17 +219,6 @@ void GroupCallBar::setupInner() {
return rpl::empty_value();
}) | rpl::start_to_stream(_barClicks, _inner->lifetime());
rpl::combine(
_inner->widthValue(),
_join->widthValue()
) | rpl::start_with_next([=](int outerWidth, int) {
// Skip shadow of the bar above.
const auto top = (st::historyReplyHeight
- st::lineWidth
- _join->height()) / 2 + st::lineWidth;
_join->moveToRight(top, top, outerWidth);
}, _join->lifetime());
_wrap.geometryValue(
) | rpl::start_with_next([=](QRect rect) {
updateShadowGeometry(rect);
@ -120,6 +226,21 @@ void GroupCallBar::setupInner() {
}, _inner->lifetime());
}
void GroupCallBar::setupRightButton(not_null<RoundButton*> button) {
rpl::combine(
_inner->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outerWidth, int) {
// Skip shadow of the bar above.
const auto top = (st::historyReplyHeight
- st::lineWidth
- button->height()) / 2 + st::lineWidth;
button->moveToRight(top, top, outerWidth);
}, button->lifetime());
button->clicks() | rpl::start_to_stream(_joinClicks, button->lifetime());
}
void GroupCallBar::paint(Painter &p) {
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
@ -131,7 +252,7 @@ void GroupCallBar::paint(Painter &p) {
p.setPen(st::defaultMessageBar.textFg);
p.setFont(font);
const auto available = _join->x() - left;
const auto available = (_join ? _join->x() : _open->x()) - left;
const auto titleWidth = font->width(_content.title);
p.drawTextLeft(
left,
@ -279,7 +400,7 @@ rpl::producer<> GroupCallBar::barClicks() const {
}
rpl::producer<> GroupCallBar::joinClicks() const {
return _join->clicks() | rpl::to_empty;
return _joinClicks.events() | rpl::to_empty;
}
} // namespace Ui

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/slide_wrap.h"
#include "ui/effects/animations.h"
#include "base/object_ptr.h"
#include "base/timer.h"
class Painter;
@ -28,6 +29,27 @@ struct GroupCallBarContent {
std::vector<GroupCallUser> users;
};
class GroupCallScheduledLeft final {
public:
explicit GroupCallScheduledLeft(TimeId date);
void setDate(TimeId date);
[[nodiscard]] rpl::producer<QString> text() const;
private:
[[nodiscard]] crl::time computePreciseDate() const;
void restart();
void update();
rpl::variable<QString> _text;
TimeId _date = 0;
crl::time _datePrecise = 0;
base::Timer _timer;
rpl::lifetime _lifetime;
};
class GroupCallBar final {
public:
GroupCallBar(
@ -57,15 +79,22 @@ public:
private:
using User = GroupCallUser;
void refreshOpenBrush();
void refreshScheduledProcess();
void updateShadowGeometry(QRect wrapGeometry);
void updateControlsGeometry(QRect wrapGeometry);
void updateUserpics();
void setupInner();
void setupRightButton(not_null<RoundButton*> button);
void paint(Painter &p);
SlideWrap<> _wrap;
not_null<RpWidget*> _inner;
std::unique_ptr<RoundButton> _join;
std::unique_ptr<RoundButton> _open;
rpl::event_stream<Qt::MouseButton> _joinClicks;
QBrush _openBrushOverride;
int _openBrushForWidth = 0;
std::unique_ptr<PlainShadow> _shadow;
rpl::event_stream<> _barClicks;
Fn<QRect(QRect)> _shadowGeometryPostprocess;
@ -73,6 +102,7 @@ private:
bool _forceHidden = false;
GroupCallBarContent _content;
std::unique_ptr<GroupCallScheduledLeft> _scheduledProcess;
std::unique_ptr<GroupCallUserpics> _userpics;
};

View File

@ -1003,6 +1003,8 @@ void SessionController::startOrJoinGroupCall(
&& calls.inGroupCall()) {
if (calls.currentGroupCall()->peer() == peer) {
calls.activateCurrentCall(joinHash);
} else if (calls.currentGroupCall()->scheduleDate()) {
calls.startOrJoinGroupCall(peer, joinHash);
} else {
askConfirmation(
tr::lng_group_call_leave_to_other_sure(tr::now),