AyuGramDesktop/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
2021-05-30 19:14:08 +04:00

2460 lines
68 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_panel.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_members.h"
#include "calls/group/calls_group_settings.h"
#include "calls/group/calls_group_menu.h"
#include "calls/group/calls_group_viewport.h"
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/controls/call_mute_button.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/window.h"
#include "ui/widgets/call_button.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/tooltip.h"
#include "ui/chat/group_call_bar.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toasts/common_toasts.h"
#include "ui/round_rect.h"
#include "ui/special_buttons.h"
#include "info/profile/info_profile_values.h" // Info::Profile::Value.
#include "core/application.h"
#include "lang/lang_keys.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "main/main_session.h"
#include "base/event_filter.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/add_participants_box.h"
#include "boxes/peer_lists_box.h"
#include "boxes/confirm_box.h"
#include "base/unixtime.h"
#include "base/qt_signal_producer.h"
#include "base/timer_rpl.h"
#include "app.h"
#include "apiwrap.h" // api().kickParticipant.
#include "webrtc/webrtc_video_track.h"
#include "styles/style_calls.h"
#include "styles/style_layers.h"
#include <QtWidgets/QDesktopWidget>
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
#include <QtGui/QScreen>
namespace Calls::Group {
namespace {
constexpr auto kSpacePushToTalkDelay = crl::time(250);
constexpr auto kRecordingAnimationDuration = crl::time(1200);
constexpr auto kRecordingOpacity = 0.6;
constexpr auto kStartNoConfirmation = TimeId(10);
constexpr auto kControlsBackgroundOpacity = 0.8;
constexpr auto kOverrideActiveColorBgAlpha = 172;
constexpr auto kErrorDuration = 2 * crl::time(1000);
class InviteController final : public ParticipantsBoxController {
public:
InviteController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void itemDeselectedHook(not_null<PeerData*> peer) override;
[[nodiscard]] auto peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*>;
[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;
[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;
private:
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const override;
not_null<PeerData*> _peer;
const base::flat_set<not_null<UserData*>> _alreadyIn;
mutable base::flat_set<not_null<UserData*>> _inGroup;
rpl::event_stream<not_null<UserData*>> _rowAdded;
};
class InviteContactsController final : public AddParticipantsBoxController {
public:
InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> discoveredInGroup);
private:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
bool needsInviteLinkButton() override {
return false;
}
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
rpl::producer<not_null<UserData*>> _discoveredInGroup;
rpl::lifetime _lifetime;
};
[[nodiscard]] rpl::producer<QString> StartsWhenText(
rpl::producer<TimeId> date) {
return std::move(
date
) | rpl::map([](TimeId date) -> rpl::producer<QString> {
const auto parsedDate = base::unixtime::parse(date);
const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0));
const auto previousDay = QDateTime(
parsedDate.date().addDays(-1),
QTime(0, 0));
const auto now = QDateTime::currentDateTime();
const auto kDay = int64(24 * 60 * 60);
const auto tillTomorrow = int64(now.secsTo(previousDay));
const auto tillToday = tillTomorrow + kDay;
const auto tillAfter = tillToday + kDay;
const auto time = parsedDate.time().toString(
QLocale::system().timeFormat(QLocale::ShortFormat));
auto exact = tr::lng_group_call_starts_short_date(
lt_date,
rpl::single(langDayOfMonthFull(dateDay.date())),
lt_time,
rpl::single(time)
) | rpl::type_erased();
auto tomorrow = tr::lng_group_call_starts_short_tomorrow(
lt_time,
rpl::single(time));
auto today = tr::lng_group_call_starts_short_today(
lt_time,
rpl::single(time));
auto todayAndAfter = rpl::single(
std::move(today)
) | rpl::then(base::timer_once(
std::min(tillAfter, kDay) * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(exact);
})) | rpl::flatten_latest() | rpl::type_erased();
auto tomorrowAndAfter = rpl::single(
std::move(tomorrow)
) | rpl::then(base::timer_once(
std::min(tillToday, kDay) * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(todayAndAfter);
})) | rpl::flatten_latest() | rpl::type_erased();
auto full = rpl::single(
rpl::duplicate(exact)
) | rpl::then(base::timer_once(
tillTomorrow * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(tomorrowAndAfter);
})) | rpl::flatten_latest() | rpl::type_erased();
if (tillTomorrow > 0) {
return full;
} else if (tillToday > 0) {
return tomorrowAndAfter;
} else if (tillAfter > 0) {
return todayAndAfter;
} else {
return exact;
}
}) | rpl::flatten_latest();
}
[[nodiscard]] object_ptr<Ui::RpWidget> CreateGradientLabel(
QWidget *parent,
rpl::producer<QString> text) {
struct State {
QBrush brush;
QPainterPath path;
};
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto state = raw->lifetime().make_state<State>();
std::move(
text
) | rpl::start_with_next([=](const QString &text) {
state->path = QPainterPath();
const auto &font = st::groupCallCountdownFont;
state->path.addText(0, font->ascent, font->f, text);
const auto width = font->width(text);
raw->resize(width, font->height);
auto gradient = QLinearGradient(QPoint(width, 0), QPoint());
gradient.setStops(QGradientStops{
{ 0.0, st::groupCallForceMutedBar1->c },
{ .7, st::groupCallForceMutedBar2->c },
{ 1.0, st::groupCallForceMutedBar3->c }
});
state->brush = QBrush(std::move(gradient));
raw->update();
}, raw->lifetime());
raw->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto skip = st::groupCallWidth / 20;
const auto available = parent->width() - 2 * skip;
const auto full = raw->width();
if (available > 0 && full > available) {
const auto scale = available / float64(full);
const auto shift = raw->rect().center();
p.translate(shift);
p.scale(scale, scale);
p.translate(-shift);
}
p.setPen(Qt::NoPen);
p.setBrush(state->brush);
p.drawPath(state->path);
}, raw->lifetime());
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> CreateSectionSubtitle(
QWidget *parent,
rpl::producer<QString> text) {
auto result = object_ptr<Ui::FixedHeightWidget>(
parent,
st::searchedBarHeight);
const auto raw = result.data();
raw->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(raw);
p.fillRect(clip, st::groupCallMembersBgOver);
}, raw->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
std::move(text),
st::groupCallBoxLabel);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto padding = st::groupCallInviteDividerPadding;
const auto available = width - padding.left() - padding.right();
label->resizeToNaturalWidth(available);
label->moveToLeft(padding.left(), padding.top(), width);
}, label->lifetime());
return result;
}
InviteController::InviteController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn)
: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members)
, _peer(peer)
, _alreadyIn(std::move(alreadyIn)) {
SubscribeToMigration(
_peer,
lifetime(),
[=](not_null<ChannelData*> channel) { _peer = channel; });
}
void InviteController::prepare() {
delegate()->peerListSetHideEmpty(true);
ParticipantsBoxController::prepare();
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_members()));
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_members()));
}
void InviteController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListSetRowChecked(row, !row->checked());
}
base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
}
bool InviteController::hasRowFor(not_null<PeerData*> peer) const {
return (delegate()->peerListFindRow(peer->id.value) != nullptr);
}
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
return _alreadyIn.contains(user);
}
std::unique_ptr<PeerListRow> InviteController::createRow(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user
|| user->isSelf()
|| user->isBot()
|| (user->flags() & MTPDuser::Flag::f_deleted)) {
return nullptr;
}
auto result = std::make_unique<PeerListRow>(user);
_rowAdded.fire_copy(user);
_inGroup.emplace(user);
if (isAlreadyIn(user)) {
result->setDisabledState(PeerListRow::State::DisabledChecked);
}
return result;
}
auto InviteController::peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*> {
return &_inGroup;
}
rpl::producer<not_null<UserData*>> InviteController::rowAdded() const {
return _rowAdded.events();
}
InviteContactsController::InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> discoveredInGroup)
: AddParticipantsBoxController(peer, std::move(alreadyIn))
, _inGroup(inGroup)
, _discoveredInGroup(std::move(discoveredInGroup)) {
}
void InviteContactsController::prepareViewHook() {
AddParticipantsBoxController::prepareViewHook();
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
nullptr,
tr::lng_contacts_header()));
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_search_results()));
std::move(
_discoveredInGroup
) | rpl::start_with_next([=](not_null<UserData*> user) {
if (auto row = delegate()->peerListFindRow(user->id.value)) {
delegate()->peerListRemoveRow(row);
}
}, _lifetime);
}
std::unique_ptr<PeerListRow> InviteContactsController::createRow(
not_null<UserData*> user) {
return _inGroup->contains(user)
? nullptr
: AddParticipantsBoxController::createRow(user);
}
} // namespace
struct Panel::ControlsBackgroundNarrow {
explicit ControlsBackgroundNarrow(not_null<QWidget*> parent)
: shadow(parent)
, blocker(parent) {
}
Ui::RpWidget shadow;
Ui::RpWidget blocker;
};
Panel::Panel(not_null<GroupCall*> call)
: _call(call)
, _peer(call->peer())
, _window(std::make_unique<Ui::Window>())
, _layerBg(std::make_unique<Ui::LayerManager>(_window->body()))
#ifndef Q_OS_MAC
, _controls(std::make_unique<Ui::Platform::TitleControls>(
_window->body(),
st::groupCallTitle))
#endif // !Q_OS_MAC
, _viewport(std::make_unique<Viewport>(widget(), PanelMode::Wide))
, _mute(std::make_unique<Ui::CallMuteButton>(
widget(),
st::callMuteButton,
Core::App().appDeactivatedValue(),
Ui::CallMuteButtonState{
.text = (_call->scheduleDate()
? tr::lng_group_call_start_now(tr::now)
: tr::lng_group_call_connecting(tr::now)),
.type = (!_call->scheduleDate()
? Ui::CallMuteButtonType::Connecting
: _peer->canManageGroupCall()
? Ui::CallMuteButtonType::ScheduledCanStart
: _call->scheduleStartSubscribed()
? Ui::CallMuteButtonType::ScheduledNotify
: Ui::CallMuteButtonType::ScheduledSilent),
}))
, _hangup(widget(), st::groupCallHangup) {
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_layerBg->setHideByBackgroundClick(true);
_viewport->widget()->hide();
if (!_viewport->requireARGB32()) {
_call->setNotRequireARGB32();
}
SubscribeToMigration(
_peer,
_window->lifetime(),
[=](not_null<ChannelData*> channel) { migrate(channel); });
setupRealCallViewers();
initWindow();
initWidget();
initControls();
initLayout();
showAndActivate();
setupToasts();
}
Panel::~Panel() {
_menu.destroy();
_viewport = nullptr;
}
void Panel::setupRealCallViewers() {
_call->real(
) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
subscribeToChanges(real);
}, _window->lifetime());
}
bool Panel::isActive() const {
return _window->isActiveWindow()
&& _window->isVisible()
&& !(_window->windowState() & Qt::WindowMinimized);
}
void Panel::minimize() {
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
}
void Panel::close() {
_window->close();
}
void Panel::showAndActivate() {
if (_window->isHidden()) {
_window->show();
}
const auto state = _window->windowState();
if (state & Qt::WindowMinimized) {
_window->setWindowState(state & ~Qt::WindowMinimized);
}
_window->raise();
_window->activateWindow();
_window->setFocus();
}
void Panel::migrate(not_null<ChannelData*> channel) {
_peer = channel;
_peerLifetime.destroy();
subscribeToPeerChanges();
_title.destroy();
refreshTitle();
}
void Panel::subscribeToPeerChanges() {
Info::Profile::NameValue(
_peer
) | rpl::start_with_next([=](const TextWithEntities &name) {
_window->setTitle(name.text);
}, _peerLifetime);
}
QWidget *Panel::chooseSourceParent() {
return _window.get();
}
QString Panel::chooseSourceActiveDeviceId() {
return _call->screenSharingDeviceId();
}
rpl::lifetime &Panel::chooseSourceInstanceLifetime() {
return _window->lifetime();
}
void Panel::chooseSourceAccepted(const QString &deviceId) {
_call->toggleScreenSharing(deviceId);
}
void Panel::chooseSourceStop() {
_call->toggleScreenSharing(std::nullopt);
}
void Panel::initWindow() {
_window->setAttribute(Qt::WA_OpaquePaintEvent);
_window->setAttribute(Qt::WA_NoSystemBackground);
_window->setWindowIcon(
QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
_window->setTitleStyle(st::groupCallTitle);
subscribeToPeerChanges();
base::install_event_filter(_window.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close && handleClose()) {
e->ignore();
return base::EventFilterResult::Cancel;
} else if (e->type() == QEvent::KeyPress
|| e->type() == QEvent::KeyRelease) {
if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) {
_call->pushToTalk(
e->type() == QEvent::KeyPress,
kSpacePushToTalkDelay);
}
}
return base::EventFilterResult::Continue;
});
_window->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
const auto titleRect = QRect(
0,
0,
widget()->width(),
st::groupCallMembersTop);
return (titleRect.contains(widgetPoint)
&& (!_menuToggle || !_menuToggle->geometry().contains(widgetPoint))
&& (!_menu || !_menu->geometry().contains(widgetPoint))
&& (!_recordingMark || !_recordingMark->geometry().contains(widgetPoint))
&& (!_joinAsToggle || !_joinAsToggle->geometry().contains(widgetPoint)))
? (Flag::Move | Flag::Maximize)
: Flag::None;
});
_call->hasVideoWithFramesValue(
) | rpl::start_with_next([=] {
updateMode();
}, _window->lifetime());
}
void Panel::initWidget() {
widget()->setMouseTracking(true);
widget()->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
paint(clip);
}, widget()->lifetime());
widget()->sizeValue(
) | rpl::skip(1) | rpl::start_with_next([=](QSize size) {
if (!updateMode()) {
updateControlsGeometry();
}
// title geometry depends on _controls->geometry,
// which is not updated here yet.
crl::on_main(widget(), [=] { refreshTitle(); });
}, widget()->lifetime());
}
void Panel::endCall() {
if (!_call->canManage()) {
_call->hangup();
return;
}
_layerBg->showBox(Box(
LeaveBox,
_call,
false,
BoxContext::GroupCallPanel));
}
void Panel::startScheduledNow() {
const auto date = _call->scheduleDate();
const auto now = base::unixtime::now();
if (!date) {
return;
} else if (now + kStartNoConfirmation >= date) {
_call->startScheduledNow();
} else {
const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
const auto done = [=] {
if (*box) {
(*box)->closeBox();
}
_call->startScheduledNow();
};
auto owned = ConfirmBox({
.text = { tr::lng_group_call_start_now_sure(tr::now) },
.button = tr::lng_group_call_start_now(),
.callback = done,
});
*box = owned.data();
_layerBg->showBox(std::move(owned));
}
}
void Panel::initControls() {
_mute->clicks(
) | rpl::filter([=](Qt::MouseButton button) {
return (button == Qt::LeftButton);
}) | rpl::start_with_next([=] {
if (_call->scheduleDate()) {
if (_call->canManage()) {
startScheduledNow();
} else if (const auto real = _call->lookupReal()) {
_call->toggleScheduleStartSubscribed(
!real->scheduleStartSubscribed());
}
return;
}
const auto oldState = _call->muted();
const auto newState = (oldState == MuteState::ForceMuted)
? MuteState::RaisedHand
: (oldState == MuteState::RaisedHand)
? MuteState::RaisedHand
: (oldState == MuteState::Muted)
? MuteState::Active
: MuteState::Muted;
_call->setMutedAndUpdate(newState);
}, _mute->lifetime());
initShareAction();
refreshLeftButton();
refreshVideoButtons();
rpl::combine(
_mode.value(),
_call->canManageValue()
) | rpl::start_with_next([=] {
refreshTopButton();
}, widget()->lifetime());
_hangup->setClickedCallback([=] { endCall(); });
const auto scheduleDate = _call->scheduleDate();
if (scheduleDate) {
auto changes = _call->real(
) | rpl::map([=](not_null<Data::GroupCall*> real) {
return real->scheduleDateValue();
}) | rpl::flatten_latest();
setupScheduledLabels(rpl::single(
scheduleDate
) | rpl::then(rpl::duplicate(changes)));
auto started = std::move(changes) | rpl::filter([](TimeId date) {
return (date == 0);
}) | rpl::take(1);
rpl::merge(
rpl::duplicate(started) | rpl::to_empty,
_peer->session().changes().peerFlagsValue(
_peer,
Data::PeerUpdate::Flag::Username
) | rpl::skip(1) | rpl::to_empty
) | rpl::start_with_next([=] {
refreshLeftButton();
updateControlsGeometry();
}, _callLifetime);
std::move(started) | rpl::start_with_next([=] {
refreshVideoButtons();
updateButtonsStyles();
setupMembers();
}, _callLifetime);
}
_call->stateValue(
) | rpl::filter([](State state) {
return (state == State::HangingUp)
|| (state == State::Ended)
|| (state == State::FailedHangingUp)
|| (state == State::Failed);
}) | rpl::start_with_next([=] {
closeBeforeDestroy();
}, _callLifetime);
_call->levelUpdates(
) | rpl::filter([=](const LevelUpdate &update) {
return update.me;
}) | rpl::start_with_next([=](const LevelUpdate &update) {
_mute->setLevel(update.value);
}, _callLifetime);
_call->real(
) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
setupRealMuteButtonState(real);
}, _callLifetime);
refreshControlsBackground();
}
void Panel::refreshLeftButton() {
const auto share = _call->scheduleDate()
&& _peer->isBroadcast()
&& _peer->asChannel()->hasUsername();
if ((share && _callShare) || (!share && _settings)) {
return;
}
if (share) {
_settings.destroy();
_callShare.create(widget(), st::groupCallShare);
_callShare->setClickedCallback(_callShareLinkCallback);
} else {
_callShare.destroy();
_settings.create(widget(), st::groupCallSettings);
_settings->setClickedCallback([=] {
_layerBg->showBox(Box(SettingsBox, _call));
});
}
const auto raw = _callShare ? _callShare.data() : _settings.data();
raw->show();
raw->setColorOverrides(_mute->colorOverrides());
updateButtonsStyles();
}
void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
const auto real = _call->lookupReal();
const auto canStartVideo = !_call->scheduleDate()
&& real
&& real->canStartVideo();
const auto create = overrideWideMode.value_or(mode() == PanelMode::Wide)
|| canStartVideo;
const auto created = _video && _screenShare;
if (created == create) {
return;
} else if (created) {
_video.destroy();
_screenShare.destroy();
updateButtonsGeometry();
return;
}
auto toggleableOverrides = [&](rpl::producer<bool> active) {
return rpl::combine(
std::move(active),
_mute->colorOverrides()
) | rpl::map([](bool active, Ui::CallButtonColors colors) {
if (active && colors.bg) {
colors.bg->setAlpha(kOverrideActiveColorBgAlpha);
}
return colors;
});
};
if (!_video) {
_video.create(
widget(),
st::groupCallVideoSmall,
&st::groupCallVideoActiveSmall);
_video->show();
_video->setClickedCallback([=] {
_call->toggleVideo(!_call->isSharingCamera());
});
_video->setColorOverrides(
toggleableOverrides(_call->isSharingCameraValue()));
_call->isSharingCameraValue(
) | rpl::start_with_next([=](bool sharing) {
_video->setProgress(sharing ? 1. : 0.);
}, _video->lifetime());
_call->mutedValue(
) | rpl::start_with_next([=] {
updateButtonsGeometry();
}, _video->lifetime());
}
if (!_screenShare) {
_screenShare.create(widget(), st::groupCallScreenShareSmall);
_screenShare->show();
_screenShare->setClickedCallback([=] {
chooseShareScreenSource();
});
_screenShare->setColorOverrides(
toggleableOverrides(_call->isSharingScreenValue()));
_call->isSharingScreenValue(
) | rpl::start_with_next([=](bool sharing) {
_screenShare->setProgress(sharing ? 1. : 0.);
}, _screenShare->lifetime());
}
if (!_wideMenu) {
_wideMenu.create(widget(), st::groupCallMenuToggleSmall);
_wideMenu->show();
_wideMenu->setClickedCallback([=] { showMainMenu(); });
_wideMenu->setColorOverrides(
toggleableOverrides(_wideMenuShown.value()));
}
updateButtonsStyles();
updateButtonsGeometry();
}
void Panel::initShareAction() {
const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
_peer,
showBox,
showToast);
_callShareLinkCallback = [=, callback = std::move(shareLinkCallback)] {
if (_call->lookupReal()) {
callback();
}
};
widget()->lifetime().add(std::move(shareLinkLifetime));
}
void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
using namespace rpl::mappers;
rpl::combine(
_call->mutedValue() | MapPushToTalkToActive(),
_call->instanceStateValue(),
real->scheduleDateValue(),
real->scheduleStartSubscribedValue(),
_call->canManageValue(),
_mode.value()
) | rpl::distinct_until_changed(
) | rpl::filter(
_2 != GroupCall::InstanceState::TransitionToRtc
) | rpl::start_with_next([=](
MuteState mute,
GroupCall::InstanceState state,
TimeId scheduleDate,
bool scheduleStartSubscribed,
bool canManage,
PanelMode mode) {
const auto wide = (mode == PanelMode::Wide);
using Type = Ui::CallMuteButtonType;
_mute->setState(Ui::CallMuteButtonState{
.text = (wide
? QString()
: scheduleDate
? (canManage
? tr::lng_group_call_start_now(tr::now)
: scheduleStartSubscribed
? tr::lng_group_call_cancel_reminder(tr::now)
: tr::lng_group_call_set_reminder(tr::now))
: state == GroupCall::InstanceState::Disconnected
? tr::lng_group_call_connecting(tr::now)
: mute == MuteState::ForceMuted
? tr::lng_group_call_force_muted(tr::now)
: mute == MuteState::RaisedHand
? tr::lng_group_call_raised_hand(tr::now)
: mute == MuteState::Muted
? tr::lng_group_call_unmute(tr::now)
: tr::lng_group_call_you_are_live(tr::now)),
.tooltip = ((!scheduleDate && mute == MuteState::Muted)
? tr::lng_group_call_unmute_sub(tr::now)
: QString()),
.type = (scheduleDate
? (canManage
? Type::ScheduledCanStart
: scheduleStartSubscribed
? Type::ScheduledNotify
: Type::ScheduledSilent)
: state == GroupCall::InstanceState::Disconnected
? Type::Connecting
: mute == MuteState::ForceMuted
? Type::ForceMuted
: mute == MuteState::RaisedHand
? Type::RaisedHand
: mute == MuteState::Muted
? Type::Muted
: Type::Active),
});
}, _callLifetime);
}
void Panel::setupScheduledLabels(rpl::producer<TimeId> date) {
using namespace rpl::mappers;
date = std::move(date) | rpl::take_while(_1 != 0);
_startsWhen.create(
widget(),
StartsWhenText(rpl::duplicate(date)),
st::groupCallStartsWhen);
auto countdownCreated = std::move(
date
) | rpl::map([=](TimeId date) {
_countdownData = std::make_shared<Ui::GroupCallScheduledLeft>(date);
return rpl::empty_value();
}) | rpl::start_spawning(widget()->lifetime());
_countdown = CreateGradientLabel(widget(), rpl::duplicate(
countdownCreated
) | rpl::map([=] {
return _countdownData->text(
Ui::GroupCallScheduledLeft::Negative::Ignore);
}) | rpl::flatten_latest());
_startsIn.create(
widget(),
rpl::conditional(
std::move(
countdownCreated
) | rpl::map(
[=] { return _countdownData->late(); }
) | rpl::flatten_latest(),
tr::lng_group_call_late_by(),
tr::lng_group_call_starts_in()),
st::groupCallStartsIn);
const auto top = [=] {
const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;
const auto membersTop = st::groupCallMembersTop;
const auto height = st::groupCallScheduledBodyHeight;
return (membersTop + (muteTop - membersTop - height) / 2);
};
rpl::combine(
widget()->sizeValue(),
_startsIn->widthValue()
) | rpl::start_with_next([=](QSize size, int width) {
_startsIn->move(
(size.width() - width) / 2,
top() + st::groupCallStartsInTop);
}, _startsIn->lifetime());
rpl::combine(
widget()->sizeValue(),
_startsWhen->widthValue()
) | rpl::start_with_next([=](QSize size, int width) {
_startsWhen->move(
(size.width() - width) / 2,
top() + st::groupCallStartsWhenTop);
}, _startsWhen->lifetime());
rpl::combine(
widget()->sizeValue(),
_countdown->widthValue()
) | rpl::start_with_next([=](QSize size, int width) {
_countdown->move(
(size.width() - width) / 2,
top() + st::groupCallCountdownTop);
}, _startsWhen->lifetime());
}
PanelMode Panel::mode() const {
return _mode.current();
}
void Panel::setupMembers() {
if (_members) {
return;
}
_startsIn.destroy();
_countdown.destroy();
_startsWhen.destroy();
_members.create(widget(), _call, mode());
setupVideo(_viewport.get());
setupVideo(_members->viewport());
_viewport->mouseInsideValue(
) | rpl::start_with_next([=](bool inside) {
toggleWideControls(inside);
}, _viewport->lifetime());
_members->show();
refreshControlsBackground();
raiseControls();
_members->desiredHeightValue(
) | rpl::start_with_next([=] {
updateMembersGeometry();
}, _members->lifetime());
_members->toggleMuteRequests(
) | rpl::start_with_next([=](MuteRequest request) {
if (_call) {
_call->toggleMute(request);
}
}, _callLifetime);
_members->changeVolumeRequests(
) | rpl::start_with_next([=](VolumeRequest request) {
if (_call) {
_call->changeVolume(request);
}
}, _callLifetime);
_members->kickParticipantRequests(
) | rpl::start_with_next([=](not_null<PeerData*> participantPeer) {
kickParticipant(participantPeer);
}, _callLifetime);
_members->addMembersRequests(
) | rpl::start_with_next([=] {
if (_peer->isBroadcast() && _peer->asChannel()->hasUsername()) {
_callShareLinkCallback();
} else {
addMembers();
}
}, _callLifetime);
_call->videoEndpointLargeValue(
) | rpl::start_with_next([=](const VideoEndpoint &large) {
if (large && mode() != PanelMode::Wide) {
enlargeVideo();
}
_viewport->showLarge(large);
}, _callLifetime);
}
void Panel::enlargeVideo() {
_lastSmallGeometry = _window->geometry();
const auto available = _window->screen()->availableGeometry();
const auto width = std::max(
_window->width(),
std::max(
std::min(available.width(), st::groupCallWideModeSize.width()),
st::groupCallWideModeWidthMin));
const auto height = std::max(
_window->height(),
std::min(available.height(), st::groupCallWideModeSize.height()));
auto geometry = QRect(_window->pos(), QSize(width, height));
if (geometry.x() < available.x()) {
geometry.setX(std::min(available.x(), _window->x()));
}
if (geometry.x() + geometry.width()
> available.x() + available.width()) {
geometry.setX(std::max(
available.x() + available.width(),
_window->x() + _window->width()) - geometry.width());
}
if (geometry.y() < available.y()) {
geometry.setY(std::min(available.y(), _window->y()));
}
if (geometry.y() + geometry.height() > available.y() + available.height()) {
geometry.setY(std::max(
available.y() + available.height(),
_window->y() + _window->height()) - geometry.height());
}
if (_lastLargeMaximized) {
_window->setWindowState(
_window->windowState() | Qt::WindowMaximized);
} else {
_window->setGeometry((_lastLargeGeometry
&& available.intersects(*_lastLargeGeometry))
? *_lastLargeGeometry
: geometry);
}
}
void Panel::minimizeVideo() {
if (_window->windowState() & Qt::WindowMaximized) {
_lastLargeMaximized = true;
_window->setWindowState(
_window->windowState() & ~Qt::WindowMaximized);
} else {
_lastLargeMaximized = false;
_lastLargeGeometry = _window->geometry();
}
const auto available = _window->screen()->availableGeometry();
const auto width = st::groupCallWidth;
const auto height = st::groupCallHeight;
auto geometry = QRect(
_window->x() + (_window->width() - width) / 2,
_window->y() + (_window->height() - height) / 2,
width,
height);
_window->setGeometry((_lastSmallGeometry
&& available.intersects(*_lastSmallGeometry))
? *_lastSmallGeometry
: geometry);
}
void Panel::raiseControls() {
if (_controlsBackgroundWide) {
_controlsBackgroundWide->raise();
}
if (_controlsBackgroundNarrow) {
_controlsBackgroundNarrow->shadow.raise();
_controlsBackgroundNarrow->blocker.raise();
}
const auto buttons = {
&_settings,
&_callShare,
&_screenShare,
&_wideMenu,
&_video,
&_hangup
};
for (const auto button : buttons) {
if (const auto raw = button->data()) {
raw->raise();
}
}
_mute->raise();
}
void Panel::setupVideo(not_null<Viewport*> viewport) {
const auto setupTile = [=](
const VideoEndpoint &endpoint,
const GroupCall::VideoTrack &track) {
using namespace rpl::mappers;
const auto row = _members->lookupRow(track.peer);
Assert(row != nullptr);
auto pinned = rpl::combine(
_call->videoEndpointLargeValue(),
_call->videoEndpointPinnedValue()
) | rpl::map(_1 == endpoint && _2);
viewport->add(
endpoint,
VideoTileTrack{ track.track.get(), row },
std::move(pinned));
};
for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
setupTile(endpoint, track);
}
_call->videoStreamActiveUpdates(
) | rpl::start_with_next([=](const VideoActiveToggle &update) {
if (update.active) {
// Add async (=> the participant row is definitely in Members).
const auto endpoint = update.endpoint;
crl::on_main(viewport->widget(), [=] {
const auto &tracks = _call->activeVideoTracks();
const auto i = tracks.find(endpoint);
if (i != end(tracks)) {
setupTile(endpoint, i->second);
}
});
} else {
// Remove sync.
viewport->remove(update.endpoint);
}
}, viewport->lifetime());
viewport->pinToggled(
) | rpl::start_with_next([=](bool pinned) {
_call->pinVideoEndpoint(pinned
? _call->videoEndpointLarge()
: VideoEndpoint{});
}, viewport->lifetime());
viewport->clicks(
) | rpl::start_with_next([=](VideoEndpoint &&endpoint) {
if (_call->videoEndpointLarge() == endpoint) {
_call->showVideoEndpointLarge({});
} else if (_call->videoEndpointPinned()) {
_call->pinVideoEndpoint(std::move(endpoint));
} else {
_call->showVideoEndpointLarge(std::move(endpoint));
}
}, viewport->lifetime());
viewport->qualityRequests(
) | rpl::start_with_next([=](const VideoQualityRequest &request) {
_call->requestVideoQuality(request.endpoint, request.quality);
}, viewport->lifetime());
}
void Panel::toggleWideControls(bool shown) {
if (_showWideControls == shown) {
return;
}
_showWideControls = shown;
crl::on_main(widget(), [=] {
if (_wideControlsShown == _showWideControls) {
return;
}
_wideControlsShown = _showWideControls;
_wideControlsAnimation.start(
[=] { updateButtonsGeometry(); },
_wideControlsShown ? 0. : 1.,
_wideControlsShown ? 1. : 0.,
st::slideWrapDuration);
});
}
void Panel::setupToasts() {
setupJoinAsChangedToasts();
setupTitleChangedToasts();
setupRequestedToSpeakToasts();
setupAllowedToSpeakToasts();
setupErrorToasts();
}
void Panel::setupJoinAsChangedToasts() {
_call->rejoinEvents(
) | rpl::filter([](RejoinEvent event) {
return (event.wasJoinAs != event.nowJoinAs);
}) | rpl::map([=] {
return _call->stateValue() | rpl::filter([](State state) {
return (state == State::Joined);
}) | rpl::take(1);
}) | rpl::flatten_latest() | rpl::start_with_next([=] {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_join_as_changed(
tr::now,
lt_name,
Ui::Text::Bold(_call->joinAs()->name),
Ui::Text::WithEntities),
});
}, widget()->lifetime());
}
void Panel::setupTitleChangedToasts() {
_call->titleChanged(
) | rpl::filter([=] {
return (_call->lookupReal() != nullptr);
}) | rpl::map([=] {
return _peer->groupCall()->title().isEmpty()
? _peer->name
: _peer->groupCall()->title();
}) | rpl::start_with_next([=](const QString &title) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_title_changed(
tr::now,
lt_title,
Ui::Text::Bold(title),
Ui::Text::WithEntities),
});
}, widget()->lifetime());
}
void Panel::setupAllowedToSpeakToasts() {
_call->allowedToSpeakNotifications(
) | rpl::start_with_next([=] {
if (isActive()) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_can_speak_here(tr::now) },
});
} else {
const auto real = _call->lookupReal();
const auto name = (real && !real->title().isEmpty())
? real->title()
: _peer->name;
Ui::ShowMultilineToast({
.text = tr::lng_group_call_can_speak(
tr::now,
lt_chat,
Ui::Text::Bold(name),
Ui::Text::WithEntities),
});
}
}, widget()->lifetime());
}
void Panel::setupRequestedToSpeakToasts() {
_call->mutedValue(
) | rpl::combine_previous(
) | rpl::start_with_next([=](MuteState was, MuteState now) {
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_tooltip_raised_hand(tr::now),
});
}
}, widget()->lifetime());
}
void Panel::setupErrorToasts() {
_call->errors(
) | rpl::start_with_next([=](Error error) {
const auto key = [&] {
switch (error) {
case Error::NoCamera: return tr::lng_call_error_no_camera;
case Error::ScreenFailed:
return tr::lng_group_call_failed_screen;
case Error::MutedNoCamera:
return tr::lng_group_call_muted_no_camera;
case Error::MutedNoScreen:
return tr::lng_group_call_muted_no_screen;
case Error::DisabledNoCamera:
return tr::lng_group_call_chat_no_camera;
case Error::DisabledNoScreen:
return tr::lng_group_call_chat_no_screen;
}
Unexpected("Error in Calls::Group::Panel::setupErrorToasts.");
}();
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { key(tr::now) },
.duration = kErrorDuration,
});
}, widget()->lifetime());
}
void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
const auto validateRecordingMark = [=](bool recording) {
if (!recording && _recordingMark) {
_recordingMark.destroy();
} else if (recording && !_recordingMark) {
struct State {
Ui::Animations::Simple animation;
base::Timer timer;
bool opaque = true;
};
_recordingMark.create(widget());
_recordingMark->show();
const auto state = _recordingMark->lifetime().make_state<State>();
const auto size = st::groupCallRecordingMark;
const auto skip = st::groupCallRecordingMarkSkip;
_recordingMark->resize(size + 2 * skip, size + 2 * skip);
_recordingMark->setClickedCallback([=] {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_is_recorded(tr::now) },
});
});
const auto animate = [=] {
const auto opaque = state->opaque;
state->opaque = !opaque;
state->animation.start(
[=] { _recordingMark->update(); },
opaque ? 1. : kRecordingOpacity,
opaque ? kRecordingOpacity : 1.,
kRecordingAnimationDuration);
};
state->timer.setCallback(animate);
state->timer.callEach(kRecordingAnimationDuration);
animate();
_recordingMark->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(_recordingMark.data());
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::groupCallMemberMutedIcon);
p.setOpacity(state->animation.value(
state->opaque ? 1. : kRecordingOpacity));
p.drawEllipse(skip, skip, size, size);
}, _recordingMark->lifetime());
}
refreshTitleGeometry();
};
using namespace rpl::mappers;
real->recordStartDateChanges(
) | rpl::map(
_1 != 0
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool recorded) {
validateRecordingMark(recorded);
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = (recorded
? tr::lng_group_call_recording_started
: _call->recordingStoppedByMe()
? tr::lng_group_call_recording_saved
: tr::lng_group_call_recording_stopped)(
tr::now,
Ui::Text::RichLangValue),
});
}, widget()->lifetime());
validateRecordingMark(real->recordStartDate() != 0);
real->canStartVideoValue(
) | rpl::start_with_next([=] {
refreshVideoButtons();
refreshTopButton();
}, widget()->lifetime());
updateControlsGeometry();
}
void Panel::refreshTopButton() {
if (_mode.current() == PanelMode::Wide) {
_menuToggle.destroy();
_joinAsToggle.destroy();
updateButtonsGeometry(); // _wideMenu <-> _settings
return;
}
const auto real = _call->lookupReal();
const auto hasJoinAs = _call->showChooseJoinAs();
const auto wide = (_mode.current() == PanelMode::Wide);
const auto showNarrowMenu = _call->canManage()
|| (real && real->canStartVideo());
const auto showNarrowUserpic = !showNarrowMenu && hasJoinAs;
if (showNarrowMenu) {
_joinAsToggle.destroy();
if (!_menuToggle) {
_menuToggle.create(widget(), st::groupCallMenuToggle);
_menuToggle->show();
_menuToggle->setClickedCallback([=] { showMainMenu(); });
updateControlsGeometry();
}
} else if (showNarrowUserpic) {
_menuToggle.destroy();
rpl::single(
_call->joinAs()
) | rpl::then(_call->rejoinEvents(
) | rpl::map([](const RejoinEvent &event) {
return event.nowJoinAs;
})) | rpl::start_with_next([=](not_null<PeerData*> joinAs) {
auto joinAsToggle = object_ptr<Ui::UserpicButton>(
widget(),
joinAs,
Ui::UserpicButton::Role::Custom,
st::groupCallJoinAsToggle);
_joinAsToggle.destroy();
_joinAsToggle = std::move(joinAsToggle);
_joinAsToggle->show();
_joinAsToggle->setClickedCallback([=] {
chooseJoinAs();
});
updateControlsGeometry();
}, widget()->lifetime());
} else {
_menuToggle.destroy();
_joinAsToggle.destroy();
}
}
void Panel::chooseShareScreenSource() {
if (!_call->emitShareScreenError()) {
Ui::DesktopCapture::ChooseSource(this);
}
}
void Panel::chooseJoinAs() {
const auto context = ChooseJoinAsProcess::Context::Switch;
const auto callback = [=](JoinInfo info) {
_call->rejoinAs(info);
};
const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
_joinAsProcess.start(
_peer,
context,
showBox,
showToast,
callback,
_call->joinAs());
}
void Panel::showMainMenu() {
if (_menu) {
return;
}
const auto wide = (_mode.current() == PanelMode::Wide) && _wideMenu;
if (!wide && !_menuToggle) {
return;
}
_menu.create(widget(), st::groupCallDropdownMenu);
FillMenu(
_menu.data(),
_peer,
_call,
wide,
[=] { chooseJoinAs(); },
[=] { chooseShareScreenSource(); },
[=](auto box) { _layerBg->showBox(std::move(box)); });
if (_menu->empty()) {
_wideMenuShown = false;
_menu.destroy();
return;
}
const auto raw = _menu.data();
raw->setHiddenCallback([=] {
raw->deleteLater();
if (_menu == raw) {
_menu = nullptr;
_wideMenuShown = false;
_trackControlsMenuLifetime.destroy();
if (_menuToggle) {
_menuToggle->setForceRippled(false);
}
}
});
raw->setShowStartCallback([=] {
if (_menu == raw) {
if (wide) {
_wideMenuShown = true;
} else if (_menuToggle) {
_menuToggle->setForceRippled(true);
}
}
});
raw->setHideStartCallback([=] {
if (_menu == raw) {
_wideMenuShown = false;
if (_menuToggle) {
_menuToggle->setForceRippled(false);
}
}
});
if (wide) {
_wideMenu->installEventFilter(_menu);
const auto x = st::groupCallWideMenuPosition.x();
const auto y = st::groupCallWideMenuPosition.y();
_menu->moveToLeft(
_wideMenu->x() + x,
_wideMenu->y() - _menu->height() + y);
_menu->showAnimated(Ui::PanelAnimation::Origin::BottomLeft);
trackControl(_menu, _trackControlsMenuLifetime);
} else {
_menuToggle->installEventFilter(_menu);
const auto x = st::groupCallMenuPosition.x();
const auto y = st::groupCallMenuPosition.y();
if (_menuToggle->x() > widget()->width() / 2) {
_menu->moveToRight(x, y);
_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
} else {
_menu->moveToLeft(x, y);
_menu->showAnimated(Ui::PanelAnimation::Origin::TopLeft);
}
}
}
void Panel::addMembers() {
const auto real = _call->lookupReal();
if (!real) {
return;
}
auto alreadyIn = _peer->owner().invitedToCallUsers(real->id());
for (const auto &participant : real->participants()) {
if (const auto user = participant.peer->asUser()) {
alreadyIn.emplace(user);
}
}
alreadyIn.emplace(_peer->session().user());
auto controller = std::make_unique<InviteController>(
_peer,
alreadyIn);
controller->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto contactsController = std::make_unique<InviteContactsController>(
_peer,
std::move(alreadyIn),
controller->peersWithRows(),
controller->rowAdded());
contactsController->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
const auto weak = base::make_weak(_call.get());
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
const auto call = weak.get();
if (!call) {
return;
}
const auto result = call->inviteUsers(users);
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::WithEntities),
});
} else if (const auto count = std::get_if<int>(&result)) {
if (*count > 0) {
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
Ui::Text::RichLangValue),
});
}
} else {
Unexpected("Result in GroupCall::inviteUsers.");
}
};
const auto inviteWithAdd = [=](
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
_peer->session().api().addChatParticipants(
_peer,
nonMembers,
[=](bool) { invite(users); finish(); });
};
const auto inviteWithConfirmation = [=](
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
if (nonMembers.empty()) {
invite(users);
finish();
return;
}
const auto name = _peer->name;
const auto text = (nonMembers.size() == 1)
? tr::lng_group_call_add_to_group_one(
tr::now,
lt_user,
nonMembers.front()->shortName(),
lt_group,
name)
: (nonMembers.size() < users.size())
? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name)
: tr::lng_group_call_add_to_group_all(tr::now, lt_group, name);
const auto shared = std::make_shared<QPointer<Ui::GenericBox>>();
const auto finishWithConfirm = [=] {
if (*shared) {
(*shared)->closeBox();
}
finish();
};
const auto done = [=] {
inviteWithAdd(users, nonMembers, finishWithConfirm);
};
auto box = ConfirmBox({
.text = { text },
.button = tr::lng_participant_invite(),
.callback = done,
});
*shared = box.data();
_layerBg->showBox(std::move(box));
};
auto initBox = [=, controller = controller.get()](
not_null<PeerListsBox*> box) {
box->setTitle(tr::lng_group_call_invite_title());
box->addButton(tr::lng_group_call_invite_button(), [=] {
const auto rows = box->collectSelectedRows();
const auto users = ranges::views::all(
rows
) | ranges::views::transform([](not_null<PeerData*> peer) {
return not_null<UserData*>(peer->asUser());
}) | ranges::to_vector;
const auto nonMembers = ranges::views::all(
users
) | ranges::views::filter([&](not_null<UserData*> user) {
return !controller->hasRowFor(user);
}) | ranges::to_vector;
const auto finish = [box = Ui::MakeWeak(box)]() {
if (box) {
box->closeBox();
}
};
inviteWithConfirmation(users, nonMembers, finish);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
auto controllers = std::vector<std::unique_ptr<PeerListController>>();
controllers.push_back(std::move(controller));
controllers.push_back(std::move(contactsController));
_layerBg->showBox(Box<PeerListsBox>(std::move(controllers), initBox));
}
void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
_layerBg->showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
(!participantPeer->isUser()
? tr::lng_group_call_remove_channel(
tr::now,
lt_channel,
participantPeer->name)
: (_peer->isBroadcast()
? tr::lng_profile_sure_kick_channel
: tr::lng_profile_sure_kick)(
tr::now,
lt_user,
participantPeer->asUser()->firstName)),
st::groupCallBoxLabel),
style::margins(
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom()));
box->addButton(tr::lng_box_remove(), [=] {
box->closeBox();
kickParticipantSure(participantPeer);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
if (const auto chat = _peer->asChat()) {
chat->session().api().kickParticipant(chat, participantPeer);
} else if (const auto channel = _peer->asChannel()) {
const auto currentRestrictedRights = [&] {
const auto user = participantPeer->asUser();
if (!channel->mgInfo || !user) {
return ChannelData::EmptyRestrictedRights(participantPeer);
}
const auto i = channel->mgInfo->lastRestricted.find(user);
return (i != channel->mgInfo->lastRestricted.cend())
? i->second.rights
: ChannelData::EmptyRestrictedRights(participantPeer);
}();
channel->session().api().kickParticipant(
channel,
participantPeer,
currentRestrictedRights);
}
}
void Panel::initLayout() {
initGeometry();
#ifndef Q_OS_MAC
_controls->raise();
Ui::Platform::TitleControlsLayoutChanged(
) | rpl::start_with_next([=] {
// _menuToggle geometry depends on _controls arrangement.
crl::on_main(widget(), [=] { updateControlsGeometry(); });
}, widget()->lifetime());
#endif // !Q_OS_MAC
}
void Panel::showControls() {
Expects(_call != nullptr);
widget()->showChildren();
}
void Panel::closeBeforeDestroy() {
_window->close();
_callLifetime.destroy();
}
void Panel::initGeometry() {
const auto center = Core::App().getPointForCallPanelCenter();
const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight);
_window->setGeometry(rect.translated(center - rect.center()));
_window->setMinimumSize(rect.size());
_window->show();
}
QRect Panel::computeTitleRect() const {
const auto skip = st::groupCallTitleTop;
const auto remove = skip + (_menuToggle
? (_menuToggle->width() + st::groupCallMenuTogglePosition.x())
: 0) + (_joinAsToggle
? (_joinAsToggle->width() + st::groupCallMenuTogglePosition.x())
: 0);
const auto width = widget()->width();
#ifdef Q_OS_MAC
return QRect(70, 0, width - remove - 70, 28);
#else // Q_OS_MAC
const auto controls = _controls->geometry();
const auto right = controls.x() + controls.width() + skip;
return (controls.center().x() < width / 2)
? QRect(right, 0, width - right - remove, controls.height())
: QRect(remove, 0, controls.x() - skip - remove, controls.height());
#endif // !Q_OS_MAC
}
bool Panel::updateMode() {
if (!_viewport) {
return false;
}
const auto wide = _call->hasVideoWithFrames()
&& (widget()->width() >= st::groupCallWideModeWidthMin);
const auto mode = wide ? PanelMode::Wide : PanelMode::Default;
if (_mode.current() == mode) {
return false;
}
if (!wide && _call->videoEndpointLarge()) {
_call->showVideoEndpointLarge({});
}
refreshVideoButtons(wide);
_niceTooltip.destroy();
_mode = mode;
if (_title) {
_title->setTextColorOverride(wide
? std::make_optional(st::groupCallMemberNotJoinedStatus->c)
: std::nullopt);
}
if (wide && _subtitle) {
_subtitle.destroy();
} else if (!wide && !_subtitle) {
refreshTitle();
}
_wideControlsShown = _showWideControls = true;
_wideControlsAnimation.stop();
_viewport->widget()->setVisible(wide);
if (_members) {
_members->setMode(mode);
}
updateButtonsStyles();
refreshControlsBackground();
updateControlsGeometry();
return true;
}
void Panel::updateButtonsStyles() {
const auto wide = (_mode.current() == PanelMode::Wide);
_mute->setStyle(wide ? st::callMuteButtonSmall : st::callMuteButton);
if (_video) {
_video->setStyle(
wide ? st::groupCallVideoSmall : st::groupCallVideo,
(wide
? &st::groupCallVideoActiveSmall
: &st::groupCallVideoActive));
_video->setText(wide
? rpl::single(QString())
: tr::lng_group_call_video());
}
if (_settings) {
_settings->setText(wide
? rpl::single(QString())
: tr::lng_group_call_settings());
_settings->setStyle(wide
? st::groupCallSettingsSmall
: st::groupCallSettings);
}
_hangup->setText(wide
? rpl::single(QString())
: _call->scheduleDate()
? tr::lng_group_call_close()
: tr::lng_group_call_leave());
_hangup->setStyle(wide
? st::groupCallHangupSmall
: st::groupCallHangup);
}
void Panel::refreshControlsBackground() {
if (!_members) {
return;
}
if (mode() == PanelMode::Default) {
trackControls(false);
_controlsBackgroundWide.destroy();
if (_controlsBackgroundNarrow) {
return;
}
setupControlsBackgroundNarrow();
} else {
_controlsBackgroundNarrow = nullptr;
if (_controlsBackgroundWide) {
return;
}
setupControlsBackgroundWide();
}
raiseControls();
updateButtonsGeometry();
}
void Panel::setupControlsBackgroundNarrow() {
_controlsBackgroundNarrow = std::make_unique<ControlsBackgroundNarrow>(
widget());
_controlsBackgroundNarrow->shadow.show();
_controlsBackgroundNarrow->blocker.show();
auto &lifetime = _controlsBackgroundNarrow->shadow.lifetime();
const auto factor = cIntRetinaFactor();
const auto height = std::max(
st::groupCallMembersShadowHeight,
st::groupCallMembersFadeSkip + st::groupCallMembersFadeHeight);
const auto full = lifetime.make_state<QImage>(
QSize(1, height * factor),
QImage::Format_ARGB32_Premultiplied);
rpl::single(
rpl::empty_value()
) | rpl::then(
style::PaletteChanged()
) | rpl::start_with_next([=] {
full->fill(Qt::transparent);
auto p = QPainter(full);
const auto bottom = (height - st::groupCallMembersFadeSkip) * factor;
p.fillRect(
0,
bottom,
full->width(),
st::groupCallMembersFadeSkip * factor,
st::groupCallMembersBg);
p.drawImage(
QRect(
0,
bottom - (st::groupCallMembersFadeHeight * factor),
full->width(),
st::groupCallMembersFadeHeight * factor),
GenerateShadow(
st::groupCallMembersFadeHeight,
0,
255,
st::groupCallMembersBg->c));
p.drawImage(
QRect(
0,
(height - st::groupCallMembersShadowHeight) * factor,
full->width(),
st::groupCallMembersShadowHeight * factor),
GenerateShadow(
st::groupCallMembersShadowHeight,
0,
255,
st::groupCallBg->c));
}, lifetime);
_controlsBackgroundNarrow->shadow.resize(
(widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right()),
height);
_controlsBackgroundNarrow->shadow.paintRequest(
) | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(&_controlsBackgroundNarrow->shadow);
clip = clip.intersected(_controlsBackgroundNarrow->shadow.rect());
const auto inner = _members->getInnerGeometry().translated(
_members->x() - _controlsBackgroundNarrow->shadow.x(),
_members->y() - _controlsBackgroundNarrow->shadow.y());
const auto faded = clip.intersected(inner);
if (!faded.isEmpty()) {
const auto factor = cIntRetinaFactor();
p.drawImage(
faded,
*full,
QRect(
0,
faded.y() * factor,
full->width(),
faded.height() * factor));
}
const auto bottom = inner.y() + inner.height();
const auto after = clip.intersected(QRect(
0,
bottom,
inner.width(),
_controlsBackgroundNarrow->shadow.height() - bottom));
if (!after.isEmpty()) {
p.fillRect(after, st::groupCallBg);
}
}, lifetime);
_controlsBackgroundNarrow->shadow.setAttribute(
Qt::WA_TransparentForMouseEvents);
_controlsBackgroundNarrow->blocker.setUpdatesEnabled(false);
}
void Panel::setupControlsBackgroundWide() {
_controlsBackgroundWide.create(widget());
_controlsBackgroundWide->show();
auto &lifetime = _controlsBackgroundWide->lifetime();
const auto color = lifetime.make_state<style::complex_color>([] {
auto result = st::groupCallBg->c;
result.setAlphaF(kControlsBackgroundOpacity);
return result;
});
const auto corners = lifetime.make_state<Ui::RoundRect>(
st::groupCallControlsBackRadius,
color->color());
_controlsBackgroundWide->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(_controlsBackgroundWide.data());
corners->paint(p, _controlsBackgroundWide->rect());
}, lifetime);
trackControls(true);
}
void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
if (!widget) {
return;
}
widget->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::Enter) {
trackControlOver(widget, true);
} else if (e->type() == QEvent::Leave) {
trackControlOver(widget, false);
}
}, lifetime);
}
void Panel::trackControlOver(not_null<Ui::RpWidget*> control, bool over) {
if (_niceTooltip) {
_niceTooltip.release()->toggleAnimated(false);
}
if (over) {
Ui::Integration::Instance().registerLeaveSubscription(control);
showNiceTooltip(control);
} else {
Ui::Integration::Instance().unregisterLeaveSubscription(control);
}
toggleWideControls(over);
}
void Panel::showNiceTooltip(not_null<Ui::RpWidget*> control) {
auto text = [&]() -> rpl::producer<QString> {
if (control == _screenShare.data()) {
if (_call->mutedByAdmin()) {
return nullptr;
}
return tr::lng_group_call_tooltip_screen();
} else if (control == _video.data()) {
if (_call->mutedByAdmin()) {
return nullptr;
}
return _call->isSharingCameraValue(
) | rpl::map([=](bool sharing) {
return sharing
? tr::lng_group_call_tooltip_camera_off()
: tr::lng_group_call_tooltip_camera();
}) | rpl::flatten_latest();
} else if (control == _settings.data()) {
return tr::lng_group_call_settings();
} else if (control == _mute->outer()) {
return MuteButtonTooltip(_call);
} else if (control == _hangup.data()) {
return tr::lng_group_call_leave();
}
return rpl::producer<QString>();
}();
if (!text
|| _wideControlsAnimation.animating()
|| !_wideControlsShown) {
return;
}
_niceTooltip.create(
widget().get(),
object_ptr<Ui::FlatLabel>(
widget().get(),
std::move(text),
st::groupCallNiceTooltipLabel),
st::groupCallNiceTooltip);
const auto tooltip = _niceTooltip.data();
const auto weak = QPointer<QWidget>(tooltip);
const auto destroy = [=] {
delete weak.data();
};
tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
tooltip->setHiddenCallback(destroy);
base::qt_signal_producer(
control.get(),
&QObject::destroyed
) | rpl::start_with_next(destroy, tooltip->lifetime());
const auto geometry = control->geometry();
const auto countPosition = [=](QSize size) {
const auto strong = weak.data();
if (!strong) {
return QPoint();
}
const auto top = geometry.y()
- st::groupCallNiceTooltipTop
- size.height();
const auto middle = geometry.center().x();
const auto back = _controlsBackgroundWide.data();
if (size.width() >= _viewport->widget()->width()) {
return QPoint(_viewport->widget()->x(), top);
} else if (back && size.width() >= back->width()) {
return QPoint(
back->x() - (size.width() - back->width()) / 2,
top);
} else if (back && (middle - back->x() < size.width() / 2)) {
return QPoint(back->x(), top);
} else if (back
&& (back->x() + back->width() - middle < size.width() / 2)) {
return QPoint(back->x() + back->width() - size.width(), top);
} else {
return QPoint(middle - size.width() / 2, top);
}
};
tooltip->pointAt(geometry, RectPart::Top, countPosition);
tooltip->toggleAnimated(true);
}
void Panel::trackControls(bool track) {
if (_trackControls == track) {
return;
}
_trackControls = track;
if (!track) {
_trackControlsLifetime.destroy();
_trackControlsOverStateLifetime.destroy();
_trackControlsMenuLifetime.destroy();
toggleWideControls(true);
if (_wideControlsAnimation.animating()) {
_wideControlsAnimation.stop();
updateButtonsGeometry();
}
return;
}
const auto trackOne = [=](auto &&widget) {
trackControl(widget, _trackControlsOverStateLifetime);
};
trackOne(_mute->outer());
trackOne(_video);
trackOne(_screenShare);
trackOne(_wideMenu);
trackOne(_settings);
trackOne(_hangup);
trackOne(_controlsBackgroundWide);
trackControl(_menu, _trackControlsMenuLifetime);
}
void Panel::updateControlsGeometry() {
if (widget()->size().isEmpty() || (!_settings && !_callShare)) {
return;
}
updateButtonsGeometry();
updateMembersGeometry();
refreshTitle();
#ifdef Q_OS_MAC
const auto controlsOnTheLeft = true;
#else // Q_OS_MAC
const auto controlsOnTheLeft = _controls->geometry().center().x()
< widget()->width() / 2;
#endif // Q_OS_MAC
const auto menux = st::groupCallMenuTogglePosition.x();
const auto menuy = st::groupCallMenuTogglePosition.y();
if (controlsOnTheLeft) {
if (_menuToggle) {
_menuToggle->moveToRight(menux, menuy);
} else if (_joinAsToggle) {
_joinAsToggle->moveToRight(menux, menuy);
}
} else {
if (_menuToggle) {
_menuToggle->moveToLeft(menux, menuy);
} else if (_joinAsToggle) {
_joinAsToggle->moveToLeft(menux, menuy);
}
}
}
void Panel::updateButtonsGeometry() {
if (widget()->size().isEmpty() || (!_settings && !_callShare)) {
return;
}
const auto toggle = [](auto &widget, bool shown) {
if (widget && widget->isHidden() == shown) {
widget->setVisible(shown);
}
};
if (mode() == PanelMode::Wide) {
Assert(_video != nullptr);
Assert(_screenShare != nullptr);
Assert(_wideMenu != nullptr);
Assert(_settings != nullptr);
Assert(_callShare == nullptr);
const auto shown = _wideControlsAnimation.value(
_wideControlsShown ? 1. : 0.);
const auto hidden = (shown == 0.);
if (_viewport) {
_viewport->setControlsShown(shown);
}
const auto buttonsTop = widget()->height() - anim::interpolate(
0,
st::groupCallButtonBottomSkipWide,
shown);
const auto addSkip = st::callMuteButtonSmall.active.outerRadius;
const auto muteSize = _mute->innerSize().width() + 2 * addSkip;
const auto skip = st::groupCallButtonSkipSmall;
const auto fullWidth = (_video->width() + skip)
+ (_screenShare->width() + skip)
+ (muteSize + skip)
+ (_settings ->width() + skip)
+ _hangup->width();
const auto membersSkip = st::groupCallNarrowSkip;
const auto membersWidth = st::groupCallNarrowMembersWidth
+ 2 * membersSkip;
auto left = membersSkip + (widget()->width()
- membersWidth
- membersSkip
- fullWidth) / 2;
toggle(_screenShare, !hidden);
_screenShare->moveToLeft(left, buttonsTop);
left += _screenShare->width() + skip;
toggle(_video, !hidden);
_video->moveToLeft(left, buttonsTop);
left += _video->width() + skip;
toggle(_mute, !hidden);
_mute->moveInner({ left + addSkip, buttonsTop + addSkip });
left += muteSize + skip;
const auto wideMenuShown = _call->canManage()
|| _call->showChooseJoinAs();
toggle(_settings, !hidden && !wideMenuShown);
toggle(_wideMenu, !hidden && wideMenuShown);
_wideMenu->moveToLeft(left, buttonsTop);
_settings->moveToLeft(left, buttonsTop);
left += _settings->width() + skip;
toggle(_hangup, !hidden);
_hangup->moveToLeft(left, buttonsTop);
left += _hangup->width();
if (_controlsBackgroundWide) {
const auto rect = QRect(
left - fullWidth,
buttonsTop,
fullWidth,
_hangup->height());
_controlsBackgroundWide->setGeometry(
rect.marginsAdded(st::groupCallControlsBackMargin));
}
} else {
const auto muteTop = widget()->height()
- st::groupCallMuteBottomSkip;
const auto buttonsTop = widget()->height()
- st::groupCallButtonBottomSkip;
const auto muteSize = _mute->innerSize().width();
const auto fullWidth = muteSize
+ 2 * (_settings ? _settings : _callShare)->width()
+ 2 * st::groupCallButtonSkip;
toggle(_mute, true);
_mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop });
const auto leftButtonLeft = (widget()->width() - fullWidth) / 2;
toggle(_screenShare, false);
toggle(_wideMenu, false);
toggle(_callShare, true);
if (_callShare) {
_callShare->moveToLeft(leftButtonLeft, buttonsTop);
}
const auto showVideoButton = videoButtonInNarrowMode();
toggle(_video, !_callShare && showVideoButton);
if (_video) {
_video->setStyle(st::groupCallVideo, &st::groupCallVideoActive);
_video->moveToLeft(leftButtonLeft, buttonsTop);
}
toggle(_settings, !_callShare && !showVideoButton);
if (_settings) {
_settings->moveToLeft(leftButtonLeft, buttonsTop);
}
toggle(_hangup, true);
_hangup->moveToRight(leftButtonLeft, buttonsTop);
}
if (_controlsBackgroundNarrow) {
const auto left = st::groupCallMembersMargin.left();
const auto width = (widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right());
_controlsBackgroundNarrow->shadow.setGeometry(
left,
(widget()->height()
- st::groupCallMembersMargin.bottom()
- _controlsBackgroundNarrow->shadow.height()),
width,
_controlsBackgroundNarrow->shadow.height());
_controlsBackgroundNarrow->blocker.setGeometry(
left,
(widget()->height()
- st::groupCallMembersMargin.bottom()
- st::groupCallMembersBottomSkip),
width,
st::groupCallMembersBottomSkip);
}
}
bool Panel::videoButtonInNarrowMode() const {
return (_video != nullptr) && !_call->mutedByAdmin();
}
void Panel::updateMembersGeometry() {
if (!_members) {
return;
}
const auto desiredHeight = _members->desiredHeight();
if (mode() == PanelMode::Wide) {
const auto skip = st::groupCallNarrowSkip;
const auto membersWidth = st::groupCallNarrowMembersWidth;
const auto top = st::groupCallWideVideoTop;
_members->setGeometry(
widget()->width() - skip - membersWidth,
top,
membersWidth,
std::min(desiredHeight, widget()->height() - top - skip));
_viewport->setGeometry({
skip,
top,
widget()->width() - membersWidth - 3 * skip,
widget()->height() - top - skip,
});
} else {
const auto membersBottom = widget()->height();
const auto membersTop = st::groupCallMembersTop;
const auto availableHeight = membersBottom
- st::groupCallMembersMargin.bottom()
- membersTop;
const auto membersWidthAvailable = widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidthMin = st::groupCallWidth
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidth = std::clamp(
membersWidthAvailable,
membersWidthMin,
st::groupCallMembersWidthMax);
_members->setGeometry(
(widget()->width() - membersWidth) / 2,
membersTop,
membersWidth,
std::min(desiredHeight, availableHeight));
}
}
void Panel::refreshTitle() {
if (!_title) {
auto text = rpl::combine(
Info::Profile::NameValue(_peer),
rpl::single(
QString()
) | rpl::then(_call->real(
) | rpl::map([=](not_null<Data::GroupCall*> real) {
return real->titleValue();
}) | rpl::flatten_latest())
) | rpl::map([=](
const TextWithEntities &name,
const QString &title) {
return title.isEmpty() ? name.text : title;
}) | rpl::after_next([=] {
refreshTitleGeometry();
});
_title.create(
widget(),
rpl::duplicate(text),
st::groupCallTitleLabel);
_title->show();
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
refreshTitleGeometry();
if (!_subtitle && mode() == PanelMode::Default) {
_subtitle.create(
widget(),
rpl::single(
_call->scheduleDate()
) | rpl::then(
_call->real(
) | rpl::map([=](not_null<Data::GroupCall*> real) {
return real->scheduleDateValue();
}) | rpl::flatten_latest()
) | rpl::map([=](TimeId scheduleDate) {
if (scheduleDate) {
return tr::lng_group_call_scheduled_status();
} else if (!_members) {
setupMembers();
}
return tr::lng_group_call_members(
lt_count_decimal,
_members->fullCountValue() | rpl::map([](int value) {
return (value > 0) ? float64(value) : 1.;
}));
}) | rpl::flatten_latest(),
st::groupCallSubtitleLabel);
_subtitle->show();
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
}
if (_subtitle) {
const auto middle = _title
? (_title->x() + _title->width() / 2)
: (widget()->width() / 2);
const auto top = _title
? st::groupCallSubtitleTop
: st::groupCallTitleTop;
_subtitle->moveToLeft(
(widget()->width() - _subtitle->width()) / 2,
top);
}
}
void Panel::refreshTitleGeometry() {
if (!_title) {
return;
}
const auto fullRect = computeTitleRect();
const auto recordingWidth = 2 * st::groupCallRecordingMarkSkip
+ st::groupCallRecordingMark;
const auto titleRect = _recordingMark
? QRect(
fullRect.x(),
fullRect.y(),
fullRect.width() - _recordingMark->width(),
fullRect.height())
: fullRect;
const auto best = _title->naturalWidth();
const auto from = (widget()->width() - best) / 2;
const auto top = (mode() == PanelMode::Default)
? st::groupCallTitleTop
: (st::groupCallWideVideoTop
- st::groupCallTitleLabel.style.font->height) / 2;
const auto left = titleRect.x();
if (from >= left && from + best <= left + titleRect.width()) {
_title->resizeToWidth(best);
_title->moveToLeft(from, top);
} else if (titleRect.width() < best) {
_title->resizeToWidth(titleRect.width());
_title->moveToLeft(left, top);
} else if (from < left) {
_title->resizeToWidth(best);
_title->moveToLeft(left, top);
} else {
_title->resizeToWidth(best);
_title->moveToLeft(left + titleRect.width() - best, top);
}
if (_recordingMark) {
const auto markTop = top + st::groupCallRecordingMarkTop;
_recordingMark->move(
_title->x() + _title->width(),
markTop - st::groupCallRecordingMarkSkip);
}
}
void Panel::paint(QRect clip) {
Painter p(widget());
auto region = QRegion(clip);
for (const auto rect : region) {
p.fillRect(rect, st::groupCallBg);
}
}
bool Panel::handleClose() {
if (_call) {
_window->hide();
return true;
}
return false;
}
not_null<Ui::RpWidget*> Panel::widget() const {
return _window->body();
}
} // namespace Calls::Group