Show recording indicator.

This commit is contained in:
John Preston 2021-03-09 17:24:32 +04:00
parent 50265afe93
commit 7edc91e29e
7 changed files with 146 additions and 80 deletions

View file

@ -2004,7 +2004,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_recording_stop_sure" = "Do you want to stop recording this chat?"; "lng_group_call_recording_stop_sure" = "Do you want to stop recording this chat?";
"lng_group_call_recording_start_field" = "Recording Title"; "lng_group_call_recording_start_field" = "Recording Title";
"lng_group_call_recording_start_button" = "Start"; "lng_group_call_recording_start_button" = "Start";
"lng_group_call_is_recorded" = "Voice chat is being recorded"; "lng_group_call_is_recorded" = "Voice chat is being recorded.";
"lng_group_call_can_speak_here" = "You can now speak."; "lng_group_call_can_speak_here" = "You can now speak.";
"lng_group_call_can_speak" = "You can now speak in **{chat}**."; "lng_group_call_can_speak" = "You can now speak in **{chat}**.";

View file

@ -560,6 +560,9 @@ groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) {
} }
groupCallAddButtonPosition: point(10px, 7px); groupCallAddButtonPosition: point(10px, 7px);
groupCallMembersWidthMax: 360px; groupCallMembersWidthMax: 360px;
groupCallRecordingMark: 6px;
groupCallRecordingMarkSkip: 4px;
groupCallRecordingMarkTop: 6px;
groupCallActiveButton: IconButton { groupCallActiveButton: IconButton {
width: 36px; width: 36px;

View file

@ -427,7 +427,7 @@ void GroupPanel::initWindow() {
0, 0,
0, 0,
widget()->width(), widget()->width(),
computeMembersListTop()); st::groupCallMembersTop);
return titleRect.contains(widgetPoint) return titleRect.contains(widgetPoint)
? (Flag::Move | Flag::Maximize) ? (Flag::Move | Flag::Maximize)
: Flag::None; : Flag::None;
@ -593,6 +593,46 @@ void GroupPanel::initWithCall(GroupCall *call) {
void GroupPanel::subscribeToChanges(not_null<Data::GroupCall*> real) { void GroupPanel::subscribeToChanges(not_null<Data::GroupCall*> real) {
_titleText = real->titleValue(); _titleText = real->titleValue();
const auto validateRecordingMark = [=](bool recording) {
if (!recording && _recordingMark) {
_recordingMark.destroy();
} else if (recording && !_recordingMark) {
_recordingMark.create(widget());
const auto size = st::groupCallRecordingMark;
const auto skip = st::groupCallRecordingMarkSkip;
_recordingMark->resize(size + 2 * skip, size + 2 * skip);
_recordingMark->setClickedCallback([=] {
Ui::Toast::Show(
widget(),
tr::lng_group_call_is_recorded(tr::now));
});
_recordingMark->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(_recordingMark.data());
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::groupCallMemberMutedIcon);
p.drawEllipse(skip, skip, size, size);
}, _recordingMark->lifetime());
}
refreshTitleGeometry();
};
using namespace rpl::mappers;
real->recordStartDateChanges(
) | rpl::map(
_1 != 0
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool recorded) {
validateRecordingMark(recorded);
Ui::Toast::Show(
widget(),
(recorded
? tr::lng_group_call_recording_started(tr::now)
: tr::lng_group_call_recording_stopped(tr::now)));
}, _callLifetime);
validateRecordingMark(real->recordStartDate() != 0);
} }
void GroupPanel::addMembers() { void GroupPanel::addMembers() {
@ -805,20 +845,17 @@ void GroupPanel::initGeometry() {
updateControlsGeometry(); updateControlsGeometry();
} }
int GroupPanel::computeMembersListTop() const { QRect GroupPanel::computeTitleRect() const {
if (computeTitleRect().has_value()) { const auto skip = st::groupCallTitleTop;
return st::groupCallMembersTop; const auto width = widget()->width();
}
return st::groupCallMembersTop
- (st::groupCallSubtitleTop - st::groupCallTitleTop);
}
std::optional<QRect> GroupPanel::computeTitleRect() const {
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
return QRect(70, 0, widget()->width() - 70, 28); return QRect(70, 0, width - skip - 70, 28);
#else // Q_OS_MAC #else // Q_OS_MAC
const auto controls = _controls->geometry(); const auto controls = _controls->geometry();
return QRect(0, 0, controls.x(), controls.height()); const auto right = controls.x() + controls.width() + skip;
return (controls.center().x() < width / 2)
? QRect(right, 0, width - right - skip, controls.height())
: QRect(skip, 0, controls.x() - 2 * skip, controls.height());
#endif // !Q_OS_MAC #endif // !Q_OS_MAC
} }
@ -839,7 +876,7 @@ void GroupPanel::updateControlsGeometry() {
st::groupCallMembersWidthMax); st::groupCallMembersWidthMax);
const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;
const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip; const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip;
const auto membersTop = computeMembersListTop(); const auto membersTop = st::groupCallMembersTop;
const auto availableHeight = muteTop const auto availableHeight = muteTop
- membersTop - membersTop
- st::groupCallMembersMargin.bottom(); - st::groupCallMembersMargin.bottom();
@ -859,29 +896,25 @@ void GroupPanel::updateControlsGeometry() {
} }
void GroupPanel::refreshTitle() { void GroupPanel::refreshTitle() {
if (computeTitleRect().has_value()) { if (!_title) {
if (!_title) { auto text = rpl::combine(
auto text = rpl::combine( Info::Profile::NameValue(_peer),
Info::Profile::NameValue(_peer), _titleText.value()
_titleText.value() ) | rpl::map([=](
) | rpl::map([=]( const TextWithEntities &name,
const TextWithEntities &name, const QString &title) {
const QString &title) { return title.isEmpty() ? name.text : title;
return title.isEmpty() ? name.text : title; }) | rpl::after_next([=] {
}) | rpl::after_next([=] { refreshTitleGeometry();
refreshTitleGeometry(); });
}); _title.create(
_title.create( widget(),
widget(), rpl::duplicate(text),
rpl::duplicate(text), st::groupCallTitleLabel);
st::groupCallTitleLabel); _title->show();
_title->show(); _title->setAttribute(Qt::WA_TransparentForMouseEvents);
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
refreshTitleGeometry();
} else if (_title) {
_title.destroy();
} }
refreshTitleGeometry();
if (!_subtitle) { if (!_subtitle) {
_subtitle.create( _subtitle.create(
widget(), widget(),
@ -904,26 +937,41 @@ void GroupPanel::refreshTitle() {
} }
void GroupPanel::refreshTitleGeometry() { void GroupPanel::refreshTitleGeometry() {
const auto titleRect = computeTitleRect(); if (!_title) {
if (!_title || !titleRect) {
return; return;
} }
const auto fullRect = computeTitleRect();
const auto recordingWidth = 2 * st::groupCallRecordingMarkSkip
+ st::groupCallRecordingMark;
const auto titleRect = _recordingMark
? QRect(
fullRect.x(),
fullRect.y(),
fullRect.width() - _recordingMark->width(),
fullRect.height())
: fullRect;
const auto best = _title->naturalWidth(); const auto best = _title->naturalWidth();
const auto from = (widget()->width() - best) / 2; const auto from = (widget()->width() - best) / 2;
const auto top = st::groupCallTitleTop; const auto top = st::groupCallTitleTop;
const auto left = titleRect->x(); const auto left = titleRect.x();
if (from >= left && from + best <= left + titleRect->width()) { if (from >= left && from + best <= left + titleRect.width()) {
_title->resizeToWidth(best); _title->resizeToWidth(best);
_title->moveToLeft(from, top); _title->moveToLeft(from, top);
} else if (titleRect->width() < best) { } else if (titleRect.width() < best) {
_title->resizeToWidth(titleRect->width()); _title->resizeToWidth(titleRect.width());
_title->moveToLeft(left, top); _title->moveToLeft(left, top);
} else if (from < left) { } else if (from < left) {
_title->resizeToWidth(best); _title->resizeToWidth(best);
_title->moveToLeft(left, top); _title->moveToLeft(left, top);
} else { } else {
_title->resizeToWidth(best); _title->resizeToWidth(best);
_title->moveToLeft(left + titleRect->width() - best, top); _title->moveToLeft(left + titleRect.width() - best, top);
}
if (_recordingMark) {
const auto markTop = top + st::groupCallRecordingMarkTop;
_recordingMark->move(
_title->x() + _title->width(),
markTop - st::groupCallRecordingMarkSkip);
} }
} }

View file

@ -23,6 +23,7 @@ class GroupCall;
} // namespace Data } // namespace Data
namespace Ui { namespace Ui {
class AbstractButton;
class CallButton; class CallButton;
class CallMuteButton; class CallMuteButton;
class IconButton; class IconButton;
@ -98,8 +99,7 @@ private:
void addMembers(); void addMembers();
void kickMember(not_null<UserData*> user); void kickMember(not_null<UserData*> user);
void kickMemberSure(not_null<UserData*> user); void kickMemberSure(not_null<UserData*> user);
[[nodiscard]] int computeMembersListTop() const; [[nodiscard]] QRect computeTitleRect() const;
[[nodiscard]] std::optional<QRect> computeTitleRect() const;
void refreshTitle(); void refreshTitle();
void refreshTitleGeometry(); void refreshTitleGeometry();
void setupRealCallViewers(not_null<GroupCall*> call); void setupRealCallViewers(not_null<GroupCall*> call);
@ -122,6 +122,7 @@ private:
object_ptr<Ui::FlatLabel> _title = { nullptr }; object_ptr<Ui::FlatLabel> _title = { nullptr };
object_ptr<Ui::FlatLabel> _subtitle = { nullptr }; object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
object_ptr<Ui::AbstractButton> _recordingMark = { nullptr };
object_ptr<GroupMembers> _members; object_ptr<GroupMembers> _members;
rpl::variable<QString> _titleText; rpl::variable<QString> _titleText;

View file

@ -213,37 +213,45 @@ void GroupCallSettingsBox(
tr::lng_group_call_edit_title(), tr::lng_group_call_edit_title(),
st::groupCallSettingsButton).get() st::groupCallSettingsButton).get()
: nullptr; : nullptr;
const auto recordStartDate = goodReal ? real->recordStartDate() : 0; static const auto ToDurationFrom = [](TimeId startDate) {
auto recordingLabel = [&] { return [=] {
return ; const auto now = base::unixtime::now();
const auto elapsed = std::max(now - startDate, 0);
const auto hours = elapsed / 3600;
const auto minutes = (elapsed % 3600) / 60;
const auto seconds = (elapsed % 60);
return hours
? QString("%1:%2:%3"
).arg(hours
).arg(minutes, 2, 10, QChar('0')
).arg(seconds, 2, 10, QChar('0'))
: QString("%1:%2"
).arg(minutes
).arg(seconds, 2, 10, QChar('0'));
};
}; };
static const auto ToRecordDuration = [](TimeId startDate) {
return !startDate
? (rpl::single(QString()) | rpl::type_erased())
: rpl::single(
rpl::empty_value()
) | rpl::then(base::timer_each(
crl::time(1000)
)) | rpl::map(ToDurationFrom(startDate));
};
using namespace rpl::mappers;
const auto editRecording = !addEditRecording const auto editRecording = !addEditRecording
? nullptr ? nullptr
: !recordStartDate
? AddButton(
layout,
tr::lng_group_call_recording_start(),
st::groupCallSettingsButton).get()
: AddButtonWithLabel( : AddButtonWithLabel(
layout, layout,
tr::lng_group_call_recording_stop(), rpl::conditional(
base::timer_each( real->recordStartDateValue() | rpl::map(!!_1),
crl::time(1000) tr::lng_group_call_recording_stop(),
) | rpl::map([=] { tr::lng_group_call_recording_start()),
const auto now = base::unixtime::now(); real->recordStartDateValue(
const auto elapsed = std::max(now - recordStartDate, 0); ) | rpl::map(
const auto hours = elapsed / 3600; ToRecordDuration
const auto minutes = (elapsed % 3600) / 60; ) | rpl::flatten_latest(),
const auto seconds = (elapsed % 60);
return hours
? QString("%1:%2:%3"
).arg(hours
).arg(minutes, 2, 10, QChar('0')
).arg(seconds, 2, 10, QChar('0'))
: QString("%1:%2"
).arg(minutes
).arg(seconds, 2, 10, QChar('0'));
}),
st::groupCallSettingsButton).get(); st::groupCallSettingsButton).get();
if (editJoinAs) { if (editJoinAs) {
editJoinAs->setClickedCallback([=] { editJoinAs->setClickedCallback([=] {
@ -282,14 +290,14 @@ void GroupCallSettingsBox(
} }
if (editRecording) { if (editRecording) {
editRecording->setClickedCallback([=] { editRecording->setClickedCallback([=] {
const auto real = peer->groupCall();
const auto id = call->id();
if (!real || real->id() != id) {
return;
}
const auto recordStartDate = real->recordStartDate();
const auto done = [=](QString title) { const auto done = [=](QString title) {
call->toggleRecording(!recordStartDate, title); call->toggleRecording(!recordStartDate, title);
const auto container = box->getDelegate()->outerContainer();
Ui::Toast::Show(
container,
(recordStartDate
? tr::lng_group_call_recording_stopped(tr::now)
: tr::lng_group_call_recording_started(tr::now)));
box->closeBox(); box->closeBox();
}; };
if (recordStartDate) { if (recordStartDate) {

View file

@ -184,7 +184,7 @@ void GroupCall::applyCall(const MTPGroupCall &call, bool force) {
|| (_fullCount.current() != data.vparticipants_count().v) || (_fullCount.current() != data.vparticipants_count().v)
|| (_canChangeJoinMuted != data.is_can_change_join_muted()) || (_canChangeJoinMuted != data.is_can_change_join_muted())
|| (_title.current() != title) || (_title.current() != title)
|| (_recordStartDate != recordDate); || (_recordStartDate.current() != recordDate);
if (!force && !changed) { if (!force && !changed) {
return; return;
} else if (!force && _version > data.vversion().v) { } else if (!force && _version > data.vversion().v) {

View file

@ -54,7 +54,13 @@ public:
_title = title; _title = title;
} }
[[nodiscard]] TimeId recordStartDate() const { [[nodiscard]] TimeId recordStartDate() const {
return _recordStartDate; return _recordStartDate.current();
}
[[nodiscard]] rpl::producer<TimeId> recordStartDateValue() const {
return _recordStartDate.value();
}
[[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const {
return _recordStartDate.changes();
} }
void setPeer(not_null<PeerData*> peer); void setPeer(not_null<PeerData*> peer);
@ -130,7 +136,7 @@ private:
base::Timer _speakingByActiveFinishTimer; base::Timer _speakingByActiveFinishTimer;
QString _nextOffset; QString _nextOffset;
rpl::variable<int> _fullCount = 0; rpl::variable<int> _fullCount = 0;
TimeId _recordStartDate = 0; rpl::variable<TimeId> _recordStartDate = 0;
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs; base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds; base::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds;