1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "calls/group/calls_group_panel.h"
9 
10 #include "calls/group/calls_group_common.h"
11 #include "calls/group/calls_group_members.h"
12 #include "calls/group/calls_group_settings.h"
13 #include "calls/group/calls_group_menu.h"
14 #include "calls/group/calls_group_viewport.h"
15 #include "calls/group/calls_group_toasts.h"
16 #include "calls/group/calls_group_invite_controller.h"
17 #include "calls/group/ui/calls_group_scheduled_labels.h"
18 #include "calls/group/ui/desktop_capture_choose_source.h"
19 #include "ui/platform/ui_platform_window_title.h"
20 #include "ui/platform/ui_platform_utility.h"
21 #include "ui/controls/call_mute_button.h"
22 #include "ui/widgets/buttons.h"
23 #include "ui/widgets/call_button.h"
24 #include "ui/widgets/checkbox.h"
25 #include "ui/widgets/dropdown_menu.h"
26 #include "ui/widgets/input_fields.h"
27 #include "ui/widgets/tooltip.h"
28 #include "ui/widgets/rp_window.h"
29 #include "ui/chat/group_call_bar.h"
30 #include "ui/layers/layer_manager.h"
31 #include "ui/layers/generic_box.h"
32 #include "ui/text/text_utilities.h"
33 #include "ui/toast/toast.h"
34 #include "ui/toasts/common_toasts.h"
35 #include "ui/image/image_prepare.h"
36 #include "ui/round_rect.h"
37 #include "ui/special_buttons.h"
38 #include "info/profile/info_profile_values.h" // Info::Profile::Value.
39 #include "core/application.h"
40 #include "lang/lang_keys.h"
41 #include "data/data_channel.h"
42 #include "data/data_chat.h"
43 #include "data/data_user.h"
44 #include "data/data_group_call.h"
45 #include "data/data_session.h"
46 #include "data/data_changes.h"
47 #include "main/main_session.h"
48 #include "base/event_filter.h"
49 #include "base/unixtime.h"
50 #include "base/qt_signal_producer.h"
51 #include "base/timer_rpl.h"
52 #include "apiwrap.h" // api().kickParticipant.
53 #include "webrtc/webrtc_video_track.h"
54 #include "webrtc/webrtc_media_devices.h" // UniqueDesktopCaptureSource.
55 #include "webrtc/webrtc_audio_input_tester.h"
56 #include "styles/style_calls.h"
57 #include "styles/style_layers.h"
58 
59 #include <QtWidgets/QApplication>
60 #include <QtGui/QWindow>
61 #include <QtGui/QScreen>
62 
63 namespace Calls::Group {
64 namespace {
65 
66 constexpr auto kSpacePushToTalkDelay = crl::time(250);
67 constexpr auto kRecordingAnimationDuration = crl::time(1200);
68 constexpr auto kRecordingOpacity = 0.6;
69 constexpr auto kStartNoConfirmation = TimeId(10);
70 constexpr auto kControlsBackgroundOpacity = 0.8;
71 constexpr auto kOverrideActiveColorBgAlpha = 172;
72 
73 } // namespace
74 
75 struct Panel::ControlsBackgroundNarrow {
ControlsBackgroundNarrowCalls::Group::Panel::ControlsBackgroundNarrow76 	explicit ControlsBackgroundNarrow(not_null<QWidget*> parent)
77 	: shadow(parent)
78 	, blocker(parent) {
79 	}
80 
81 	Ui::RpWidget shadow;
82 	Ui::RpWidget blocker;
83 };
84 
Panel(not_null<GroupCall * > call)85 Panel::Panel(not_null<GroupCall*> call)
86 : _call(call)
87 , _peer(call->peer())
88 , _layerBg(std::make_unique<Ui::LayerManager>(widget()))
89 #ifndef Q_OS_MAC
90 , _controls(std::make_unique<Ui::Platform::TitleControls>(
91 	widget(),
92 	st::groupCallTitle))
93 #endif // !Q_OS_MAC
94 , _viewport(
95 	std::make_unique<Viewport>(widget(), PanelMode::Wide, _window.backend()))
96 , _mute(std::make_unique<Ui::CallMuteButton>(
97 	widget(),
98 	st::callMuteButton,
99 	Core::App().appDeactivatedValue(),
100 	Ui::CallMuteButtonState{
101 		.text = (_call->scheduleDate()
102 			? tr::lng_group_call_start_now(tr::now)
103 			: tr::lng_group_call_connecting(tr::now)),
104 		.type = (!_call->scheduleDate()
105 			? Ui::CallMuteButtonType::Connecting
106 			: _peer->canManageGroupCall()
107 			? Ui::CallMuteButtonType::ScheduledCanStart
108 			: _call->scheduleStartSubscribed()
109 			? Ui::CallMuteButtonType::ScheduledNotify
110 			: Ui::CallMuteButtonType::ScheduledSilent),
111 	}))
112 , _hangup(widget(), st::groupCallHangup)
113 , _stickedTooltipsShown(Core::App().settings().hiddenGroupCallTooltips()
114 	& ~StickedTooltip::Microphone) // Always show tooltip about mic.
115 , _toasts(std::make_unique<Toasts>(this)) {
116 	_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
117 	_layerBg->setHideByBackgroundClick(true);
118 
119 	_viewport->widget()->hide();
120 	if (!_viewport->requireARGB32()) {
121 		_call->setNotRequireARGB32();
122 	}
123 
124 	SubscribeToMigration(
125 		_peer,
126 		lifetime(),
__anon220bdc520202(not_null<ChannelData*> channel) 127 		[=](not_null<ChannelData*> channel) { migrate(channel); });
128 	setupRealCallViewers();
129 
130 	initWindow();
131 	initWidget();
132 	initControls();
133 	initLayout();
134 	showAndActivate();
135 }
136 
~Panel()137 Panel::~Panel() {
138 	_menu.destroy();
139 	_viewport = nullptr;
140 }
141 
setupRealCallViewers()142 void Panel::setupRealCallViewers() {
143 	_call->real(
144 	) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
145 		subscribeToChanges(real);
146 	}, lifetime());
147 }
148 
call() const149 not_null<GroupCall*> Panel::call() const {
150 	return _call;
151 }
152 
isActive() const153 bool Panel::isActive() const {
154 	return window()->isActiveWindow()
155 		&& window()->isVisible()
156 		&& !(window()->windowState() & Qt::WindowMinimized);
157 }
158 
showToast(TextWithEntities && text,crl::time duration)159 void Panel::showToast(TextWithEntities &&text, crl::time duration) {
160 	if (const auto strong = _lastToast.get()) {
161 		strong->hideAnimated();
162 	}
163 	_lastToast = Ui::ShowMultilineToast({
164 		.parentOverride = widget(),
165 		.text = std::move(text),
166 		.duration = duration,
167 	});
168 }
169 
minimize()170 void Panel::minimize() {
171 	window()->setWindowState(window()->windowState() | Qt::WindowMinimized);
172 }
173 
close()174 void Panel::close() {
175 	window()->close();
176 }
177 
showAndActivate()178 void Panel::showAndActivate() {
179 	if (window()->isHidden()) {
180 		window()->show();
181 	}
182 	const auto state = window()->windowState();
183 	if (state & Qt::WindowMinimized) {
184 		window()->setWindowState(state & ~Qt::WindowMinimized);
185 	}
186 	window()->raise();
187 	window()->activateWindow();
188 	window()->setFocus();
189 }
190 
migrate(not_null<ChannelData * > channel)191 void Panel::migrate(not_null<ChannelData*> channel) {
192 	_peer = channel;
193 	_peerLifetime.destroy();
194 	subscribeToPeerChanges();
195 	_title.destroy();
196 	refreshTitle();
197 }
198 
subscribeToPeerChanges()199 void Panel::subscribeToPeerChanges() {
200 	Info::Profile::NameValue(
201 		_peer
202 	) | rpl::start_with_next([=](const TextWithEntities &name) {
203 		window()->setTitle(name.text);
204 	}, _peerLifetime);
205 }
206 
chooseSourceParent()207 QWidget *Panel::chooseSourceParent() {
208 	return window().get();
209 }
210 
chooseSourceActiveDeviceId()211 QString Panel::chooseSourceActiveDeviceId() {
212 	return _call->screenSharingDeviceId();
213 }
214 
chooseSourceActiveWithAudio()215 bool Panel::chooseSourceActiveWithAudio() {
216 	return _call->screenSharingWithAudio();
217 }
218 
chooseSourceWithAudioSupported()219 bool Panel::chooseSourceWithAudioSupported() {
220 #ifdef Q_OS_WIN
221 	return true;
222 #else // Q_OS_WIN
223 	return false;
224 #endif // Q_OS_WIN
225 }
226 
chooseSourceInstanceLifetime()227 rpl::lifetime &Panel::chooseSourceInstanceLifetime() {
228 	return lifetime();
229 }
230 
chooseSourceAccepted(const QString & deviceId,bool withAudio)231 void Panel::chooseSourceAccepted(
232 		const QString &deviceId,
233 		bool withAudio) {
234 	_call->toggleScreenSharing(deviceId, withAudio);
235 }
236 
chooseSourceStop()237 void Panel::chooseSourceStop() {
238 	_call->toggleScreenSharing(std::nullopt);
239 }
240 
initWindow()241 void Panel::initWindow() {
242 	window()->setAttribute(Qt::WA_OpaquePaintEvent);
243 	window()->setAttribute(Qt::WA_NoSystemBackground);
244 	window()->setTitleStyle(st::groupCallTitle);
245 
246 	subscribeToPeerChanges();
247 
248 	base::install_event_filter(window().get(), [=](not_null<QEvent*> e) {
249 		if (e->type() == QEvent::Close && handleClose()) {
250 			e->ignore();
251 			return base::EventFilterResult::Cancel;
252 		} else if (e->type() == QEvent::KeyPress
253 			|| e->type() == QEvent::KeyRelease) {
254 			if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) {
255 				_call->pushToTalk(
256 					e->type() == QEvent::KeyPress,
257 					kSpacePushToTalkDelay);
258 			}
259 		}
260 		return base::EventFilterResult::Continue;
261 	});
262 
263 	window()->setBodyTitleArea([=](QPoint widgetPoint) {
264 		using Flag = Ui::WindowTitleHitTestFlag;
265 		const auto titleRect = QRect(
266 			0,
267 			0,
268 			widget()->width(),
269 			st::groupCallMembersTop);
270 		const auto moveable = (titleRect.contains(widgetPoint)
271 			&& (!_menuToggle || !_menuToggle->geometry().contains(widgetPoint))
272 			&& (!_menu || !_menu->geometry().contains(widgetPoint))
273 			&& (!_recordingMark || !_recordingMark->geometry().contains(widgetPoint))
274 			&& (!_joinAsToggle || !_joinAsToggle->geometry().contains(widgetPoint)));
275 		if (!moveable) {
276 			return (Flag::None | Flag(0));
277 		}
278 		const auto shown = _layerBg->topShownLayer();
279 		return (!shown || !shown->geometry().contains(widgetPoint))
280 			? (Flag::Move | Flag::Maximize)
281 			: Flag::None;
282 	});
283 
284 	_call->hasVideoWithFramesValue(
285 	) | rpl::start_with_next([=] {
286 		updateMode();
287 	}, lifetime());
288 }
289 
initWidget()290 void Panel::initWidget() {
291 	widget()->setMouseTracking(true);
292 
293 	widget()->paintRequest(
294 	) | rpl::start_with_next([=](QRect clip) {
295 		paint(clip);
296 	}, lifetime());
297 
298 	widget()->sizeValue(
299 	) | rpl::skip(1) | rpl::start_with_next([=](QSize size) {
300 		if (!updateMode()) {
301 			updateControlsGeometry();
302 		}
303 
304 		// title geometry depends on _controls->geometry,
305 		// which is not updated here yet.
306 		crl::on_main(widget(), [=] { refreshTitle(); });
307 	}, lifetime());
308 }
309 
endCall()310 void Panel::endCall() {
311 	if (!_call->canManage()) {
312 		_call->hangup();
313 		return;
314 	}
315 	showBox(Box(
316 		LeaveBox,
317 		_call,
318 		false,
319 		BoxContext::GroupCallPanel));
320 }
321 
startScheduledNow()322 void Panel::startScheduledNow() {
323 	const auto date = _call->scheduleDate();
324 	const auto now = base::unixtime::now();
325 	if (!date) {
326 		return;
327 	} else if (now + kStartNoConfirmation >= date) {
328 		_call->startScheduledNow();
329 	} else {
330 		const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
331 		const auto done = [=] {
332 			if (*box) {
333 				(*box)->closeBox();
334 			}
335 			_call->startScheduledNow();
336 		};
337 		auto owned = ConfirmBox({
338 			.text = { (_call->peer()->isBroadcast()
339 				? tr::lng_group_call_start_now_sure_channel
340 				: tr::lng_group_call_start_now_sure)(tr::now) },
341 			.button = tr::lng_group_call_start_now(),
342 			.callback = done,
343 		});
344 		*box = owned.data();
345 		showBox(std::move(owned));
346 	}
347 }
348 
initControls()349 void Panel::initControls() {
350 	_mute->clicks(
351 	) | rpl::filter([=](Qt::MouseButton button) {
352 		return (button == Qt::LeftButton);
353 	}) | rpl::start_with_next([=] {
354 		if (_call->scheduleDate()) {
355 			if (_call->canManage()) {
356 				startScheduledNow();
357 			} else if (const auto real = _call->lookupReal()) {
358 				_call->toggleScheduleStartSubscribed(
359 					!real->scheduleStartSubscribed());
360 			}
361 			return;
362 		}
363 		const auto oldState = _call->muted();
364 		const auto newState = (oldState == MuteState::ForceMuted)
365 			? MuteState::RaisedHand
366 			: (oldState == MuteState::RaisedHand)
367 			? MuteState::RaisedHand
368 			: (oldState == MuteState::Muted)
369 			? MuteState::Active
370 			: MuteState::Muted;
371 		_call->setMutedAndUpdate(newState);
372 	}, _mute->lifetime());
373 
374 	initShareAction();
375 	refreshLeftButton();
376 	refreshVideoButtons();
377 
378 	rpl::combine(
379 		_mode.value(),
380 		_call->canManageValue()
381 	) | rpl::start_with_next([=] {
382 		refreshTopButton();
383 	}, lifetime());
384 
385 	_hangup->setClickedCallback([=] { endCall(); });
386 
387 	const auto scheduleDate = _call->scheduleDate();
388 	if (scheduleDate) {
389 		auto changes = _call->real(
390 		) | rpl::map([=](not_null<Data::GroupCall*> real) {
391 			return real->scheduleDateValue();
392 		}) | rpl::flatten_latest();
393 
394 		setupScheduledLabels(rpl::single(
395 			scheduleDate
396 		) | rpl::then(rpl::duplicate(changes)));
397 
398 		auto started = std::move(changes) | rpl::filter([](TimeId date) {
399 			return (date == 0);
400 		}) | rpl::take(1);
401 
402 		rpl::merge(
403 			rpl::duplicate(started) | rpl::to_empty,
404 			_peer->session().changes().peerFlagsValue(
405 				_peer,
406 				Data::PeerUpdate::Flag::Username
407 			) | rpl::skip(1) | rpl::to_empty
408 		) | rpl::start_with_next([=] {
409 			refreshLeftButton();
410 			updateControlsGeometry();
411 		}, _callLifetime);
412 
413 		std::move(started) | rpl::start_with_next([=] {
414 			refreshVideoButtons();
415 			updateButtonsStyles();
416 			setupMembers();
417 		}, _callLifetime);
418 	}
419 
420 	_call->stateValue(
421 	) | rpl::before_next([=] {
422 		showStickedTooltip();
423 	}) | rpl::filter([](State state) {
424 		return (state == State::HangingUp)
425 			|| (state == State::Ended)
426 			|| (state == State::FailedHangingUp)
427 			|| (state == State::Failed);
428 	}) | rpl::start_with_next([=] {
429 		closeBeforeDestroy();
430 	}, _callLifetime);
431 
432 	_call->levelUpdates(
433 	) | rpl::filter([=](const LevelUpdate &update) {
434 		return update.me;
435 	}) | rpl::start_with_next([=](const LevelUpdate &update) {
436 		_mute->setLevel(update.value);
437 	}, _callLifetime);
438 
439 	_call->real(
440 	) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
441 		setupRealMuteButtonState(real);
442 	}, _callLifetime);
443 
444 	refreshControlsBackground();
445 }
446 
refreshLeftButton()447 void Panel::refreshLeftButton() {
448 	const auto share = _call->scheduleDate()
449 		&& _peer->isBroadcast()
450 		&& _peer->asChannel()->hasUsername();
451 	if ((share && _callShare) || (!share && _settings)) {
452 		return;
453 	}
454 	if (share) {
455 		_settings.destroy();
456 		_callShare.create(widget(), st::groupCallShare);
457 		_callShare->setClickedCallback(_callShareLinkCallback);
458 	} else {
459 		_callShare.destroy();
460 		_settings.create(widget(), st::groupCallSettings);
461 		_settings->setClickedCallback([=] {
462 			showBox(Box(SettingsBox, _call));
463 		});
464 	}
465 	const auto raw = _callShare ? _callShare.data() : _settings.data();
466 	raw->show();
467 	raw->setColorOverrides(_mute->colorOverrides());
468 	updateButtonsStyles();
469 }
470 
refreshVideoButtons(std::optional<bool> overrideWideMode)471 void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
472 	const auto create = overrideWideMode.value_or(mode() == PanelMode::Wide)
473 		|| (!_call->scheduleDate() && _call->videoIsWorking());
474 	const auto created = _video && _screenShare;
475 	if (created == create) {
476 		return;
477 	} else if (created) {
478 		_video.destroy();
479 		_screenShare.destroy();
480 		if (!overrideWideMode) {
481 			updateButtonsGeometry();
482 		}
483 		return;
484 	}
485 	auto toggleableOverrides = [&](rpl::producer<bool> active) {
486 		return rpl::combine(
487 			std::move(active),
488 			_mute->colorOverrides()
489 		) | rpl::map([](bool active, Ui::CallButtonColors colors) {
490 			if (active && colors.bg) {
491 				colors.bg->setAlpha(kOverrideActiveColorBgAlpha);
492 			}
493 			return colors;
494 		});
495 	};
496 	if (!_video) {
497 		_video.create(
498 			widget(),
499 			st::groupCallVideoSmall,
500 			&st::groupCallVideoActiveSmall);
501 		_video->show();
502 		_video->setClickedCallback([=] {
503 			hideStickedTooltip(
504 				StickedTooltip::Camera,
505 				StickedTooltipHide::Activated);
506 			_call->toggleVideo(!_call->isSharingCamera());
507 		});
508 		_video->setColorOverrides(
509 			toggleableOverrides(_call->isSharingCameraValue()));
510 		_call->isSharingCameraValue(
511 		) | rpl::start_with_next([=](bool sharing) {
512 			if (sharing) {
513 				hideStickedTooltip(
514 					StickedTooltip::Camera,
515 					StickedTooltipHide::Activated);
516 			}
517 			_video->setProgress(sharing ? 1. : 0.);
518 		}, _video->lifetime());
519 	}
520 	if (!_screenShare) {
521 		_screenShare.create(widget(), st::groupCallScreenShareSmall);
522 		_screenShare->show();
523 		_screenShare->setClickedCallback([=] {
524 			chooseShareScreenSource();
525 		});
526 		_screenShare->setColorOverrides(
527 			toggleableOverrides(_call->isSharingScreenValue()));
528 		_call->isSharingScreenValue(
529 		) | rpl::start_with_next([=](bool sharing) {
530 			_screenShare->setProgress(sharing ? 1. : 0.);
531 		}, _screenShare->lifetime());
532 	}
533 	if (!_wideMenu) {
534 		_wideMenu.create(widget(), st::groupCallMenuToggleSmall);
535 		_wideMenu->show();
536 		_wideMenu->setClickedCallback([=] { showMainMenu(); });
537 		_wideMenu->setColorOverrides(
538 			toggleableOverrides(_wideMenuShown.value()));
539 	}
540 	updateButtonsStyles();
541 	updateButtonsGeometry();
542 	raiseControls();
543 }
544 
hideStickedTooltip(StickedTooltipHide hide)545 void Panel::hideStickedTooltip(StickedTooltipHide hide) {
546 	if (!_stickedTooltipClose || !_niceTooltipControl) {
547 		return;
548 	}
549 	if (_niceTooltipControl.data() == _video.data()) {
550 		hideStickedTooltip(StickedTooltip::Camera, hide);
551 	} else if (_niceTooltipControl.data() == _mute->outer().get()) {
552 		hideStickedTooltip(StickedTooltip::Microphone, hide);
553 	}
554 }
555 
hideStickedTooltip(StickedTooltip type,StickedTooltipHide hide)556 void Panel::hideStickedTooltip(
557 		StickedTooltip type,
558 		StickedTooltipHide hide) {
559 	if (hide != StickedTooltipHide::Unavailable) {
560 		_stickedTooltipsShown |= type;
561 		if (hide == StickedTooltipHide::Discarded) {
562 			Core::App().settings().setHiddenGroupCallTooltip(type);
563 			Core::App().saveSettingsDelayed();
564 		}
565 	}
566 	const auto control = (type == StickedTooltip::Camera)
567 		? _video.data()
568 		: (type == StickedTooltip::Microphone)
569 		? _mute->outer().get()
570 		: nullptr;
571 	if (_niceTooltipControl.data() == control) {
572 		hideNiceTooltip();
573 	}
574 }
575 
hideNiceTooltip()576 void Panel::hideNiceTooltip() {
577 	if (!_niceTooltip) {
578 		return;
579 	}
580 	_stickedTooltipClose = nullptr;
581 	_niceTooltip.release()->toggleAnimated(false);
582 	_niceTooltipControl = nullptr;
583 }
584 
initShareAction()585 void Panel::initShareAction() {
586 	const auto showBoxCallback = [=](object_ptr<Ui::BoxContent> next) {
587 		showBox(std::move(next));
588 	};
589 	const auto showToastCallback = [=](QString text) {
590 		showToast({ text });
591 	};
592 	auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
593 		_peer,
594 		showBoxCallback,
595 		showToastCallback);
596 	_callShareLinkCallback = [=, callback = std::move(shareLinkCallback)] {
597 		if (_call->lookupReal()) {
598 			callback();
599 		}
600 	};
601 	lifetime().add(std::move(shareLinkLifetime));
602 }
603 
setupRealMuteButtonState(not_null<Data::GroupCall * > real)604 void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
605 	using namespace rpl::mappers;
606 	rpl::combine(
607 		_call->mutedValue() | MapPushToTalkToActive(),
608 		_call->instanceStateValue(),
609 		real->scheduleDateValue(),
610 		real->scheduleStartSubscribedValue(),
611 		_call->canManageValue(),
612 		_mode.value()
613 	) | rpl::distinct_until_changed(
614 	) | rpl::filter(
615 		_2 != GroupCall::InstanceState::TransitionToRtc
616 	) | rpl::start_with_next([=](
617 			MuteState mute,
618 			GroupCall::InstanceState state,
619 			TimeId scheduleDate,
620 			bool scheduleStartSubscribed,
621 			bool canManage,
622 			PanelMode mode) {
623 		const auto wide = (mode == PanelMode::Wide);
624 		using Type = Ui::CallMuteButtonType;
625 		_mute->setState(Ui::CallMuteButtonState{
626 			.text = (wide
627 				? QString()
628 				: scheduleDate
629 				? (canManage
630 					? tr::lng_group_call_start_now(tr::now)
631 					: scheduleStartSubscribed
632 					? tr::lng_group_call_cancel_reminder(tr::now)
633 					: tr::lng_group_call_set_reminder(tr::now))
634 				: state == GroupCall::InstanceState::Disconnected
635 				? tr::lng_group_call_connecting(tr::now)
636 				: mute == MuteState::ForceMuted
637 				? tr::lng_group_call_force_muted(tr::now)
638 				: mute == MuteState::RaisedHand
639 				? tr::lng_group_call_raised_hand(tr::now)
640 				: mute == MuteState::Muted
641 				? tr::lng_group_call_unmute(tr::now)
642 				: tr::lng_group_call_you_are_live(tr::now)),
643 			.tooltip = ((!scheduleDate && mute == MuteState::Muted)
644 				? tr::lng_group_call_unmute_sub(tr::now)
645 				: QString()),
646 			.type = (scheduleDate
647 				? (canManage
648 					? Type::ScheduledCanStart
649 					: scheduleStartSubscribed
650 					? Type::ScheduledNotify
651 					: Type::ScheduledSilent)
652 				: state == GroupCall::InstanceState::Disconnected
653 				? Type::Connecting
654 				: mute == MuteState::ForceMuted
655 				? Type::ForceMuted
656 				: mute == MuteState::RaisedHand
657 				? Type::RaisedHand
658 				: mute == MuteState::Muted
659 				? Type::Muted
660 				: Type::Active),
661 		});
662 	}, _callLifetime);
663 }
664 
setupScheduledLabels(rpl::producer<TimeId> date)665 void Panel::setupScheduledLabels(rpl::producer<TimeId> date) {
666 	using namespace rpl::mappers;
667 	date = std::move(date) | rpl::take_while(_1 != 0);
668 	_startsWhen.create(
669 		widget(),
670 		Ui::StartsWhenText(rpl::duplicate(date)),
671 		st::groupCallStartsWhen);
672 	auto countdownCreated = std::move(
673 		date
674 	) | rpl::map([=](TimeId date) {
675 		_countdownData = std::make_shared<Ui::GroupCallScheduledLeft>(date);
676 		return rpl::empty_value();
677 	}) | rpl::start_spawning(lifetime());
678 
679 	_countdown = Ui::CreateGradientLabel(widget(), rpl::duplicate(
680 		countdownCreated
681 	) | rpl::map([=] {
682 		return _countdownData->text(
683 			Ui::GroupCallScheduledLeft::Negative::Ignore);
684 	}) | rpl::flatten_latest());
685 
686 	_startsIn.create(
687 		widget(),
688 		rpl::conditional(
689 			std::move(
690 				countdownCreated
691 			) | rpl::map(
692 				[=] { return _countdownData->late(); }
693 			) | rpl::flatten_latest(),
694 			tr::lng_group_call_late_by(),
695 			tr::lng_group_call_starts_in()),
696 		st::groupCallStartsIn);
697 
698 	const auto top = [=] {
699 		const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;
700 		const auto membersTop = st::groupCallMembersTop;
701 		const auto height = st::groupCallScheduledBodyHeight;
702 		return (membersTop + (muteTop - membersTop - height) / 2);
703 	};
704 	rpl::combine(
705 		widget()->sizeValue(),
706 		_startsIn->widthValue()
707 	) | rpl::start_with_next([=](QSize size, int width) {
708 		_startsIn->move(
709 			(size.width() - width) / 2,
710 			top() + st::groupCallStartsInTop);
711 	}, _startsIn->lifetime());
712 
713 	rpl::combine(
714 		widget()->sizeValue(),
715 		_startsWhen->widthValue()
716 	) | rpl::start_with_next([=](QSize size, int width) {
717 		_startsWhen->move(
718 			(size.width() - width) / 2,
719 			top() + st::groupCallStartsWhenTop);
720 	}, _startsWhen->lifetime());
721 
722 	rpl::combine(
723 		widget()->sizeValue(),
724 		_countdown->widthValue()
725 	) | rpl::start_with_next([=](QSize size, int width) {
726 		_countdown->move(
727 			(size.width() - width) / 2,
728 			top() + st::groupCallCountdownTop);
729 	}, _startsWhen->lifetime());
730 }
731 
mode() const732 PanelMode Panel::mode() const {
733 	return _mode.current();
734 }
735 
setupMembers()736 void Panel::setupMembers() {
737 	if (_members) {
738 		return;
739 	}
740 
741 	_startsIn.destroy();
742 	_countdown.destroy();
743 	_startsWhen.destroy();
744 
745 	_members.create(widget(), _call, mode(), _window.backend());
746 
747 	setupVideo(_viewport.get());
748 	setupVideo(_members->viewport());
749 	_viewport->mouseInsideValue(
750 	) | rpl::start_with_next([=](bool inside) {
751 		toggleWideControls(inside);
752 	}, _viewport->lifetime());
753 
754 	_members->show();
755 
756 	refreshControlsBackground();
757 	raiseControls();
758 
759 	_members->desiredHeightValue(
760 	) | rpl::start_with_next([=] {
761 		updateMembersGeometry();
762 	}, _members->lifetime());
763 
764 	_members->toggleMuteRequests(
765 	) | rpl::start_with_next([=](MuteRequest request) {
766 		if (_call) {
767 			_call->toggleMute(request);
768 		}
769 	}, _callLifetime);
770 
771 	_members->changeVolumeRequests(
772 	) | rpl::start_with_next([=](VolumeRequest request) {
773 		if (_call) {
774 			_call->changeVolume(request);
775 		}
776 	}, _callLifetime);
777 
778 	_members->kickParticipantRequests(
779 	) | rpl::start_with_next([=](not_null<PeerData*> participantPeer) {
780 		kickParticipant(participantPeer);
781 	}, _callLifetime);
782 
783 	_members->addMembersRequests(
784 	) | rpl::start_with_next([=] {
785 		if (!_peer->isBroadcast()
786 			&& _peer->canWrite()
787 			&& _call->joinAs()->isSelf()) {
788 			addMembers();
789 		} else if (const auto channel = _peer->asChannel()) {
790 			if (channel->hasUsername()) {
791 				_callShareLinkCallback();
792 			}
793 		}
794 	}, _callLifetime);
795 
796 	_call->videoEndpointLargeValue(
797 	) | rpl::start_with_next([=](const VideoEndpoint &large) {
798 		if (large && mode() != PanelMode::Wide) {
799 			enlargeVideo();
800 		}
801 		_viewport->showLarge(large);
802 	}, _callLifetime);
803 }
804 
enlargeVideo()805 void Panel::enlargeVideo() {
806 	_lastSmallGeometry = window()->geometry();
807 
808 	const auto available = window()->screen()->availableGeometry();
809 	const auto width = std::max(
810 		window()->width(),
811 		std::max(
812 			std::min(available.width(), st::groupCallWideModeSize.width()),
813 			st::groupCallWideModeWidthMin));
814 	const auto height = std::max(
815 		window()->height(),
816 		std::min(available.height(), st::groupCallWideModeSize.height()));
817 	auto geometry = QRect(window()->pos(), QSize(width, height));
818 	if (geometry.x() < available.x()) {
819 		geometry.moveLeft(std::min(available.x(), window()->x()));
820 	}
821 	if (geometry.x() + geometry.width()
822 		> available.x() + available.width()) {
823 		geometry.moveLeft(std::max(
824 			available.x() + available.width(),
825 			window()->x() + window()->width()) - geometry.width());
826 	}
827 	if (geometry.y() < available.y()) {
828 		geometry.moveTop(std::min(available.y(), window()->y()));
829 	}
830 	if (geometry.y() + geometry.height() > available.y() + available.height()) {
831 		geometry.moveTop(std::max(
832 			available.y() + available.height(),
833 			window()->y() + window()->height()) - geometry.height());
834 	}
835 	if (_lastLargeMaximized) {
836 		window()->setWindowState(
837 			window()->windowState() | Qt::WindowMaximized);
838 	} else {
839 		window()->setGeometry((_lastLargeGeometry
840 			&& available.intersects(*_lastLargeGeometry))
841 			? *_lastLargeGeometry
842 			: geometry);
843 	}
844 }
845 
minimizeVideo()846 void Panel::minimizeVideo() {
847 	if (window()->windowState() & Qt::WindowMaximized) {
848 		_lastLargeMaximized = true;
849 		window()->setWindowState(
850 			window()->windowState() & ~Qt::WindowMaximized);
851 	} else {
852 		_lastLargeMaximized = false;
853 		_lastLargeGeometry = window()->geometry();
854 	}
855 	const auto available = window()->screen()->availableGeometry();
856 	const auto width = st::groupCallWidth;
857 	const auto height = st::groupCallHeight;
858 	auto geometry = QRect(
859 		window()->x() + (window()->width() - width) / 2,
860 		window()->y() + (window()->height() - height) / 2,
861 		width,
862 		height);
863 	window()->setGeometry((_lastSmallGeometry
864 		&& available.intersects(*_lastSmallGeometry))
865 		? *_lastSmallGeometry
866 		: geometry);
867 }
868 
raiseControls()869 void Panel::raiseControls() {
870 	if (_controlsBackgroundWide) {
871 		_controlsBackgroundWide->raise();
872 	}
873 	if (_controlsBackgroundNarrow) {
874 		_controlsBackgroundNarrow->shadow.raise();
875 		_controlsBackgroundNarrow->blocker.raise();
876 	}
877 	const auto buttons = {
878 		&_settings,
879 		&_callShare,
880 		&_screenShare,
881 		&_wideMenu,
882 		&_video,
883 		&_hangup
884 	};
885 	for (const auto button : buttons) {
886 		if (const auto raw = button->data()) {
887 			raw->raise();
888 		}
889 	}
890 	_mute->raise();
891 	_layerBg->raise();
892 	if (_niceTooltip) {
893 		_niceTooltip->raise();
894 	}
895 }
896 
setupVideo(not_null<Viewport * > viewport)897 void Panel::setupVideo(not_null<Viewport*> viewport) {
898 	const auto setupTile = [=](
899 			const VideoEndpoint &endpoint,
900 			const std::unique_ptr<GroupCall::VideoTrack> &track) {
901 		using namespace rpl::mappers;
902 		const auto row = _members->lookupRow(GroupCall::TrackPeer(track));
903 		Assert(row != nullptr);
904 		auto pinned = rpl::combine(
905 			_call->videoEndpointLargeValue(),
906 			_call->videoEndpointPinnedValue()
907 		) | rpl::map(_1 == endpoint && _2);
908 		viewport->add(
909 			endpoint,
910 			VideoTileTrack{ GroupCall::TrackPointer(track), row },
911 			GroupCall::TrackSizeValue(track),
912 			std::move(pinned));
913 	};
914 	for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
915 		setupTile(endpoint, track);
916 	}
917 	_call->videoStreamActiveUpdates(
918 	) | rpl::start_with_next([=](const VideoStateToggle &update) {
919 		if (update.value) {
920 			// Add async (=> the participant row is definitely in Members).
921 			const auto endpoint = update.endpoint;
922 			crl::on_main(viewport->widget(), [=] {
923 				const auto &tracks = _call->activeVideoTracks();
924 				const auto i = tracks.find(endpoint);
925 				if (i != end(tracks)) {
926 					setupTile(endpoint, i->second);
927 				}
928 			});
929 		} else {
930 			// Remove sync.
931 			viewport->remove(update.endpoint);
932 		}
933 	}, viewport->lifetime());
934 
935 	viewport->pinToggled(
936 	) | rpl::start_with_next([=](bool pinned) {
937 		_call->pinVideoEndpoint(pinned
938 			? _call->videoEndpointLarge()
939 			: VideoEndpoint{});
940 	}, viewport->lifetime());
941 
942 	viewport->clicks(
943 	) | rpl::start_with_next([=](VideoEndpoint &&endpoint) {
944 		if (_call->videoEndpointLarge() == endpoint) {
945 			_call->showVideoEndpointLarge({});
946 		} else if (_call->videoEndpointPinned()) {
947 			_call->pinVideoEndpoint(std::move(endpoint));
948 		} else {
949 			_call->showVideoEndpointLarge(std::move(endpoint));
950 		}
951 	}, viewport->lifetime());
952 
953 	viewport->qualityRequests(
954 	) | rpl::start_with_next([=](const VideoQualityRequest &request) {
955 		_call->requestVideoQuality(request.endpoint, request.quality);
956 	}, viewport->lifetime());
957 }
958 
toggleWideControls(bool shown)959 void Panel::toggleWideControls(bool shown) {
960 	if (_showWideControls == shown) {
961 		return;
962 	}
963 	_showWideControls = shown;
964 	crl::on_main(widget(), [=] {
965 		updateWideControlsVisibility();
966 	});
967 }
968 
updateWideControlsVisibility()969 void Panel::updateWideControlsVisibility() {
970 	const auto shown = _showWideControls
971 		|| (_stickedTooltipClose != nullptr);
972 	if (_wideControlsShown == shown) {
973 		return;
974 	}
975 	_wideControlsShown = shown;
976 	_wideControlsAnimation.start(
977 		[=] { updateButtonsGeometry(); },
978 		_wideControlsShown ? 0. : 1.,
979 		_wideControlsShown ? 1. : 0.,
980 		st::slideWrapDuration);
981 }
982 
subscribeToChanges(not_null<Data::GroupCall * > real)983 void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
984 	const auto livestream = real->peer()->isBroadcast();
985 	const auto validateRecordingMark = [=](bool recording) {
986 		if (!recording && _recordingMark) {
987 			_recordingMark.destroy();
988 		} else if (recording && !_recordingMark) {
989 			struct State {
990 				Ui::Animations::Simple animation;
991 				base::Timer timer;
992 				bool opaque = true;
993 			};
994 			_recordingMark.create(widget());
995 			_recordingMark->show();
996 			const auto state = _recordingMark->lifetime().make_state<State>();
997 			const auto size = st::groupCallRecordingMark;
998 			const auto skip = st::groupCallRecordingMarkSkip;
999 			_recordingMark->resize(size + 2 * skip, size + 2 * skip);
1000 			_recordingMark->setClickedCallback([=] {
1001 				showToast({ (livestream
1002 					? tr::lng_group_call_is_recorded_channel
1003 					: real->recordVideo()
1004 					? tr::lng_group_call_is_recorded_video
1005 					: tr::lng_group_call_is_recorded)(tr::now) });
1006 			});
1007 			const auto animate = [=] {
1008 				const auto opaque = state->opaque;
1009 				state->opaque = !opaque;
1010 				state->animation.start(
1011 					[=] { _recordingMark->update(); },
1012 					opaque ? 1. : kRecordingOpacity,
1013 					opaque ? kRecordingOpacity : 1.,
1014 					kRecordingAnimationDuration);
1015 			};
1016 			state->timer.setCallback(animate);
1017 			state->timer.callEach(kRecordingAnimationDuration);
1018 			animate();
1019 
1020 			_recordingMark->paintRequest(
1021 			) | rpl::start_with_next([=] {
1022 				auto p = QPainter(_recordingMark.data());
1023 				auto hq = PainterHighQualityEnabler(p);
1024 				p.setPen(Qt::NoPen);
1025 				p.setBrush(st::groupCallMemberMutedIcon);
1026 				p.setOpacity(state->animation.value(
1027 					state->opaque ? 1. : kRecordingOpacity));
1028 				p.drawEllipse(skip, skip, size, size);
1029 			}, _recordingMark->lifetime());
1030 		}
1031 		refreshTitleGeometry();
1032 	};
1033 
1034 	using namespace rpl::mappers;
1035 	const auto startedAsVideo = std::make_shared<bool>(real->recordVideo());
1036 	real->recordStartDateChanges(
1037 	) | rpl::map(
1038 		_1 != 0
1039 	) | rpl::distinct_until_changed(
1040 	) | rpl::start_with_next([=](bool recorded) {
1041 		const auto livestream = _call->peer()->isBroadcast();
1042 		const auto isVideo = real->recordVideo();
1043 		if (recorded) {
1044 			*startedAsVideo = isVideo;
1045 		}
1046 		validateRecordingMark(recorded);
1047 		showToast((recorded
1048 			? (livestream
1049 				? tr::lng_group_call_recording_started_channel
1050 				: isVideo
1051 				? tr::lng_group_call_recording_started_video
1052 				: tr::lng_group_call_recording_started)
1053 			: _call->recordingStoppedByMe()
1054 			? ((*startedAsVideo)
1055 				? tr::lng_group_call_recording_saved_video
1056 				: tr::lng_group_call_recording_saved)
1057 			: (livestream
1058 				? tr::lng_group_call_recording_stopped_channel
1059 				: tr::lng_group_call_recording_stopped))(
1060 				tr::now,
1061 				Ui::Text::RichLangValue));
1062 	}, lifetime());
1063 	validateRecordingMark(real->recordStartDate() != 0);
1064 
1065 	rpl::combine(
1066 		_call->videoIsWorkingValue(),
1067 		_call->isSharingCameraValue()
1068 	) | rpl::start_with_next([=] {
1069 		refreshVideoButtons();
1070 		showStickedTooltip();
1071 	}, lifetime());
1072 
1073 	rpl::combine(
1074 		_call->videoIsWorkingValue(),
1075 		_call->isSharingScreenValue()
1076 	) | rpl::start_with_next([=] {
1077 		refreshTopButton();
1078 	}, lifetime());
1079 
1080 	_call->mutedValue(
1081 	) | rpl::skip(1) | rpl::start_with_next([=](MuteState state) {
1082 		updateButtonsGeometry();
1083 		if (state == MuteState::Active
1084 			|| state == MuteState::PushToTalk) {
1085 			hideStickedTooltip(
1086 				StickedTooltip::Microphone,
1087 				StickedTooltipHide::Activated);
1088 		}
1089 		showStickedTooltip();
1090 	}, lifetime());
1091 
1092 	updateControlsGeometry();
1093 }
1094 
refreshTopButton()1095 void Panel::refreshTopButton() {
1096 	if (_mode.current() == PanelMode::Wide) {
1097 		_menuToggle.destroy();
1098 		_joinAsToggle.destroy();
1099 		updateButtonsGeometry(); // _wideMenu <-> _settings
1100 		return;
1101 	}
1102 	const auto hasJoinAs = _call->showChooseJoinAs();
1103 	const auto showNarrowMenu = _call->canManage()
1104 		|| _call->videoIsWorking();
1105 	const auto showNarrowUserpic = !showNarrowMenu && hasJoinAs;
1106 	if (showNarrowMenu) {
1107 		_joinAsToggle.destroy();
1108 		if (!_menuToggle) {
1109 			_menuToggle.create(widget(), st::groupCallMenuToggle);
1110 			_menuToggle->show();
1111 			_menuToggle->setClickedCallback([=] { showMainMenu(); });
1112 			updateControlsGeometry();
1113 		}
1114 	} else if (showNarrowUserpic) {
1115 		_menuToggle.destroy();
1116 		rpl::single(
1117 			_call->joinAs()
1118 		) | rpl::then(_call->rejoinEvents(
1119 		) | rpl::map([](const RejoinEvent &event) {
1120 			return event.nowJoinAs;
1121 		})) | rpl::start_with_next([=](not_null<PeerData*> joinAs) {
1122 			auto joinAsToggle = object_ptr<Ui::UserpicButton>(
1123 				widget(),
1124 				joinAs,
1125 				Ui::UserpicButton::Role::Custom,
1126 				st::groupCallJoinAsToggle);
1127 			_joinAsToggle.destroy();
1128 			_joinAsToggle = std::move(joinAsToggle);
1129 			_joinAsToggle->show();
1130 			_joinAsToggle->setClickedCallback([=] {
1131 				chooseJoinAs();
1132 			});
1133 			updateControlsGeometry();
1134 		}, lifetime());
1135 	} else {
1136 		_menuToggle.destroy();
1137 		_joinAsToggle.destroy();
1138 	}
1139 }
1140 
screenSharingPrivacyRequest()1141 void Panel::screenSharingPrivacyRequest() {
1142 	if (auto box = ScreenSharingPrivacyRequestBox()) {
1143 		showBox(std::move(box));
1144 	}
1145 }
1146 
chooseShareScreenSource()1147 void Panel::chooseShareScreenSource() {
1148 	if (_call->emitShareScreenError()) {
1149 		return;
1150 	}
1151 	const auto choose = [=] {
1152 		if (!Webrtc::DesktopCaptureAllowed()) {
1153 			screenSharingPrivacyRequest();
1154 		} else if (const auto source = Webrtc::UniqueDesktopCaptureSource()) {
1155 			if (_call->isSharingScreen()) {
1156 				_call->toggleScreenSharing(std::nullopt);
1157 			} else {
1158 				chooseSourceAccepted(*source, false);
1159 			}
1160 		} else {
1161 			Ui::DesktopCapture::ChooseSource(this);
1162 		}
1163 	};
1164 	const auto screencastFromPeer = [&]() -> PeerData* {
1165 		for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
1166 			if (endpoint.type == VideoEndpointType::Screen) {
1167 				return endpoint.peer;
1168 			}
1169 		}
1170 		return nullptr;
1171 	}();
1172 	if (!screencastFromPeer || _call->isSharingScreen()) {
1173 		choose();
1174 		return;
1175 	}
1176 	const auto text = tr::lng_group_call_sure_screencast(
1177 		tr::now,
1178 		lt_user,
1179 		screencastFromPeer->shortName());
1180 	const auto shared = std::make_shared<QPointer<Ui::GenericBox>>();
1181 	const auto done = [=] {
1182 		if (*shared) {
1183 			base::take(*shared)->closeBox();
1184 		}
1185 		choose();
1186 	};
1187 	auto box = ConfirmBox({
1188 		.text = { text },
1189 		.button = tr::lng_continue(),
1190 		.callback = done,
1191 	});
1192 	*shared = box.data();
1193 	showBox(std::move(box));
1194 }
1195 
chooseJoinAs()1196 void Panel::chooseJoinAs() {
1197 	const auto context = ChooseJoinAsProcess::Context::Switch;
1198 	const auto callback = [=](JoinInfo info) {
1199 		_call->rejoinAs(info);
1200 	};
1201 	const auto showBoxCallback = [=](object_ptr<Ui::BoxContent> next) {
1202 		showBox(std::move(next));
1203 	};
1204 	const auto showToastCallback = [=](QString text) {
1205 		showToast({ text });
1206 	};
1207 	_joinAsProcess.start(
1208 		_peer,
1209 		context,
1210 		showBoxCallback,
1211 		showToastCallback,
1212 		callback,
1213 		_call->joinAs());
1214 }
1215 
showMainMenu()1216 void Panel::showMainMenu() {
1217 	if (_menu) {
1218 		return;
1219 	}
1220 	const auto wide = (_mode.current() == PanelMode::Wide) && _wideMenu;
1221 	if (!wide && !_menuToggle) {
1222 		return;
1223 	}
1224 	_menu.create(widget(), st::groupCallDropdownMenu);
1225 	FillMenu(
1226 		_menu.data(),
1227 		_peer,
1228 		_call,
1229 		wide,
1230 		[=] { chooseJoinAs(); },
1231 		[=] { chooseShareScreenSource(); },
1232 		[=](auto box) { showBox(std::move(box)); });
1233 	if (_menu->empty()) {
1234 		_wideMenuShown = false;
1235 		_menu.destroy();
1236 		return;
1237 	}
1238 
1239 	const auto raw = _menu.data();
1240 	raw->setHiddenCallback([=] {
1241 		raw->deleteLater();
1242 		if (_menu == raw) {
1243 			_menu = nullptr;
1244 			_wideMenuShown = false;
1245 			_trackControlsMenuLifetime.destroy();
1246 			if (_menuToggle) {
1247 				_menuToggle->setForceRippled(false);
1248 			}
1249 		}
1250 	});
1251 	raw->setShowStartCallback([=] {
1252 		if (_menu == raw) {
1253 			if (wide) {
1254 				_wideMenuShown = true;
1255 			} else if (_menuToggle) {
1256 				_menuToggle->setForceRippled(true);
1257 			}
1258 		}
1259 	});
1260 	raw->setHideStartCallback([=] {
1261 		if (_menu == raw) {
1262 			_wideMenuShown = false;
1263 			if (_menuToggle) {
1264 				_menuToggle->setForceRippled(false);
1265 			}
1266 		}
1267 	});
1268 
1269 	if (wide) {
1270 		_wideMenu->installEventFilter(_menu);
1271 		const auto x = st::groupCallWideMenuPosition.x();
1272 		const auto y = st::groupCallWideMenuPosition.y();
1273 		_menu->moveToLeft(
1274 			_wideMenu->x() + x,
1275 			_wideMenu->y() - _menu->height() + y);
1276 		_menu->showAnimated(Ui::PanelAnimation::Origin::BottomLeft);
1277 		trackControl(_menu, _trackControlsMenuLifetime);
1278 	} else {
1279 		_menuToggle->installEventFilter(_menu);
1280 		const auto x = st::groupCallMenuPosition.x();
1281 		const auto y = st::groupCallMenuPosition.y();
1282 		if (_menuToggle->x() > widget()->width() / 2) {
1283 			_menu->moveToRight(x, y);
1284 			_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
1285 		} else {
1286 			_menu->moveToLeft(x, y);
1287 			_menu->showAnimated(Ui::PanelAnimation::Origin::TopLeft);
1288 		}
1289 	}
1290 }
1291 
addMembers()1292 void Panel::addMembers() {
1293 	const auto showToastCallback = [=](TextWithEntities &&text) {
1294 		showToast(std::move(text));
1295 	};
1296 	if (auto box = PrepareInviteBox(_call, showToastCallback)) {
1297 		showBox(std::move(box));
1298 	}
1299 }
1300 
kickParticipant(not_null<PeerData * > participantPeer)1301 void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
1302 	showBox(Box([=](not_null<Ui::GenericBox*> box) {
1303 		box->addRow(
1304 			object_ptr<Ui::FlatLabel>(
1305 				box.get(),
1306 				(!participantPeer->isUser()
1307 					? (_peer->isBroadcast()
1308 						? tr::lng_group_call_remove_channel_from_channel
1309 						: tr::lng_group_call_remove_channel)(
1310 							tr::now,
1311 							lt_channel,
1312 							participantPeer->name)
1313 					: (_peer->isBroadcast()
1314 						? tr::lng_profile_sure_kick_channel
1315 						: tr::lng_profile_sure_kick)(
1316 							tr::now,
1317 							lt_user,
1318 							participantPeer->asUser()->firstName)),
1319 				st::groupCallBoxLabel),
1320 			style::margins(
1321 				st::boxRowPadding.left(),
1322 				st::boxPadding.top(),
1323 				st::boxRowPadding.right(),
1324 				st::boxPadding.bottom()));
1325 		box->addButton(tr::lng_box_remove(), [=] {
1326 			box->closeBox();
1327 			kickParticipantSure(participantPeer);
1328 		});
1329 		box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
1330 	}));
1331 }
1332 
showBox(object_ptr<Ui::BoxContent> box)1333 void Panel::showBox(object_ptr<Ui::BoxContent> box) {
1334 	showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
1335 }
1336 
showBox(object_ptr<Ui::BoxContent> box,Ui::LayerOptions options,anim::type animated)1337 void Panel::showBox(
1338 		object_ptr<Ui::BoxContent> box,
1339 		Ui::LayerOptions options,
1340 		anim::type animated) {
1341 	hideStickedTooltip(StickedTooltipHide::Unavailable);
1342 	_layerBg->showBox(std::move(box), options, animated);
1343 }
1344 
kickParticipantSure(not_null<PeerData * > participantPeer)1345 void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
1346 	if (const auto chat = _peer->asChat()) {
1347 		chat->session().api().kickParticipant(chat, participantPeer);
1348 	} else if (const auto channel = _peer->asChannel()) {
1349 		const auto currentRestrictedRights = [&] {
1350 			const auto user = participantPeer->asUser();
1351 			if (!channel->mgInfo || !user) {
1352 				return ChatRestrictionsInfo();
1353 			}
1354 			const auto i = channel->mgInfo->lastRestricted.find(user);
1355 			return (i != channel->mgInfo->lastRestricted.cend())
1356 				? i->second.rights
1357 				: ChatRestrictionsInfo();
1358 		}();
1359 		channel->session().api().kickParticipant(
1360 			channel,
1361 			participantPeer,
1362 			currentRestrictedRights);
1363 	}
1364 }
1365 
initLayout()1366 void Panel::initLayout() {
1367 	initGeometry();
1368 
1369 #ifndef Q_OS_MAC
1370 	_controls->raise();
1371 
1372 	Ui::Platform::TitleControlsLayoutChanged(
1373 	) | rpl::start_with_next([=] {
1374 		// _menuToggle geometry depends on _controls arrangement.
1375 		crl::on_main(widget(), [=] { updateControlsGeometry(); });
1376 	}, lifetime());
1377 
1378 #endif // !Q_OS_MAC
1379 }
1380 
showControls()1381 void Panel::showControls() {
1382 	Expects(_call != nullptr);
1383 
1384 	widget()->showChildren();
1385 }
1386 
closeBeforeDestroy()1387 void Panel::closeBeforeDestroy() {
1388 	window()->close();
1389 	_callLifetime.destroy();
1390 }
1391 
lifetime()1392 rpl::lifetime &Panel::lifetime() {
1393 	return window()->lifetime();
1394 }
1395 
initGeometry()1396 void Panel::initGeometry() {
1397 	const auto center = Core::App().getPointForCallPanelCenter();
1398 	const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight);
1399 	window()->setGeometry(rect.translated(center - rect.center()));
1400 	window()->setMinimumSize(rect.size());
1401 	window()->show();
1402 }
1403 
computeTitleRect() const1404 QRect Panel::computeTitleRect() const {
1405 	const auto skip = st::groupCallTitleTop;
1406 	const auto remove = skip + (_menuToggle
1407 		? (_menuToggle->width() + st::groupCallMenuTogglePosition.x())
1408 		: 0) + (_joinAsToggle
1409 			? (_joinAsToggle->width() + st::groupCallMenuTogglePosition.x())
1410 			: 0);
1411 	const auto width = widget()->width();
1412 #ifdef Q_OS_MAC
1413 	return QRect(70, 0, width - remove - 70, 28);
1414 #else // Q_OS_MAC
1415 	const auto controls = _controls->geometry();
1416 	const auto right = controls.x() + controls.width() + skip;
1417 	return (controls.center().x() < width / 2)
1418 		? QRect(right, 0, width - right - remove, controls.height())
1419 		: QRect(remove, 0, controls.x() - skip - remove, controls.height());
1420 #endif // !Q_OS_MAC
1421 }
1422 
updateMode()1423 bool Panel::updateMode() {
1424 	if (!_viewport) {
1425 		return false;
1426 	}
1427 	const auto wide = _call->hasVideoWithFrames()
1428 		&& (widget()->width() >= st::groupCallWideModeWidthMin);
1429 	const auto mode = wide ? PanelMode::Wide : PanelMode::Default;
1430 	if (_mode.current() == mode) {
1431 		return false;
1432 	}
1433 	if (!wide && _call->videoEndpointLarge()) {
1434 		_call->showVideoEndpointLarge({});
1435 	}
1436 	refreshVideoButtons(wide);
1437 	if (!_stickedTooltipClose
1438 		|| _niceTooltipControl.data() != _mute->outer().get()) {
1439 		_niceTooltip.destroy();
1440 	}
1441 	_mode = mode;
1442 	if (_title) {
1443 		_title->setTextColorOverride(wide
1444 			? std::make_optional(st::groupCallMemberNotJoinedStatus->c)
1445 			: std::nullopt);
1446 	}
1447 	if (wide && _subtitle) {
1448 		_subtitle.destroy();
1449 	} else if (!wide && !_subtitle) {
1450 		refreshTitle();
1451 	}
1452 	_wideControlsShown = _showWideControls = true;
1453 	_wideControlsAnimation.stop();
1454 	_viewport->widget()->setVisible(wide);
1455 	if (_members) {
1456 		_members->setMode(mode);
1457 	}
1458 	updateButtonsStyles();
1459 	refreshControlsBackground();
1460 	updateControlsGeometry();
1461 	showStickedTooltip();
1462 	return true;
1463 }
1464 
updateButtonsStyles()1465 void Panel::updateButtonsStyles() {
1466 	const auto wide = (_mode.current() == PanelMode::Wide);
1467 	_mute->setStyle(wide ? st::callMuteButtonSmall : st::callMuteButton);
1468 	if (_video) {
1469 		_video->setStyle(
1470 			wide ? st::groupCallVideoSmall : st::groupCallVideo,
1471 			(wide
1472 				? &st::groupCallVideoActiveSmall
1473 				: &st::groupCallVideoActive));
1474 		_video->setText(wide
1475 			? rpl::single(QString())
1476 			: tr::lng_group_call_video());
1477 	}
1478 	if (_settings) {
1479 		_settings->setText(wide
1480 			? rpl::single(QString())
1481 			: tr::lng_group_call_settings());
1482 		_settings->setStyle(wide
1483 			? st::groupCallSettingsSmall
1484 			: st::groupCallSettings);
1485 	}
1486 	_hangup->setText(wide
1487 		? rpl::single(QString())
1488 		: _call->scheduleDate()
1489 		? tr::lng_group_call_close()
1490 		: tr::lng_group_call_leave());
1491 	_hangup->setStyle(wide
1492 		? st::groupCallHangupSmall
1493 		: st::groupCallHangup);
1494 }
1495 
refreshControlsBackground()1496 void Panel::refreshControlsBackground() {
1497 	if (!_members) {
1498 		return;
1499 	}
1500 	if (mode() == PanelMode::Default) {
1501 		trackControls(false);
1502 		_controlsBackgroundWide.destroy();
1503 		if (_controlsBackgroundNarrow) {
1504 			return;
1505 		}
1506 		setupControlsBackgroundNarrow();
1507 	} else {
1508 		_controlsBackgroundNarrow = nullptr;
1509 		if (_controlsBackgroundWide) {
1510 			return;
1511 		}
1512 		setupControlsBackgroundWide();
1513 	}
1514 	raiseControls();
1515 	updateButtonsGeometry();
1516 }
1517 
setupControlsBackgroundNarrow()1518 void Panel::setupControlsBackgroundNarrow() {
1519 	_controlsBackgroundNarrow = std::make_unique<ControlsBackgroundNarrow>(
1520 		widget());
1521 	_controlsBackgroundNarrow->shadow.show();
1522 	_controlsBackgroundNarrow->blocker.show();
1523 	auto &lifetime = _controlsBackgroundNarrow->shadow.lifetime();
1524 
1525 	const auto factor = cIntRetinaFactor();
1526 	const auto height = std::max(
1527 		st::groupCallMembersShadowHeight,
1528 		st::groupCallMembersFadeSkip + st::groupCallMembersFadeHeight);
1529 	const auto full = lifetime.make_state<QImage>(
1530 		QSize(1, height * factor),
1531 		QImage::Format_ARGB32_Premultiplied);
1532 	rpl::single(
1533 		rpl::empty_value()
1534 	) | rpl::then(
1535 		style::PaletteChanged()
1536 	) | rpl::start_with_next([=] {
1537 		full->fill(Qt::transparent);
1538 
1539 		auto p = QPainter(full);
1540 		const auto bottom = (height - st::groupCallMembersFadeSkip) * factor;
1541 		p.fillRect(
1542 			0,
1543 			bottom,
1544 			full->width(),
1545 			st::groupCallMembersFadeSkip * factor,
1546 			st::groupCallMembersBg);
1547 		p.drawImage(
1548 			QRect(
1549 				0,
1550 				bottom - (st::groupCallMembersFadeHeight * factor),
1551 				full->width(),
1552 				st::groupCallMembersFadeHeight * factor),
1553 			Images::GenerateShadow(
1554 				st::groupCallMembersFadeHeight,
1555 				0,
1556 				255,
1557 				st::groupCallMembersBg->c));
1558 		p.drawImage(
1559 			QRect(
1560 				0,
1561 				(height - st::groupCallMembersShadowHeight) * factor,
1562 				full->width(),
1563 				st::groupCallMembersShadowHeight * factor),
1564 			Images::GenerateShadow(
1565 				st::groupCallMembersShadowHeight,
1566 				0,
1567 				255,
1568 				st::groupCallBg->c));
1569 	}, lifetime);
1570 
1571 	_controlsBackgroundNarrow->shadow.resize(
1572 		(widget()->width()
1573 			- st::groupCallMembersMargin.left()
1574 			- st::groupCallMembersMargin.right()),
1575 		height);
1576 	_controlsBackgroundNarrow->shadow.paintRequest(
1577 	) | rpl::start_with_next([=](QRect clip) {
1578 		auto p = QPainter(&_controlsBackgroundNarrow->shadow);
1579 		clip = clip.intersected(_controlsBackgroundNarrow->shadow.rect());
1580 		const auto inner = _members->getInnerGeometry().translated(
1581 			_members->x() - _controlsBackgroundNarrow->shadow.x(),
1582 			_members->y() - _controlsBackgroundNarrow->shadow.y());
1583 		const auto faded = clip.intersected(inner);
1584 		if (!faded.isEmpty()) {
1585 			const auto factor = cIntRetinaFactor();
1586 			p.drawImage(
1587 				faded,
1588 				*full,
1589 				QRect(
1590 					0,
1591 					faded.y() * factor,
1592 					full->width(),
1593 					faded.height() * factor));
1594 		}
1595 		const auto bottom = inner.y() + inner.height();
1596 		const auto after = clip.intersected(QRect(
1597 			0,
1598 			bottom,
1599 			inner.width(),
1600 			_controlsBackgroundNarrow->shadow.height() - bottom));
1601 		if (!after.isEmpty()) {
1602 			p.fillRect(after, st::groupCallBg);
1603 		}
1604 	}, lifetime);
1605 	_controlsBackgroundNarrow->shadow.setAttribute(
1606 		Qt::WA_TransparentForMouseEvents);
1607 	_controlsBackgroundNarrow->blocker.setUpdatesEnabled(false);
1608 }
1609 
setupControlsBackgroundWide()1610 void Panel::setupControlsBackgroundWide() {
1611 	_controlsBackgroundWide.create(widget());
1612 	_controlsBackgroundWide->show();
1613 	auto &lifetime = _controlsBackgroundWide->lifetime();
1614 	const auto color = lifetime.make_state<style::complex_color>([] {
1615 		auto result = st::groupCallBg->c;
1616 		result.setAlphaF(kControlsBackgroundOpacity);
1617 		return result;
1618 	});
1619 	const auto corners = lifetime.make_state<Ui::RoundRect>(
1620 		st::groupCallControlsBackRadius,
1621 		color->color());
1622 	_controlsBackgroundWide->paintRequest(
1623 	) | rpl::start_with_next([=] {
1624 		auto p = QPainter(_controlsBackgroundWide.data());
1625 		corners->paint(p, _controlsBackgroundWide->rect());
1626 	}, lifetime);
1627 
1628 	trackControls(true);
1629 }
1630 
trackControl(Ui::RpWidget * widget,rpl::lifetime & lifetime)1631 void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
1632 	if (!widget) {
1633 		return;
1634 	}
1635 	widget->events(
1636 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
1637 		if (e->type() == QEvent::Enter) {
1638 			trackControlOver(widget, true);
1639 		} else if (e->type() == QEvent::Leave) {
1640 			trackControlOver(widget, false);
1641 		}
1642 	}, lifetime);
1643 }
1644 
trackControlOver(not_null<Ui::RpWidget * > control,bool over)1645 void Panel::trackControlOver(not_null<Ui::RpWidget*> control, bool over) {
1646 	if (_stickedTooltipClose) {
1647 		if (!over) {
1648 			return;
1649 		}
1650 	} else {
1651 		hideNiceTooltip();
1652 	}
1653 	if (over) {
1654 		Ui::Integration::Instance().registerLeaveSubscription(control);
1655 		showNiceTooltip(control);
1656 	} else {
1657 		Ui::Integration::Instance().unregisterLeaveSubscription(control);
1658 	}
1659 	toggleWideControls(over);
1660 }
1661 
showStickedTooltip()1662 void Panel::showStickedTooltip() {
1663 	static const auto kHasCamera = !Webrtc::GetVideoInputList().empty();
1664 	const auto callReady = (_call->state() == State::Joined
1665 		|| _call->state() == State::Connecting);
1666 	if (!(_stickedTooltipsShown & StickedTooltip::Camera)
1667 		&& callReady
1668 		&& (_mode.current() == PanelMode::Wide)
1669 		&& _video
1670 		&& _call->videoIsWorking()
1671 		&& !_call->mutedByAdmin()
1672 		&& kHasCamera) { // Don't recount this every time for now.
1673 		showNiceTooltip(_video, NiceTooltipType::Sticked);
1674 		return;
1675 	}
1676 	hideStickedTooltip(
1677 		StickedTooltip::Camera,
1678 		StickedTooltipHide::Unavailable);
1679 
1680 	if (!(_stickedTooltipsShown & StickedTooltip::Microphone)
1681 		&& callReady
1682 		&& _mute
1683 		&& !_call->mutedByAdmin()
1684 		&& !_layerBg->topShownLayer()) {
1685 		if (_stickedTooltipClose) {
1686 			// Showing already.
1687 			return;
1688 		} else if (!_micLevelTester) {
1689 			// Check if there is incoming sound.
1690 			_micLevelTester = std::make_unique<MicLevelTester>([=] {
1691 				showStickedTooltip();
1692 			});
1693 		}
1694 		if (_micLevelTester->showTooltip()) {
1695 			_micLevelTester = nullptr;
1696 			showNiceTooltip(_mute->outer(), NiceTooltipType::Sticked);
1697 		}
1698 		return;
1699 	}
1700 	_micLevelTester = nullptr;
1701 	hideStickedTooltip(
1702 		StickedTooltip::Microphone,
1703 		StickedTooltipHide::Unavailable);
1704 }
1705 
showNiceTooltip(not_null<Ui::RpWidget * > control,NiceTooltipType type)1706 void Panel::showNiceTooltip(
1707 		not_null<Ui::RpWidget*> control,
1708 		NiceTooltipType type) {
1709 	auto text = [&]() -> rpl::producer<QString> {
1710 		if (control == _screenShare.data()) {
1711 			if (_call->mutedByAdmin()) {
1712 				return nullptr;
1713 			}
1714 			return tr::lng_group_call_tooltip_screen();
1715 		} else if (control == _video.data()) {
1716 			if (_call->mutedByAdmin()) {
1717 				return nullptr;
1718 			}
1719 			return _call->isSharingCameraValue(
1720 			) | rpl::map([=](bool sharing) {
1721 				return sharing
1722 					? tr::lng_group_call_tooltip_camera_off()
1723 					: tr::lng_group_call_tooltip_camera();
1724 			}) | rpl::flatten_latest();
1725 		} else if (control == _settings.data()) {
1726 			return tr::lng_group_call_settings();
1727 		} else if (control == _mute->outer()) {
1728 			return MuteButtonTooltip(_call);
1729 		} else if (control == _hangup.data()) {
1730 			return tr::lng_group_call_leave();
1731 		}
1732 		return rpl::producer<QString>();
1733 	}();
1734 	if (!text || _stickedTooltipClose) {
1735 		return;
1736 	} else if (_wideControlsAnimation.animating() || !_wideControlsShown) {
1737 		if (type == NiceTooltipType::Normal) {
1738 			return;
1739 		}
1740 	}
1741 	const auto inner = [&]() -> Ui::RpWidget* {
1742 		const auto normal = (type == NiceTooltipType::Normal);
1743 		auto container = normal
1744 			? nullptr
1745 			: Ui::CreateChild<Ui::RpWidget>(widget().get());
1746 		const auto label = Ui::CreateChild<Ui::FlatLabel>(
1747 			(normal ? widget().get() : container),
1748 			std::move(text),
1749 			st::groupCallNiceTooltipLabel);
1750 		if (normal) {
1751 			return label;
1752 		}
1753 		const auto button = Ui::CreateChild<Ui::IconButton>(
1754 			container,
1755 			st::groupCallStickedTooltipClose);
1756 		rpl::combine(
1757 			label->sizeValue(),
1758 			button->sizeValue()
1759 		) | rpl::start_with_next([=](QSize text, QSize close) {
1760 			const auto height = std::max(text.height(), close.height());
1761 			container->resize(text.width() + close.width(), height);
1762 			label->move(0, (height - text.height()) / 2);
1763 			button->move(text.width(), (height - close.height()) / 2);
1764 		}, container->lifetime());
1765 		button->setClickedCallback([=] {
1766 			hideStickedTooltip(StickedTooltipHide::Discarded);
1767 		});
1768 		_stickedTooltipClose = button;
1769 		updateWideControlsVisibility();
1770 		return container;
1771 	}();
1772 	_niceTooltip.create(
1773 		widget().get(),
1774 		object_ptr<Ui::RpWidget>::fromRaw(inner),
1775 		(type == NiceTooltipType::Sticked
1776 			? st::groupCallStickedTooltip
1777 			: st::groupCallNiceTooltip));
1778 	const auto tooltip = _niceTooltip.data();
1779 	const auto weak = QPointer<QWidget>(tooltip);
1780 	const auto destroy = [=] {
1781 		delete weak.data();
1782 	};
1783 	if (type != NiceTooltipType::Sticked) {
1784 		tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
1785 	}
1786 	tooltip->setHiddenCallback(destroy);
1787 	base::qt_signal_producer(
1788 		control.get(),
1789 		&QObject::destroyed
1790 	) | rpl::start_with_next(destroy, tooltip->lifetime());
1791 
1792 	_niceTooltipControl = control;
1793 	updateTooltipGeometry();
1794 	tooltip->toggleAnimated(true);
1795 }
1796 
updateTooltipGeometry()1797 void Panel::updateTooltipGeometry() {
1798 	if (!_niceTooltip) {
1799 		return;
1800 	} else if (!_niceTooltipControl) {
1801 		hideNiceTooltip();
1802 		return;
1803 	}
1804 	const auto geometry = _niceTooltipControl->geometry();
1805 	const auto weak = QPointer<QWidget>(_niceTooltip);
1806 	const auto countPosition = [=](QSize size) {
1807 		const auto strong = weak.data();
1808 		const auto wide = (_mode.current() == PanelMode::Wide);
1809 		const auto top = geometry.y()
1810 			- (wide ? st::groupCallNiceTooltipTop : 0)
1811 			- size.height();
1812 		const auto middle = geometry.center().x();
1813 		if (!strong) {
1814 			return QPoint();
1815 		} else if (!wide) {
1816 			return QPoint(
1817 				std::max(
1818 					std::min(
1819 						middle - size.width() / 2,
1820 						(widget()->width()
1821 							- st::groupCallMembersMargin.right()
1822 							- size.width())),
1823 					st::groupCallMembersMargin.left()),
1824 				top);
1825 		}
1826 		const auto back = _controlsBackgroundWide.data();
1827 		if (size.width() >= _viewport->widget()->width()) {
1828 			return QPoint(_viewport->widget()->x(), top);
1829 		} else if (back && size.width() >= back->width()) {
1830 			return QPoint(
1831 				back->x() - (size.width() - back->width()) / 2,
1832 				top);
1833 		} else if (back && (middle - back->x() < size.width() / 2)) {
1834 			return QPoint(back->x(), top);
1835 		} else if (back
1836 			&& (back->x() + back->width() - middle < size.width() / 2)) {
1837 			return QPoint(back->x() + back->width() - size.width(), top);
1838 		} else {
1839 			return QPoint(middle - size.width() / 2, top);
1840 		}
1841 	};
1842 	_niceTooltip->pointAt(geometry, RectPart::Top, countPosition);
1843 }
1844 
trackControls(bool track)1845 void Panel::trackControls(bool track) {
1846 	if (_trackControls == track) {
1847 		return;
1848 	}
1849 	_trackControls = track;
1850 	if (!track) {
1851 		_trackControlsLifetime.destroy();
1852 		_trackControlsOverStateLifetime.destroy();
1853 		_trackControlsMenuLifetime.destroy();
1854 		toggleWideControls(true);
1855 		if (_wideControlsAnimation.animating()) {
1856 			_wideControlsAnimation.stop();
1857 			updateButtonsGeometry();
1858 		}
1859 		return;
1860 	}
1861 
1862 	const auto trackOne = [=](auto &&widget) {
1863 		trackControl(widget, _trackControlsOverStateLifetime);
1864 	};
1865 	trackOne(_mute->outer());
1866 	trackOne(_video);
1867 	trackOne(_screenShare);
1868 	trackOne(_wideMenu);
1869 	trackOne(_settings);
1870 	trackOne(_hangup);
1871 	trackOne(_controlsBackgroundWide);
1872 	trackControl(_menu, _trackControlsMenuLifetime);
1873 }
1874 
updateControlsGeometry()1875 void Panel::updateControlsGeometry() {
1876 	if (widget()->size().isEmpty() || (!_settings && !_callShare)) {
1877 		return;
1878 	}
1879 	updateButtonsGeometry();
1880 	updateMembersGeometry();
1881 	refreshTitle();
1882 
1883 #ifdef Q_OS_MAC
1884 	const auto controlsOnTheLeft = true;
1885 #else // Q_OS_MAC
1886 	const auto controlsOnTheLeft = _controls->geometry().center().x()
1887 		< widget()->width() / 2;
1888 #endif // Q_OS_MAC
1889 	const auto menux = st::groupCallMenuTogglePosition.x();
1890 	const auto menuy = st::groupCallMenuTogglePosition.y();
1891 	if (controlsOnTheLeft) {
1892 		if (_menuToggle) {
1893 			_menuToggle->moveToRight(menux, menuy);
1894 		} else if (_joinAsToggle) {
1895 			_joinAsToggle->moveToRight(menux, menuy);
1896 		}
1897 	} else {
1898 		if (_menuToggle) {
1899 			_menuToggle->moveToLeft(menux, menuy);
1900 		} else if (_joinAsToggle) {
1901 			_joinAsToggle->moveToLeft(menux, menuy);
1902 		}
1903 	}
1904 }
1905 
updateButtonsGeometry()1906 void Panel::updateButtonsGeometry() {
1907 	if (widget()->size().isEmpty() || (!_settings && !_callShare)) {
1908 		return;
1909 	}
1910 	const auto toggle = [](auto &widget, bool shown) {
1911 		if (widget && widget->isHidden() == shown) {
1912 			widget->setVisible(shown);
1913 		}
1914 	};
1915 	if (mode() == PanelMode::Wide) {
1916 		Assert(_video != nullptr);
1917 		Assert(_screenShare != nullptr);
1918 		Assert(_wideMenu != nullptr);
1919 		Assert(_settings != nullptr);
1920 		Assert(_callShare == nullptr);
1921 
1922 		const auto shown = _wideControlsAnimation.value(
1923 			_wideControlsShown ? 1. : 0.);
1924 		const auto hidden = (shown == 0.);
1925 
1926 		if (_viewport) {
1927 			_viewport->setControlsShown(shown);
1928 		}
1929 
1930 		const auto buttonsTop = widget()->height() - anim::interpolate(
1931 			0,
1932 			st::groupCallButtonBottomSkipWide,
1933 			shown);
1934 		const auto addSkip = st::callMuteButtonSmall.active.outerRadius;
1935 		const auto muteSize = _mute->innerSize().width() + 2 * addSkip;
1936 		const auto skip = st::groupCallButtonSkipSmall;
1937 		const auto fullWidth = (_video->width() + skip)
1938 			+ (_screenShare->width() + skip)
1939 			+ (muteSize + skip)
1940 			+ (_settings ->width() + skip)
1941 			+ _hangup->width();
1942 		const auto membersSkip = st::groupCallNarrowSkip;
1943 		const auto membersWidth = st::groupCallNarrowMembersWidth
1944 			+ 2 * membersSkip;
1945 		auto left = membersSkip + (widget()->width()
1946 			- membersWidth
1947 			- membersSkip
1948 			- fullWidth) / 2;
1949 		toggle(_screenShare, !hidden);
1950 		_screenShare->moveToLeft(left, buttonsTop);
1951 		left += _screenShare->width() + skip;
1952 		toggle(_video, !hidden);
1953 		_video->moveToLeft(left, buttonsTop);
1954 		left += _video->width() + skip;
1955 		toggle(_mute, !hidden);
1956 		_mute->moveInner({ left + addSkip, buttonsTop + addSkip });
1957 		left += muteSize + skip;
1958 		const auto wideMenuShown = _call->canManage()
1959 			|| _call->showChooseJoinAs();
1960 		toggle(_settings, !hidden && !wideMenuShown);
1961 		toggle(_wideMenu, !hidden && wideMenuShown);
1962 		_wideMenu->moveToLeft(left, buttonsTop);
1963 		_settings->moveToLeft(left, buttonsTop);
1964 		left += _settings->width() + skip;
1965 		toggle(_hangup, !hidden);
1966 		_hangup->moveToLeft(left, buttonsTop);
1967 		left += _hangup->width();
1968 		if (_controlsBackgroundWide) {
1969 			const auto rect = QRect(
1970 				left - fullWidth,
1971 				buttonsTop,
1972 				fullWidth,
1973 				_hangup->height());
1974 			_controlsBackgroundWide->setGeometry(
1975 				rect.marginsAdded(st::groupCallControlsBackMargin));
1976 		}
1977 	} else {
1978 		const auto muteTop = widget()->height()
1979 			- st::groupCallMuteBottomSkip;
1980 		const auto buttonsTop = widget()->height()
1981 			- st::groupCallButtonBottomSkip;
1982 		const auto muteSize = _mute->innerSize().width();
1983 		const auto fullWidth = muteSize
1984 			+ 2 * (_settings ? _settings : _callShare)->width()
1985 			+ 2 * st::groupCallButtonSkip;
1986 		toggle(_mute, true);
1987 		_mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop });
1988 		const auto leftButtonLeft = (widget()->width() - fullWidth) / 2;
1989 		toggle(_screenShare, false);
1990 		toggle(_wideMenu, false);
1991 		toggle(_callShare, true);
1992 		if (_callShare) {
1993 			_callShare->moveToLeft(leftButtonLeft, buttonsTop);
1994 		}
1995 		const auto showVideoButton = videoButtonInNarrowMode();
1996 		toggle(_video, !_callShare && showVideoButton);
1997 		if (_video) {
1998 			_video->setStyle(st::groupCallVideo, &st::groupCallVideoActive);
1999 			_video->moveToLeft(leftButtonLeft, buttonsTop);
2000 		}
2001 		toggle(_settings, !_callShare && !showVideoButton);
2002 		if (_settings) {
2003 			_settings->moveToLeft(leftButtonLeft, buttonsTop);
2004 		}
2005 		toggle(_hangup, true);
2006 		_hangup->moveToRight(leftButtonLeft, buttonsTop);
2007 	}
2008 	if (_controlsBackgroundNarrow) {
2009 		const auto left = st::groupCallMembersMargin.left();
2010 		const auto width = (widget()->width()
2011 			- st::groupCallMembersMargin.left()
2012 			- st::groupCallMembersMargin.right());
2013 		_controlsBackgroundNarrow->shadow.setGeometry(
2014 			left,
2015 			(widget()->height()
2016 				- st::groupCallMembersMargin.bottom()
2017 				- _controlsBackgroundNarrow->shadow.height()),
2018 			width,
2019 			_controlsBackgroundNarrow->shadow.height());
2020 		_controlsBackgroundNarrow->blocker.setGeometry(
2021 			left,
2022 			(widget()->height()
2023 				- st::groupCallMembersMargin.bottom()
2024 				- st::groupCallMembersBottomSkip),
2025 			width,
2026 			st::groupCallMembersBottomSkip);
2027 	}
2028 	updateTooltipGeometry();
2029 }
2030 
videoButtonInNarrowMode() const2031 bool Panel::videoButtonInNarrowMode() const {
2032 	return (_video != nullptr) && !_call->mutedByAdmin();
2033 }
2034 
updateMembersGeometry()2035 void Panel::updateMembersGeometry() {
2036 	if (!_members) {
2037 		return;
2038 	}
2039 	const auto desiredHeight = _members->desiredHeight();
2040 	if (mode() == PanelMode::Wide) {
2041 		const auto skip = st::groupCallNarrowSkip;
2042 		const auto membersWidth = st::groupCallNarrowMembersWidth;
2043 		const auto top = st::groupCallWideVideoTop;
2044 		_members->setGeometry(
2045 			widget()->width() - skip - membersWidth,
2046 			top,
2047 			membersWidth,
2048 			std::min(desiredHeight, widget()->height() - top - skip));
2049 		_viewport->setGeometry({
2050 			skip,
2051 			top,
2052 			widget()->width() - membersWidth - 3 * skip,
2053 			widget()->height() - top - skip,
2054 		});
2055 	} else {
2056 		const auto membersBottom = widget()->height();
2057 		const auto membersTop = st::groupCallMembersTop;
2058 		const auto availableHeight = membersBottom
2059 			- st::groupCallMembersMargin.bottom()
2060 			- membersTop;
2061 		const auto membersWidthAvailable = widget()->width()
2062 			- st::groupCallMembersMargin.left()
2063 			- st::groupCallMembersMargin.right();
2064 		const auto membersWidthMin = st::groupCallWidth
2065 			- st::groupCallMembersMargin.left()
2066 			- st::groupCallMembersMargin.right();
2067 		const auto membersWidth = std::clamp(
2068 			membersWidthAvailable,
2069 			membersWidthMin,
2070 			st::groupCallMembersWidthMax);
2071 		_members->setGeometry(
2072 			(widget()->width() - membersWidth) / 2,
2073 			membersTop,
2074 			membersWidth,
2075 			std::min(desiredHeight, availableHeight));
2076 	}
2077 }
2078 
refreshTitle()2079 void Panel::refreshTitle() {
2080 	if (!_title) {
2081 		auto text = rpl::combine(
2082 			Info::Profile::NameValue(_peer),
2083 			rpl::single(
2084 				QString()
2085 			) | rpl::then(_call->real(
2086 			) | rpl::map([=](not_null<Data::GroupCall*> real) {
2087 				return real->titleValue();
2088 			}) | rpl::flatten_latest())
2089 		) | rpl::map([=](
2090 				const TextWithEntities &name,
2091 				const QString &title) {
2092 			return title.isEmpty() ? name.text : title;
2093 		}) | rpl::after_next([=] {
2094 			refreshTitleGeometry();
2095 		});
2096 		_title.create(
2097 			widget(),
2098 			rpl::duplicate(text),
2099 			st::groupCallTitleLabel);
2100 		_title->show();
2101 		_title->setAttribute(Qt::WA_TransparentForMouseEvents);
2102 	}
2103 	refreshTitleGeometry();
2104 	if (!_subtitle && mode() == PanelMode::Default) {
2105 		_subtitle.create(
2106 			widget(),
2107 			rpl::single(
2108 				_call->scheduleDate()
2109 			) | rpl::then(
2110 				_call->real(
2111 				) | rpl::map([=](not_null<Data::GroupCall*> real) {
2112 					return real->scheduleDateValue();
2113 				}) | rpl::flatten_latest()
2114 			) | rpl::map([=](TimeId scheduleDate) {
2115 				if (scheduleDate) {
2116 					return tr::lng_group_call_scheduled_status();
2117 				} else if (!_members) {
2118 					setupMembers();
2119 				}
2120 				return tr::lng_group_call_members(
2121 					lt_count_decimal,
2122 					_members->fullCountValue() | rpl::map([](int value) {
2123 						return (value > 0) ? float64(value) : 1.;
2124 					}));
2125 			}) | rpl::flatten_latest(),
2126 			st::groupCallSubtitleLabel);
2127 		_subtitle->show();
2128 		_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
2129 	}
2130 	if (_subtitle) {
2131 		const auto top = _title
2132 			? st::groupCallSubtitleTop
2133 			: st::groupCallTitleTop;
2134 		_subtitle->moveToLeft(
2135 			(widget()->width() - _subtitle->width()) / 2,
2136 			top);
2137 	}
2138 }
2139 
refreshTitleGeometry()2140 void Panel::refreshTitleGeometry() {
2141 	if (!_title) {
2142 		return;
2143 	}
2144 	const auto fullRect = computeTitleRect();
2145 	const auto titleRect = _recordingMark
2146 		? QRect(
2147 			fullRect.x(),
2148 			fullRect.y(),
2149 			fullRect.width() - _recordingMark->width(),
2150 			fullRect.height())
2151 		: fullRect;
2152 	const auto best = _title->naturalWidth();
2153 	const auto from = (widget()->width() - best) / 2;
2154 	const auto top = (mode() == PanelMode::Default)
2155 		? st::groupCallTitleTop
2156 		: (st::groupCallWideVideoTop
2157 			- st::groupCallTitleLabel.style.font->height) / 2;
2158 	const auto left = titleRect.x();
2159 	if (from >= left && from + best <= left + titleRect.width()) {
2160 		_title->resizeToWidth(best);
2161 		_title->moveToLeft(from, top);
2162 	} else if (titleRect.width() < best) {
2163 		_title->resizeToWidth(titleRect.width());
2164 		_title->moveToLeft(left, top);
2165 	} else if (from < left) {
2166 		_title->resizeToWidth(best);
2167 		_title->moveToLeft(left, top);
2168 	} else {
2169 		_title->resizeToWidth(best);
2170 		_title->moveToLeft(left + titleRect.width() - best, top);
2171 	}
2172 	if (_recordingMark) {
2173 		const auto markTop = top + st::groupCallRecordingMarkTop;
2174 		_recordingMark->move(
2175 			_title->x() + _title->width(),
2176 			markTop - st::groupCallRecordingMarkSkip);
2177 	}
2178 }
2179 
paint(QRect clip)2180 void Panel::paint(QRect clip) {
2181 	Painter p(widget());
2182 
2183 	auto region = QRegion(clip);
2184 	for (const auto &rect : region) {
2185 		p.fillRect(rect, st::groupCallBg);
2186 	}
2187 }
2188 
handleClose()2189 bool Panel::handleClose() {
2190 	if (_call) {
2191 		window()->hide();
2192 		return true;
2193 	}
2194 	return false;
2195 }
2196 
window() const2197 not_null<Ui::RpWindow*> Panel::window() const {
2198 	return _window.window();
2199 }
2200 
widget() const2201 not_null<Ui::RpWidget*> Panel::widget() const {
2202 	return _window.widget();
2203 }
2204 
2205 } // namespace Calls::Group
2206