calls_group_menu.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "calls/group/calls_group_menu.h"
  8. #include "calls/group/calls_group_call.h"
  9. #include "calls/group/calls_group_settings.h"
  10. #include "calls/group/calls_group_panel.h"
  11. #include "calls/group/ui/calls_group_recording_box.h"
  12. #include "data/data_peer.h"
  13. #include "data/data_group_call.h"
  14. #include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
  15. #include "ui/widgets/dropdown_menu.h"
  16. #include "ui/widgets/menu/menu.h"
  17. #include "ui/widgets/menu/menu_action.h"
  18. #include "ui/widgets/labels.h"
  19. #include "ui/widgets/checkbox.h"
  20. #include "ui/widgets/fields/input_field.h"
  21. #include "ui/effects/ripple_animation.h"
  22. #include "ui/layers/generic_box.h"
  23. #include "ui/painter.h"
  24. #include "lang/lang_keys.h"
  25. #include "base/unixtime.h"
  26. #include "base/timer_rpl.h"
  27. #include "styles/style_calls.h"
  28. #include "styles/style_layers.h"
  29. #include "styles/style_boxes.h"
  30. namespace Calls::Group {
  31. namespace {
  32. class JoinAsAction final : public Ui::Menu::ItemBase {
  33. public:
  34. JoinAsAction(
  35. not_null<Ui::RpWidget*> parent,
  36. const style::Menu &st,
  37. not_null<PeerData*> peer,
  38. Fn<void()> callback);
  39. bool isEnabled() const override;
  40. not_null<QAction*> action() const override;
  41. void handleKeyPress(not_null<QKeyEvent*> e) override;
  42. protected:
  43. QPoint prepareRippleStartPosition() const override;
  44. QImage prepareRippleMask() const override;
  45. int contentHeight() const override;
  46. private:
  47. void prepare();
  48. void paint(Painter &p);
  49. const not_null<QAction*> _dummyAction;
  50. const style::Menu &_st;
  51. const not_null<PeerData*> _peer;
  52. Ui::PeerUserpicView _userpicView;
  53. Ui::Text::String _text;
  54. Ui::Text::String _name;
  55. int _textWidth = 0;
  56. int _nameWidth = 0;
  57. const int _height = 0;
  58. };
  59. class RecordingAction final : public Ui::Menu::ItemBase {
  60. public:
  61. RecordingAction(
  62. not_null<Ui::RpWidget*> parent,
  63. const style::Menu &st,
  64. rpl::producer<QString> text,
  65. rpl::producer<TimeId> startAtValues,
  66. Fn<void()> callback);
  67. bool isEnabled() const override;
  68. not_null<QAction*> action() const override;
  69. void handleKeyPress(not_null<QKeyEvent*> e) override;
  70. protected:
  71. QPoint prepareRippleStartPosition() const override;
  72. QImage prepareRippleMask() const override;
  73. int contentHeight() const override;
  74. private:
  75. void prepare(rpl::producer<QString> text);
  76. void refreshElapsedText();
  77. void paint(Painter &p);
  78. const not_null<QAction*> _dummyAction;
  79. const style::Menu &_st;
  80. TimeId _startAt = 0;
  81. crl::time _startedAt = 0;
  82. base::Timer _refreshTimer;
  83. Ui::Text::String _text;
  84. int _textWidth = 0;
  85. QString _elapsedText;
  86. const int _smallHeight = 0;
  87. const int _bigHeight = 0;
  88. };
  89. TextParseOptions MenuTextOptions = {
  90. TextParseLinks, // flags
  91. 0, // maxw
  92. 0, // maxh
  93. Qt::LayoutDirectionAuto, // dir
  94. };
  95. JoinAsAction::JoinAsAction(
  96. not_null<Ui::RpWidget*> parent,
  97. const style::Menu &st,
  98. not_null<PeerData*> peer,
  99. Fn<void()> callback)
  100. : ItemBase(parent, st)
  101. , _dummyAction(new QAction(parent))
  102. , _st(st)
  103. , _peer(peer)
  104. , _height(st::groupCallJoinAsPadding.top()
  105. + st::groupCallJoinAsPhotoSize
  106. + st::groupCallJoinAsPadding.bottom()) {
  107. setAcceptBoth(true);
  108. initResizeHook(parent->sizeValue());
  109. setClickedCallback(std::move(callback));
  110. paintRequest(
  111. ) | rpl::start_with_next([=] {
  112. Painter p(this);
  113. paint(p);
  114. }, lifetime());
  115. enableMouseSelecting();
  116. prepare();
  117. }
  118. void JoinAsAction::paint(Painter &p) {
  119. const auto selected = isSelected();
  120. const auto height = contentHeight();
  121. if (selected && _st.itemBgOver->c.alpha() < 255) {
  122. p.fillRect(0, 0, width(), height, _st.itemBg);
  123. }
  124. p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
  125. if (isEnabled()) {
  126. paintRipple(p, 0, 0);
  127. }
  128. const auto &padding = st::groupCallJoinAsPadding;
  129. _peer->paintUserpic(
  130. p,
  131. _userpicView,
  132. padding.left(),
  133. padding.top(),
  134. st::groupCallJoinAsPhotoSize);
  135. const auto textLeft = padding.left()
  136. + st::groupCallJoinAsPhotoSize
  137. + padding.left();
  138. p.setPen(selected ? _st.itemFgOver : _st.itemFg);
  139. _text.drawLeftElided(
  140. p,
  141. textLeft,
  142. st::groupCallJoinAsTextTop,
  143. _textWidth,
  144. width());
  145. p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
  146. _name.drawLeftElided(
  147. p,
  148. textLeft,
  149. st::groupCallJoinAsNameTop,
  150. _nameWidth,
  151. width());
  152. }
  153. void JoinAsAction::prepare() {
  154. rpl::combine(
  155. tr::lng_group_call_display_as_header(),
  156. Info::Profile::NameValue(_peer)
  157. ) | rpl::start_with_next([=](QString text, QString name) {
  158. const auto &padding = st::groupCallJoinAsPadding;
  159. _text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
  160. _name.setMarkedText(_st.itemStyle, { name }, MenuTextOptions);
  161. const auto textWidth = _text.maxWidth();
  162. const auto nameWidth = _name.maxWidth();
  163. const auto textLeft = padding.left()
  164. + st::groupCallJoinAsPhotoSize
  165. + padding.left();
  166. const auto w = std::clamp(
  167. (textLeft
  168. + std::max(textWidth, nameWidth)
  169. + padding.right()),
  170. _st.widthMin,
  171. _st.widthMax);
  172. setMinWidth(w);
  173. _textWidth = w - textLeft - padding.right();
  174. _nameWidth = w - textLeft - padding.right();
  175. update();
  176. }, lifetime());
  177. }
  178. bool JoinAsAction::isEnabled() const {
  179. return true;
  180. }
  181. not_null<QAction*> JoinAsAction::action() const {
  182. return _dummyAction;
  183. }
  184. QPoint JoinAsAction::prepareRippleStartPosition() const {
  185. return mapFromGlobal(QCursor::pos());
  186. }
  187. QImage JoinAsAction::prepareRippleMask() const {
  188. return Ui::RippleAnimation::RectMask(size());
  189. }
  190. int JoinAsAction::contentHeight() const {
  191. return _height;
  192. }
  193. void JoinAsAction::handleKeyPress(not_null<QKeyEvent*> e) {
  194. if (!isSelected()) {
  195. return;
  196. }
  197. const auto key = e->key();
  198. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  199. setClicked(Ui::Menu::TriggeredSource::Keyboard);
  200. }
  201. }
  202. RecordingAction::RecordingAction(
  203. not_null<Ui::RpWidget*> parent,
  204. const style::Menu &st,
  205. rpl::producer<QString> text,
  206. rpl::producer<TimeId> startAtValues,
  207. Fn<void()> callback)
  208. : ItemBase(parent, st)
  209. , _dummyAction(new QAction(parent))
  210. , _st(st)
  211. , _refreshTimer([=] { refreshElapsedText(); })
  212. , _smallHeight(st.itemPadding.top()
  213. + _st.itemStyle.font->height
  214. + st.itemPadding.bottom())
  215. , _bigHeight(st::groupCallRecordingTimerPadding.top()
  216. + _st.itemStyle.font->height
  217. + st::groupCallRecordingTimerFont->height
  218. + st::groupCallRecordingTimerPadding.bottom()) {
  219. std::move(
  220. startAtValues
  221. ) | rpl::start_with_next([=](TimeId startAt) {
  222. _startAt = startAt;
  223. _startedAt = crl::now();
  224. _refreshTimer.cancel();
  225. refreshElapsedText();
  226. resize(width(), contentHeight());
  227. }, lifetime());
  228. setAcceptBoth(true);
  229. initResizeHook(parent->sizeValue());
  230. setClickedCallback(std::move(callback));
  231. paintRequest(
  232. ) | rpl::start_with_next([=] {
  233. Painter p(this);
  234. paint(p);
  235. }, lifetime());
  236. enableMouseSelecting();
  237. prepare(std::move(text));
  238. }
  239. void RecordingAction::paint(Painter &p) {
  240. const auto selected = isSelected();
  241. const auto height = contentHeight();
  242. if (selected && _st.itemBgOver->c.alpha() < 255) {
  243. p.fillRect(0, 0, width(), height, _st.itemBg);
  244. }
  245. p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
  246. if (isEnabled()) {
  247. paintRipple(p, 0, 0);
  248. }
  249. const auto smallTop = st::groupCallRecordingTimerPadding.top();
  250. const auto textTop = _startAt ? smallTop : _st.itemPadding.top();
  251. p.setPen(selected ? _st.itemFgOver : _st.itemFg);
  252. _text.drawLeftElided(
  253. p,
  254. _st.itemPadding.left(),
  255. textTop,
  256. _textWidth,
  257. width());
  258. if (_startAt) {
  259. p.setFont(st::groupCallRecordingTimerFont);
  260. p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
  261. p.drawTextLeft(
  262. _st.itemPadding.left(),
  263. smallTop + _st.itemStyle.font->height,
  264. width(),
  265. _elapsedText);
  266. }
  267. }
  268. void RecordingAction::refreshElapsedText() {
  269. const auto now = base::unixtime::now();
  270. const auto elapsed = std::max(now - _startAt, 0);
  271. const auto text = !_startAt
  272. ? QString()
  273. : (elapsed >= 3600)
  274. ? QString("%1:%2:%3"
  275. ).arg(elapsed / 3600
  276. ).arg((elapsed % 3600) / 60, 2, 10, QChar('0')
  277. ).arg(elapsed % 60, 2, 10, QChar('0'))
  278. : QString("%1:%2"
  279. ).arg(elapsed / 60
  280. ).arg(elapsed % 60, 2, 10, QChar('0'));
  281. if (_elapsedText != text) {
  282. _elapsedText = text;
  283. update();
  284. }
  285. const auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500);
  286. _refreshTimer.callOnce(nextCall);
  287. }
  288. void RecordingAction::prepare(rpl::producer<QString> text) {
  289. refreshElapsedText();
  290. const auto &padding = _st.itemPadding;
  291. const auto textWidth1 = _st.itemStyle.font->width(
  292. tr::lng_group_call_recording_start(tr::now));
  293. const auto textWidth2 = _st.itemStyle.font->width(
  294. tr::lng_group_call_recording_stop(tr::now));
  295. const auto maxWidth = st::groupCallRecordingTimerFont->width("23:59:59");
  296. const auto w = std::clamp(
  297. (padding.left()
  298. + std::max({ textWidth1, textWidth2, maxWidth })
  299. + padding.right()),
  300. _st.widthMin,
  301. _st.widthMax);
  302. setMinWidth(w);
  303. std::move(text) | rpl::start_with_next([=](QString text) {
  304. const auto &padding = _st.itemPadding;
  305. _text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
  306. _textWidth = w - padding.left() - padding.right();
  307. update();
  308. }, lifetime());
  309. }
  310. bool RecordingAction::isEnabled() const {
  311. return true;
  312. }
  313. not_null<QAction*> RecordingAction::action() const {
  314. return _dummyAction;
  315. }
  316. QPoint RecordingAction::prepareRippleStartPosition() const {
  317. return mapFromGlobal(QCursor::pos());
  318. }
  319. QImage RecordingAction::prepareRippleMask() const {
  320. return Ui::RippleAnimation::RectMask(size());
  321. }
  322. int RecordingAction::contentHeight() const {
  323. return _startAt ? _bigHeight : _smallHeight;
  324. }
  325. void RecordingAction::handleKeyPress(not_null<QKeyEvent*> e) {
  326. if (!isSelected()) {
  327. return;
  328. }
  329. const auto key = e->key();
  330. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  331. setClicked(Ui::Menu::TriggeredSource::Keyboard);
  332. }
  333. }
  334. base::unique_qptr<Ui::Menu::ItemBase> MakeJoinAsAction(
  335. not_null<Ui::Menu::Menu*> menu,
  336. not_null<PeerData*> peer,
  337. Fn<void()> callback) {
  338. return base::make_unique_q<JoinAsAction>(
  339. menu,
  340. menu->st(),
  341. peer,
  342. std::move(callback));
  343. }
  344. base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
  345. not_null<Ui::Menu::Menu*> menu,
  346. rpl::producer<TimeId> startDate,
  347. Fn<void()> callback) {
  348. using namespace rpl::mappers;
  349. return base::make_unique_q<RecordingAction>(
  350. menu,
  351. menu->st(),
  352. rpl::conditional(
  353. rpl::duplicate(startDate) | rpl::map(!!_1),
  354. tr::lng_group_call_recording_stop(),
  355. tr::lng_group_call_recording_start()),
  356. rpl::duplicate(startDate),
  357. std::move(callback));
  358. }
  359. } // namespace
  360. void LeaveBox(
  361. not_null<Ui::GenericBox*> box,
  362. not_null<GroupCall*> call,
  363. bool discardChecked,
  364. BoxContext context) {
  365. const auto livestream = call->peer()->isBroadcast();
  366. const auto scheduled = (call->scheduleDate() != 0);
  367. if (!scheduled) {
  368. box->setTitle(livestream
  369. ? tr::lng_group_call_leave_title_channel()
  370. : tr::lng_group_call_leave_title());
  371. }
  372. const auto inCall = (context == BoxContext::GroupCallPanel);
  373. box->addRow(
  374. object_ptr<Ui::FlatLabel>(
  375. box.get(),
  376. (scheduled
  377. ? (livestream
  378. ? tr::lng_group_call_close_sure_channel()
  379. : tr::lng_group_call_close_sure())
  380. : (livestream
  381. ? tr::lng_group_call_leave_sure_channel()
  382. : tr::lng_group_call_leave_sure())),
  383. (inCall ? st::groupCallBoxLabel : st::boxLabel)),
  384. scheduled ? st::boxPadding : st::boxRowPadding);
  385. const auto discard = call->peer()->canManageGroupCall()
  386. ? box->addRow(object_ptr<Ui::Checkbox>(
  387. box.get(),
  388. (scheduled
  389. ? (livestream
  390. ? tr::lng_group_call_also_cancel_channel()
  391. : tr::lng_group_call_also_cancel())
  392. : (livestream
  393. ? tr::lng_group_call_also_end_channel()
  394. : tr::lng_group_call_also_end())),
  395. discardChecked,
  396. (inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),
  397. (inCall ? st::groupCallCheck : st::defaultCheck)),
  398. style::margins(
  399. st::boxRowPadding.left(),
  400. st::boxRowPadding.left(),
  401. st::boxRowPadding.right(),
  402. st::boxRowPadding.bottom()))
  403. : nullptr;
  404. const auto weak = base::make_weak(call);
  405. auto label = scheduled
  406. ? tr::lng_group_call_close()
  407. : tr::lng_group_call_leave();
  408. box->addButton(std::move(label), [=] {
  409. const auto discardCall = (discard && discard->checked());
  410. box->closeBox();
  411. if (!weak) {
  412. return;
  413. } else if (discardCall) {
  414. call->discard();
  415. } else {
  416. call->hangup();
  417. }
  418. });
  419. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  420. }
  421. object_ptr<Ui::GenericBox> ConfirmBox(Ui::ConfirmBoxArgs &&args) {
  422. auto copy = std::move(args);
  423. copy.labelStyle = &st::groupCallBoxLabel;
  424. return Ui::MakeConfirmBox(std::move(copy));
  425. }
  426. void FillMenu(
  427. not_null<Ui::DropdownMenu*> menu,
  428. not_null<PeerData*> peer,
  429. not_null<GroupCall*> call,
  430. bool wide,
  431. Fn<void()> chooseJoinAs,
  432. Fn<void()> chooseShareScreenSource,
  433. Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
  434. const auto weak = base::make_weak(call);
  435. const auto resolveReal = [=] {
  436. const auto real = peer->groupCall();
  437. const auto strong = weak.get();
  438. return (real && strong && (real->id() == strong->id()))
  439. ? real
  440. : nullptr;
  441. };
  442. const auto real = resolveReal();
  443. if (!real) {
  444. return;
  445. }
  446. const auto addEditJoinAs = call->showChooseJoinAs();
  447. const auto addEditTitle = call->canManage();
  448. const auto addEditRecording = call->canManage() && !real->scheduleDate();
  449. const auto addScreenCast = !wide
  450. && call->videoIsWorking()
  451. && !real->scheduleDate();
  452. if (addEditJoinAs) {
  453. menu->addAction(MakeJoinAsAction(
  454. menu->menu(),
  455. call->joinAs(),
  456. chooseJoinAs));
  457. menu->addSeparator();
  458. }
  459. if (addEditTitle) {
  460. const auto livestream = call->peer()->isBroadcast();
  461. const auto text = (livestream
  462. ? tr::lng_group_call_edit_title_channel
  463. : tr::lng_group_call_edit_title)(tr::now);
  464. menu->addAction(text, [=] {
  465. const auto done = [=](const QString &title) {
  466. if (const auto strong = weak.get()) {
  467. strong->changeTitle(title);
  468. }
  469. };
  470. if (const auto real = resolveReal()) {
  471. showBox(Box(
  472. EditGroupCallTitleBox,
  473. peer->name(),
  474. real->title(),
  475. livestream,
  476. done));
  477. }
  478. });
  479. }
  480. if (addEditRecording) {
  481. const auto handler = [=] {
  482. const auto real = resolveReal();
  483. if (!real) {
  484. return;
  485. }
  486. const auto type = std::make_shared<RecordingType>();
  487. const auto recordStartDate = real->recordStartDate();
  488. const auto done = [=](QString title) {
  489. if (const auto strong = weak.get()) {
  490. strong->toggleRecording(
  491. !recordStartDate,
  492. title,
  493. (*type) != RecordingType::AudioOnly,
  494. (*type) == RecordingType::VideoPortrait);
  495. }
  496. };
  497. if (recordStartDate) {
  498. showBox(Box(
  499. StopGroupCallRecordingBox,
  500. done));
  501. } else {
  502. const auto typeDone = [=](RecordingType newType) {
  503. *type = newType;
  504. showBox(Box(
  505. AddTitleGroupCallRecordingBox,
  506. real->title(),
  507. done));
  508. };
  509. showBox(Box(StartGroupCallRecordingBox, typeDone));
  510. }
  511. };
  512. menu->addAction(MakeRecordingAction(
  513. menu->menu(),
  514. real->recordStartDateValue(),
  515. handler));
  516. }
  517. if (addScreenCast) {
  518. const auto sharing = call->isSharingScreen();
  519. const auto toggle = [=] {
  520. if (const auto strong = weak.get()) {
  521. if (sharing) {
  522. strong->toggleScreenSharing(std::nullopt);
  523. } else {
  524. chooseShareScreenSource();
  525. }
  526. }
  527. };
  528. menu->addAction(
  529. (call->isSharingScreen()
  530. ? tr::lng_group_call_screen_share_stop(tr::now)
  531. : tr::lng_group_call_screen_share_start(tr::now)),
  532. toggle);
  533. }
  534. menu->addAction(tr::lng_group_call_settings(tr::now), [=] {
  535. if (const auto strong = weak.get()) {
  536. showBox(Box(SettingsBox, strong));
  537. }
  538. });
  539. const auto finish = [=] {
  540. if (const auto strong = weak.get()) {
  541. showBox(Box(
  542. LeaveBox,
  543. strong,
  544. true,
  545. BoxContext::GroupCallPanel));
  546. }
  547. };
  548. const auto livestream = real->peer()->isBroadcast();
  549. menu->addAction(MakeAttentionAction(
  550. menu->menu(),
  551. (!call->canManage()
  552. ? tr::lng_group_call_leave
  553. : real->scheduleDate()
  554. ? (livestream
  555. ? tr::lng_group_call_cancel_channel
  556. : tr::lng_group_call_cancel)
  557. : (livestream
  558. ? tr::lng_group_call_end_channel
  559. : tr::lng_group_call_end))(tr::now),
  560. finish));
  561. }
  562. base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
  563. not_null<Ui::Menu::Menu*> menu,
  564. const QString &text,
  565. Fn<void()> callback) {
  566. return base::make_unique_q<Ui::Menu::Action>(
  567. menu,
  568. st::groupCallFinishMenu,
  569. Ui::Menu::CreateAction(
  570. menu,
  571. text,
  572. std::move(callback)),
  573. nullptr,
  574. nullptr);
  575. }
  576. } // namespace Calls::Group