/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/calls_group_menu.h" #include "calls/calls_group_call.h" #include "calls/calls_group_settings.h" #include "calls/calls_group_panel.h" #include "data/data_peer.h" #include "data/data_group_call.h" #include "info/profile/info_profile_values.h" // Info::Profile::NameValue. #include "ui/widgets/dropdown_menu.h" #include "ui/widgets/menu/menu.h" #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/input_fields.h" #include "ui/effects/ripple_animation.h" #include "ui/layers/generic_box.h" #include "lang/lang_keys.h" #include "base/unixtime.h" #include "base/timer_rpl.h" #include "styles/style_calls.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" namespace Calls::Group { namespace { constexpr auto kMaxGroupCallLength = 40; void EditGroupCallTitleBox( not_null box, const QString &placeholder, const QString &title, Fn done) { box->setTitle(tr::lng_group_call_edit_title()); const auto input = box->addRow(object_ptr( box, st::groupCallField, rpl::single(placeholder), title)); input->setMaxLength(kMaxGroupCallLength); box->setFocusCallback([=] { input->setFocusFast(); }); box->addButton(tr::lng_settings_save(), [=] { const auto result = input->getLastText().trimmed(); box->closeBox(); done(result); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void StartGroupCallRecordingBox( not_null box, const QString &title, Fn done) { box->setTitle(tr::lng_group_call_recording_start()); box->addRow( object_ptr( box.get(), tr::lng_group_call_recording_start_sure(), st::groupCallBoxLabel)); const auto input = box->addRow(object_ptr( box, st::groupCallField, tr::lng_group_call_recording_start_field(), title)); box->setFocusCallback([=] { input->setFocusFast(); }); box->addButton(tr::lng_group_call_recording_start_button(), [=] { const auto result = input->getLastText().trimmed(); if (result.isEmpty()) { input->showError(); return; } box->closeBox(); done(result); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void StopGroupCallRecordingBox( not_null box, Fn done) { box->addRow( object_ptr( box.get(), tr::lng_group_call_recording_stop_sure(), st::groupCallBoxLabel), style::margins( st::boxRowPadding.left(), st::boxPadding.top(), st::boxRowPadding.right(), st::boxPadding.bottom())); box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); done(QString()); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } [[nodiscard]] auto ToDurationFrom(TimeId startDate) { 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')); }; } [[nodiscard]] rpl::producer 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)); } class JoinAsAction final : public Ui::Menu::ItemBase { public: JoinAsAction( not_null parent, const style::Menu &st, not_null peer, Fn callback); bool isEnabled() const override; not_null action() const override; void handleKeyPress(not_null e) override; protected: QPoint prepareRippleStartPosition() const override; QImage prepareRippleMask() const override; int contentHeight() const override; private: void prepare(); void paint(Painter &p); const not_null _dummyAction; const style::Menu &_st; const not_null _peer; std::shared_ptr _userpicView; Ui::Text::String _text; Ui::Text::String _name; int _textWidth = 0; int _nameWidth = 0; const int _height = 0; }; class RecordingAction final : public Ui::Menu::ItemBase { public: RecordingAction( not_null parent, const style::Menu &st, rpl::producer text, rpl::producer startAtValues, Fn callback); bool isEnabled() const override; not_null action() const override; void handleKeyPress(not_null e) override; protected: QPoint prepareRippleStartPosition() const override; QImage prepareRippleMask() const override; int contentHeight() const override; private: void prepare(rpl::producer text); void refreshElapsedText(); void paint(Painter &p); const not_null _dummyAction; const style::Menu &_st; TimeId _startAt = 0; crl::time _startedAt = 0; base::Timer _refreshTimer; Ui::Text::String _text; int _textWidth = 0; QString _elapsedText; const int _smallHeight = 0; const int _bigHeight = 0; }; TextParseOptions MenuTextOptions = { TextParseLinks | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; JoinAsAction::JoinAsAction( not_null parent, const style::Menu &st, not_null peer, Fn callback) : ItemBase(parent, st) , _dummyAction(new QAction(parent)) , _st(st) , _peer(peer) , _height(st::groupCallJoinAsPadding.top() + st::groupCallJoinAsPhotoSize + st::groupCallJoinAsPadding.bottom()) { setAcceptBoth(true); initResizeHook(parent->sizeValue()); setClickedCallback(std::move(callback)); paintRequest( ) | rpl::start_with_next([=] { Painter p(this); paint(p); }, lifetime()); enableMouseSelecting(); prepare(); } void JoinAsAction::paint(Painter &p) { const auto selected = isSelected(); const auto height = contentHeight(); if (selected && _st.itemBgOver->c.alpha() < 255) { p.fillRect(0, 0, width(), height, _st.itemBg); } p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg); if (isEnabled()) { paintRipple(p, 0, 0); } const auto &padding = st::groupCallJoinAsPadding; _peer->paintUserpic( p, _userpicView, padding.left(), padding.top(), st::groupCallJoinAsPhotoSize); const auto textLeft = padding.left() + st::groupCallJoinAsPhotoSize + padding.left(); p.setPen(selected ? _st.itemFgOver : _st.itemFg); _text.drawLeftElided( p, textLeft, st::groupCallJoinAsTextTop, _textWidth, width()); p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); _name.drawLeftElided( p, textLeft, st::groupCallJoinAsNameTop, _nameWidth, width()); } void JoinAsAction::prepare() { rpl::combine( tr::lng_group_call_display_as_header(), Info::Profile::NameValue(_peer) ) | rpl::start_with_next([=](QString text, TextWithEntities name) { const auto &padding = st::groupCallJoinAsPadding; _text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions); _name.setMarkedText(_st.itemStyle, name, MenuTextOptions); const auto textWidth = _text.maxWidth(); const auto nameWidth = _name.maxWidth(); const auto textLeft = padding.left() + st::groupCallJoinAsPhotoSize + padding.left(); const auto w = std::clamp( (textLeft + std::max(textWidth, nameWidth) + padding.right()), _st.widthMin, _st.widthMax); setMinWidth(w); _textWidth = w - textLeft - padding.right(); _nameWidth = w - textLeft - padding.right(); update(); }, lifetime()); } bool JoinAsAction::isEnabled() const { return true; } not_null JoinAsAction::action() const { return _dummyAction; } QPoint JoinAsAction::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()); } QImage JoinAsAction::prepareRippleMask() const { return Ui::RippleAnimation::rectMask(size()); } int JoinAsAction::contentHeight() const { return _height; } void JoinAsAction::handleKeyPress(not_null e) { if (!isSelected()) { return; } const auto key = e->key(); if (key == Qt::Key_Enter || key == Qt::Key_Return) { setClicked(Ui::Menu::TriggeredSource::Keyboard); } } RecordingAction::RecordingAction( not_null parent, const style::Menu &st, rpl::producer text, rpl::producer startAtValues, Fn callback) : ItemBase(parent, st) , _dummyAction(new QAction(parent)) , _st(st) , _refreshTimer([=] { refreshElapsedText(); }) , _smallHeight(st.itemPadding.top() + _st.itemStyle.font->height + st.itemPadding.bottom()) , _bigHeight(st::groupCallRecordingTimerPadding.top() + _st.itemStyle.font->height + st::groupCallRecordingTimerFont->height + st::groupCallRecordingTimerPadding.bottom()) { std::move( startAtValues ) | rpl::start_with_next([=](TimeId startAt) { _startAt = startAt; _startedAt = crl::now(); _refreshTimer.cancel(); refreshElapsedText(); resize(width(), contentHeight()); }, lifetime()); setAcceptBoth(true); initResizeHook(parent->sizeValue()); setClickedCallback(std::move(callback)); paintRequest( ) | rpl::start_with_next([=] { Painter p(this); paint(p); }, lifetime()); enableMouseSelecting(); prepare(std::move(text)); } void RecordingAction::paint(Painter &p) { const auto selected = isSelected(); const auto height = contentHeight(); if (selected && _st.itemBgOver->c.alpha() < 255) { p.fillRect(0, 0, width(), height, _st.itemBg); } p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg); if (isEnabled()) { paintRipple(p, 0, 0); } const auto smallTop = st::groupCallRecordingTimerPadding.top(); const auto textTop = _startAt ? smallTop : _st.itemPadding.top(); p.setPen(selected ? _st.itemFgOver : _st.itemFg); _text.drawLeftElided( p, _st.itemPadding.left(), textTop, _textWidth, width()); if (_startAt) { p.setFont(st::groupCallRecordingTimerFont); p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); p.drawTextLeft( _st.itemPadding.left(), smallTop + _st.itemStyle.font->height, width(), _elapsedText); } } void RecordingAction::refreshElapsedText() { const auto now = base::unixtime::now(); const auto elapsed = std::max(now - _startAt, 0); const auto text = !_startAt ? QString() : (elapsed >= 3600) ? QString("%1:%2:%3" ).arg(elapsed / 3600 ).arg((elapsed % 3600) / 60, 2, 10, QChar('0') ).arg(elapsed % 60, 2, 10, QChar('0')) : QString("%1:%2" ).arg(elapsed / 60 ).arg(elapsed % 60, 2, 10, QChar('0')); if (_elapsedText != text) { _elapsedText = text; update(); } const auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500); _refreshTimer.callOnce(nextCall); } void RecordingAction::prepare(rpl::producer text) { refreshElapsedText(); const auto &padding = _st.itemPadding; const auto textWidth1 = _st.itemStyle.font->width( tr::lng_group_call_recording_start(tr::now)); const auto textWidth2 = _st.itemStyle.font->width( tr::lng_group_call_recording_stop(tr::now)); const auto maxWidth = st::groupCallRecordingTimerFont->width("23:59:59"); const auto w = std::clamp( (padding.left() + std::max({ textWidth1, textWidth2, maxWidth }) + padding.right()), _st.widthMin, _st.widthMax); setMinWidth(w); std::move(text) | rpl::start_with_next([=](QString text) { const auto &padding = _st.itemPadding; _text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions); const auto textWidth = _text.maxWidth(); _textWidth = w - padding.left() - padding.right(); update(); }, lifetime()); } bool RecordingAction::isEnabled() const { return true; } not_null RecordingAction::action() const { return _dummyAction; } QPoint RecordingAction::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()); } QImage RecordingAction::prepareRippleMask() const { return Ui::RippleAnimation::rectMask(size()); } int RecordingAction::contentHeight() const { return _startAt ? _bigHeight : _smallHeight; } void RecordingAction::handleKeyPress(not_null e) { if (!isSelected()) { return; } const auto key = e->key(); if (key == Qt::Key_Enter || key == Qt::Key_Return) { setClicked(Ui::Menu::TriggeredSource::Keyboard); } } base::unique_qptr MakeJoinAsAction( not_null menu, not_null peer, Fn callback) { return base::make_unique_q( menu, menu->st(), peer, std::move(callback)); } base::unique_qptr MakeRecordingAction( not_null menu, rpl::producer startDate, Fn callback) { using namespace rpl::mappers; return base::make_unique_q( menu, menu->st(), rpl::conditional( rpl::duplicate(startDate) | rpl::map(!!_1), tr::lng_group_call_recording_stop(), tr::lng_group_call_recording_start()), rpl::duplicate(startDate), std::move(callback)); } } // namespace void LeaveBox( not_null box, not_null 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( box.get(), (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( box.get(), (scheduled ? tr::lng_group_call_also_cancel() : tr::lng_group_call_also_end()), discardChecked, (inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox), (inCall ? st::groupCallCheck : st::defaultCheck)), style::margins( st::boxRowPadding.left(), st::boxRowPadding.left(), st::boxRowPadding.right(), st::boxRowPadding.bottom())) : nullptr; const auto weak = base::make_weak(call.get()); 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(); if (!weak) { return; } else if (discardCall) { call->discard(); } else { call->hangup(); } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void ConfirmBoxBuilder( not_null box, ConfirmBoxArgs &&args) { const auto label = box->addRow( object_ptr( box.get(), rpl::single(args.text), args.st ? *args.st : st::groupCallBoxLabel), st::boxPadding); if (args.callback) { box->addButton(std::move(args.button), std::move(args.callback)); } if (args.filter) { label->setClickHandlerFilter(std::move(args.filter)); } box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void FillMenu( not_null menu, not_null peer, not_null call, Fn chooseJoinAs, Fn)> showBox) { const auto weak = base::make_weak(call.get()); const auto resolveReal = [=] { const auto real = peer->groupCall(); const auto strong = weak.get(); return (real && strong && (real->id() == strong->id())) ? real : nullptr; }; const auto real = resolveReal(); if (!real) { return; } const auto addEditJoinAs = call->showChooseJoinAs(); const auto addEditTitle = peer->canManageGroupCall(); const auto addEditRecording = peer->canManageGroupCall() && !real->scheduleDate(); if (addEditJoinAs) { menu->addAction(MakeJoinAsAction( menu->menu(), call->joinAs(), chooseJoinAs)); menu->addSeparator(); } if (addEditTitle) { menu->addAction(tr::lng_group_call_edit_title(tr::now), [=] { const auto done = [=](const QString &title) { if (const auto strong = weak.get()) { strong->changeTitle(title); } }; if (const auto real = resolveReal()) { showBox(Box( EditGroupCallTitleBox, peer->name, real->title(), done)); } }); } if (addEditRecording) { const auto handler = [=] { const auto real = resolveReal(); if (!real) { return; } const auto recordStartDate = real->recordStartDate(); const auto done = [=](QString title) { if (const auto strong = weak.get()) { strong->toggleRecording(!recordStartDate, title); } }; if (recordStartDate) { showBox(Box( StopGroupCallRecordingBox, done)); } else { showBox(Box( StartGroupCallRecordingBox, real->title(), done)); } }; menu->addAction(MakeRecordingAction( menu->menu(), real->recordStartDateValue(), handler)); } menu->addAction(tr::lng_group_call_settings(tr::now), [=] { if (const auto strong = weak.get()) { showBox(Box(SettingsBox, strong)); } }); const auto finish = [=] { if (const auto strong = weak.get()) { showBox(Box( LeaveBox, strong, 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 MakeAttentionAction( not_null menu, const QString &text, Fn callback) { return base::make_unique_q( menu, st::groupCallFinishMenu, Ui::Menu::CreateAction( menu, text, std::move(callback)), nullptr, nullptr); } } // namespace Calls::Group