init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
501
Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp
Normal file
501
Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
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_choose_join_as.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "apiwrap.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/boxes/choose_date_time.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
|
||||
|
||||
using Context = ChooseJoinAsProcess::Context;
|
||||
|
||||
class ListController : public PeerListController {
|
||||
public:
|
||||
ListController(
|
||||
std::vector<not_null<PeerData*>> list,
|
||||
not_null<PeerData*> selected);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> selected() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);
|
||||
|
||||
std::vector<not_null<PeerData*>> _list;
|
||||
not_null<PeerData*> _selected;
|
||||
|
||||
};
|
||||
|
||||
ListController::ListController(
|
||||
std::vector<not_null<PeerData*>> list,
|
||||
not_null<PeerData*> selected)
|
||||
: PeerListController()
|
||||
, _list(std::move(list))
|
||||
, _selected(selected) {
|
||||
}
|
||||
|
||||
Main::Session &ListController::session() const {
|
||||
return _selected->session();
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> ListController::createRow(
|
||||
not_null<PeerData*> peer) {
|
||||
auto result = std::make_unique<PeerListRow>(peer);
|
||||
if (peer->isSelf()) {
|
||||
result->setCustomStatus(
|
||||
tr::lng_group_call_join_as_personal(tr::now));
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
result->setCustomStatus(
|
||||
(channel->isMegagroup()
|
||||
? tr::lng_chat_status_members
|
||||
: tr::lng_chat_status_subscribers)(
|
||||
tr::now,
|
||||
lt_count,
|
||||
channel->membersCount()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ListController::prepare() {
|
||||
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
|
||||
for (const auto &peer : _list) {
|
||||
auto row = createRow(peer);
|
||||
const auto raw = row.get();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (peer == _selected) {
|
||||
delegate()->peerListSetRowChecked(raw, true);
|
||||
raw->finishCheckedAnimation();
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void ListController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto peer = row->peer();
|
||||
if (peer == _selected) {
|
||||
return;
|
||||
}
|
||||
const auto previous = delegate()->peerListFindRow(_selected->id.value);
|
||||
Assert(previous != nullptr);
|
||||
delegate()->peerListSetRowChecked(previous, false);
|
||||
delegate()->peerListSetRowChecked(row, true);
|
||||
_selected = peer;
|
||||
}
|
||||
|
||||
not_null<PeerData*> ListController::selected() const {
|
||||
return _selected;
|
||||
}
|
||||
|
||||
void ScheduleGroupCallBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const JoinInfo &info,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
const auto send = [=](TimeId date) {
|
||||
box->closeBox();
|
||||
|
||||
auto copy = info;
|
||||
copy.scheduleDate = date;
|
||||
done(std::move(copy));
|
||||
};
|
||||
const auto livestream = info.peer->isBroadcast();
|
||||
const auto duration = box->lifetime().make_state<
|
||||
rpl::variable<QString>>();
|
||||
auto description = (info.peer->isBroadcast()
|
||||
? tr::lng_group_call_schedule_notified_channel
|
||||
: tr::lng_group_call_schedule_notified_group)(
|
||||
lt_duration,
|
||||
duration->value());
|
||||
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto min = [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime::currentDateTime().addSecs(12));
|
||||
};
|
||||
const auto max = [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime(QDate::currentDate().addDays(8), QTime(0, 0))) - 1;
|
||||
};
|
||||
|
||||
// At least half an hour later, at zero minutes/seconds.
|
||||
const auto schedule = QDateTime(
|
||||
now.date(),
|
||||
QTime(now.time().hour(), 0)
|
||||
).addSecs(60 * 60 * (now.time().minute() < 30 ? 1 : 2));
|
||||
|
||||
auto descriptor = Ui::ChooseDateTimeBox(box, {
|
||||
.title = (livestream
|
||||
? tr::lng_group_call_schedule_title_channel()
|
||||
: tr::lng_group_call_schedule_title()),
|
||||
.submit = tr::lng_schedule_button(),
|
||||
.done = send,
|
||||
.min = min,
|
||||
.time = base::unixtime::serialize(schedule),
|
||||
.max = max,
|
||||
.description = std::move(description),
|
||||
});
|
||||
|
||||
using namespace rpl::mappers;
|
||||
*duration = rpl::combine(
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
base::timer_each(kLabelRefreshInterval)
|
||||
),
|
||||
std::move(descriptor.values) | rpl::filter(_1 != 0),
|
||||
_2
|
||||
) | rpl::map([](TimeId date) {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto duration = (date - now);
|
||||
if (duration >= 24 * 60 * 60) {
|
||||
return tr::lng_days(tr::now, lt_count, duration / (24 * 60 * 60));
|
||||
} else if (duration >= 60 * 60) {
|
||||
return tr::lng_hours(tr::now, lt_count, duration / (60 * 60));
|
||||
}
|
||||
return tr::lng_minutes(tr::now, lt_count, std::max(duration / 60, 1));
|
||||
});
|
||||
}
|
||||
|
||||
void ChooseJoinAsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Context context,
|
||||
JoinInfo info,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
box->setWidth(st::groupCallJoinAsWidth);
|
||||
const auto livestream = info.peer->isBroadcast();
|
||||
box->setTitle([&] {
|
||||
switch (context) {
|
||||
case Context::Create: return livestream
|
||||
? tr::lng_group_call_start_as_header_channel()
|
||||
: tr::lng_group_call_start_as_header();
|
||||
case Context::Join:
|
||||
case Context::JoinWithConfirm: return livestream
|
||||
? tr::lng_group_call_join_as_header_channel()
|
||||
: tr::lng_group_call_join_as_header();
|
||||
case Context::Switch: return tr::lng_group_call_display_as_header();
|
||||
}
|
||||
Unexpected("Context in ChooseJoinAsBox.");
|
||||
}());
|
||||
const auto &labelSt = (context == Context::Switch)
|
||||
? st::groupCallJoinAsLabel
|
||||
: st::confirmPhoneAboutLabel;
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_group_call_join_as_about(),
|
||||
labelSt));
|
||||
|
||||
auto &lifetime = box->lifetime();
|
||||
const auto delegate = lifetime.make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = lifetime.make_state<ListController>(
|
||||
info.possibleJoinAs,
|
||||
info.joinAs);
|
||||
if (context == Context::Switch) {
|
||||
controller->setStyleOverrides(
|
||||
&st::groupCallJoinAsList,
|
||||
&st::groupCallMultiSelect);
|
||||
} else {
|
||||
controller->setStyleOverrides(
|
||||
&st::defaultChooseSendAs.list,
|
||||
nullptr);
|
||||
}
|
||||
const auto content = box->addRow(
|
||||
object_ptr<PeerListContent>(box, controller),
|
||||
style::margins());
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
const auto &peer = info.peer;
|
||||
if ((context == Context::Create)
|
||||
&& (peer->isChannel() && peer->asChannel()->hasAdminRights())) {
|
||||
const auto makeLink = [](const QString &text) {
|
||||
return tr::link(text);
|
||||
};
|
||||
const auto label = box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_group_call_or_schedule(
|
||||
lt_link,
|
||||
(livestream
|
||||
? tr::lng_group_call_schedule_channel
|
||||
: tr::lng_group_call_schedule)(makeLink),
|
||||
tr::marked),
|
||||
labelSt));
|
||||
label->overrideLinkClickHandler([=] {
|
||||
auto withJoinAs = info;
|
||||
withJoinAs.joinAs = controller->selected();
|
||||
box->getDelegate()->show(
|
||||
Box(ScheduleGroupCallBox, withJoinAs, done));
|
||||
});
|
||||
}
|
||||
auto next = (context == Context::Switch)
|
||||
? tr::lng_settings_save()
|
||||
: tr::lng_continue();
|
||||
box->addButton(std::move(next), [=] {
|
||||
auto copy = info;
|
||||
copy.joinAs = controller->selected();
|
||||
done(std::move(copy));
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities CreateOrJoinConfirmation(
|
||||
not_null<PeerData*> peer,
|
||||
ChooseJoinAsProcess::Context context,
|
||||
bool joinAsAlreadyUsed) {
|
||||
const auto existing = peer->groupCall();
|
||||
if (!existing) {
|
||||
return { peer->isBroadcast()
|
||||
? tr::lng_group_call_create_sure_channel(tr::now)
|
||||
: tr::lng_group_call_create_sure(tr::now) };
|
||||
}
|
||||
const auto channel = peer->asChannel();
|
||||
const auto anonymouseAdmin = channel
|
||||
&& ((channel->isMegagroup() && channel->amAnonymous())
|
||||
|| (channel->isBroadcast()
|
||||
&& (channel->amCreator() || channel->hasAdminRights())));
|
||||
if (anonymouseAdmin && !joinAsAlreadyUsed) {
|
||||
return { tr::lng_group_call_join_sure_personal(tr::now) };
|
||||
} else if (context != ChooseJoinAsProcess::Context::JoinWithConfirm) {
|
||||
return {};
|
||||
}
|
||||
const auto name = !existing->title().isEmpty()
|
||||
? existing->title()
|
||||
: peer->name();
|
||||
return (peer->isBroadcast()
|
||||
? tr::lng_group_call_join_confirm_channel
|
||||
: tr::lng_group_call_join_confirm)(
|
||||
tr::now,
|
||||
lt_chat,
|
||||
tr::bold(name),
|
||||
tr::marked);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChooseJoinAsProcess::~ChooseJoinAsProcess() {
|
||||
if (_request) {
|
||||
_request->peer->session().api().request(_request->id).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::start(
|
||||
not_null<PeerData*> peer,
|
||||
Context context,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done,
|
||||
PeerData *changingJoinAsFrom) {
|
||||
Expects(done != nullptr);
|
||||
|
||||
const auto isScheduled = (context == Context::CreateScheduled);
|
||||
|
||||
const auto session = &peer->session();
|
||||
if (_request) {
|
||||
if (_request->peer == peer && !isScheduled) {
|
||||
_request->context = context;
|
||||
_request->show = std::move(show);
|
||||
_request->done = std::move(done);
|
||||
_request->changingJoinAsFrom = changingJoinAsFrom;
|
||||
return;
|
||||
}
|
||||
session->api().request(_request->id).cancel();
|
||||
_request = nullptr;
|
||||
}
|
||||
|
||||
const auto createRequest = [=, done = std::move(done)] {
|
||||
_request = std::make_unique<ChannelsListRequest>(ChannelsListRequest{
|
||||
.peer = peer,
|
||||
.show = show,
|
||||
.done = std::move(done),
|
||||
.context = context,
|
||||
.changingJoinAsFrom = changingJoinAsFrom });
|
||||
};
|
||||
|
||||
if (isScheduled) {
|
||||
auto box = Box(
|
||||
ScheduleGroupCallBox,
|
||||
JoinInfo{ .peer = peer, .joinAs = peer },
|
||||
[=, createRequest = std::move(createRequest)](JoinInfo info) {
|
||||
createRequest();
|
||||
finish(info);
|
||||
});
|
||||
show->showBox(std::move(box));
|
||||
return;
|
||||
}
|
||||
|
||||
createRequest();
|
||||
session->account().sessionChanges(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
requestList();
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::requestList() {
|
||||
const auto session = &_request->peer->session();
|
||||
_request->id = session->api().request(MTPphone_GetGroupCallJoinAs(
|
||||
_request->peer->input()
|
||||
)).done([=](const MTPphone_JoinAsPeers &result) {
|
||||
auto list = result.match([&](const MTPDphone_joinAsPeers &data) {
|
||||
session->data().processUsers(data.vusers());
|
||||
session->data().processChats(data.vchats());
|
||||
const auto &peers = data.vpeers().v;
|
||||
auto list = std::vector<not_null<PeerData*>>();
|
||||
list.reserve(peers.size());
|
||||
for (const auto &peer : peers) {
|
||||
const auto peerId = peerFromMTP(peer);
|
||||
if (const auto peer = session->data().peerLoaded(peerId)) {
|
||||
if (!ranges::contains(list, not_null{ peer })) {
|
||||
list.push_back(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
processList(std::move(list));
|
||||
}).fail([=] {
|
||||
finish({
|
||||
.peer = _request->peer,
|
||||
.joinAs = _request->peer->session().user(),
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::finish(JoinInfo info) {
|
||||
const auto done = std::move(_request->done);
|
||||
const auto box = _request->box;
|
||||
_request = nullptr;
|
||||
done(std::move(info));
|
||||
if (const auto strong = box.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::processList(
|
||||
std::vector<not_null<PeerData*>> &&list) {
|
||||
const auto session = &_request->peer->session();
|
||||
const auto peer = _request->peer;
|
||||
const auto self = peer->session().user();
|
||||
auto info = JoinInfo{ .peer = peer, .joinAs = self };
|
||||
const auto selectedId = peer->groupCallDefaultJoinAs();
|
||||
if (list.empty()) {
|
||||
_request->show->showToast(Lang::Hard::ServerError());
|
||||
return;
|
||||
}
|
||||
info.joinAs = [&]() -> not_null<PeerData*> {
|
||||
const auto loaded = selectedId
|
||||
? session->data().peerLoaded(selectedId)
|
||||
: nullptr;
|
||||
const auto changingJoinAsFrom = _request->changingJoinAsFrom;
|
||||
return (changingJoinAsFrom
|
||||
&& ranges::contains(list, not_null{ changingJoinAsFrom }))
|
||||
? not_null(changingJoinAsFrom)
|
||||
: (loaded && ranges::contains(list, not_null{ loaded }))
|
||||
? not_null(loaded)
|
||||
: ranges::contains(list, self)
|
||||
? self
|
||||
: list.front();
|
||||
}();
|
||||
info.possibleJoinAs = std::move(list);
|
||||
|
||||
const auto onlyByMe = (info.possibleJoinAs.size() == 1)
|
||||
&& (info.possibleJoinAs.front() == self);
|
||||
|
||||
// We already joined this voice chat, just rejoin with the same.
|
||||
const auto byAlreadyUsed = selectedId
|
||||
&& (info.joinAs->id == selectedId)
|
||||
&& (peer->groupCall() != nullptr);
|
||||
|
||||
if (!_request->changingJoinAsFrom && (onlyByMe || byAlreadyUsed)) {
|
||||
auto confirmation = CreateOrJoinConfirmation(
|
||||
peer,
|
||||
_request->context,
|
||||
byAlreadyUsed);
|
||||
if (confirmation.text.isEmpty()) {
|
||||
finish(info);
|
||||
return;
|
||||
}
|
||||
const auto livestream = peer->isBroadcast();
|
||||
const auto creating = !peer->groupCall();
|
||||
if (creating) {
|
||||
confirmation
|
||||
.append("\n\n")
|
||||
.append(tr::lng_group_call_or_schedule(
|
||||
tr::now,
|
||||
lt_link,
|
||||
tr::link((livestream
|
||||
? tr::lng_group_call_schedule_channel
|
||||
: tr::lng_group_call_schedule)(tr::now)),
|
||||
tr::marked));
|
||||
}
|
||||
const auto guard = base::make_weak(&_request->guard);
|
||||
const auto safeFinish = crl::guard(guard, [=] { finish(info); });
|
||||
const auto filter = [=](const auto &...) {
|
||||
if (guard) {
|
||||
_request->show->showBox(Box(
|
||||
ScheduleGroupCallBox,
|
||||
info,
|
||||
crl::guard(guard, [=](auto info) { finish(info); })));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto box = Ui::MakeConfirmBox({
|
||||
.text = confirmation,
|
||||
.confirmed = crl::guard(guard, [=] { finish(info); }),
|
||||
.confirmText = (creating
|
||||
? tr::lng_create_group_create()
|
||||
: tr::lng_group_call_join()),
|
||||
.labelFilter = filter,
|
||||
});
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
_request->box = box.data();
|
||||
_request->show->showBox(std::move(box));
|
||||
return;
|
||||
}
|
||||
auto box = Box(
|
||||
ChooseJoinAsBox,
|
||||
_request->context,
|
||||
std::move(info),
|
||||
crl::guard(&_request->guard, [=](auto info) { finish(info); }));
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
_request->box = box.data();
|
||||
_request->show->showBox(std::move(box));
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
64
Telegram/SourceFiles/calls/group/calls_choose_join_as.h
Normal file
64
Telegram/SourceFiles/calls/group/calls_choose_join_as.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct JoinInfo;
|
||||
|
||||
class ChooseJoinAsProcess final {
|
||||
public:
|
||||
ChooseJoinAsProcess() = default;
|
||||
~ChooseJoinAsProcess();
|
||||
|
||||
enum class Context {
|
||||
Create,
|
||||
CreateScheduled,
|
||||
Join,
|
||||
JoinWithConfirm,
|
||||
Switch,
|
||||
};
|
||||
|
||||
void start(
|
||||
not_null<PeerData*> peer,
|
||||
Context context,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done,
|
||||
PeerData *changingJoinAsFrom = nullptr);
|
||||
|
||||
private:
|
||||
void requestList();
|
||||
void processList(std::vector<not_null<PeerData*>> &&list);
|
||||
void finish(JoinInfo info);
|
||||
|
||||
struct ChannelsListRequest {
|
||||
not_null<PeerData*> peer;
|
||||
std::shared_ptr<Ui::Show> show;
|
||||
Fn<void(JoinInfo)> done;
|
||||
base::has_weak_ptr guard;
|
||||
base::weak_qptr<Ui::BoxContent> box;
|
||||
rpl::lifetime lifetime;
|
||||
Context context = Context();
|
||||
mtpRequestId id = 0;
|
||||
PeerData *changingJoinAsFrom = nullptr;
|
||||
};
|
||||
std::unique_ptr<ChannelsListRequest> _request;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
92
Telegram/SourceFiles/calls/group/calls_cover_item.cpp
Normal file
92
Telegram/SourceFiles/calls/group/calls_cover_item.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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_cover_item.h"
|
||||
|
||||
#include "boxes/peers/prepare_short_info_box.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
CoverItem::CoverItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &stMenu,
|
||||
const style::ShortInfoCover &st,
|
||||
rpl::producer<QString> name,
|
||||
rpl::producer<QString> status,
|
||||
PreparedShortInfoUserpic userpic)
|
||||
: Ui::Menu::ItemBase(parent, stMenu)
|
||||
, _cover(
|
||||
this,
|
||||
st,
|
||||
std::move(name),
|
||||
std::move(status),
|
||||
std::move(userpic.value),
|
||||
[] { return false; })
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_cover.widget());
|
||||
|
||||
_cover.widget()->move(0, 0);
|
||||
_cover.moveRequests(
|
||||
) | rpl::on_next(userpic.move, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> CoverItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool CoverItem::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int CoverItem::contentHeight() const {
|
||||
return _st.size + st::groupCallMenu.separator.padding.bottom();
|
||||
}
|
||||
|
||||
AboutItem::AboutItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
TextWithEntities &&about)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _st(st)
|
||||
, _text(base::make_unique_q<Ui::FlatLabel>(
|
||||
this,
|
||||
rpl::single(std::move(about)),
|
||||
st::groupCallMenuAbout))
|
||||
, _dummyAction(new QAction(parent)) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_text.get());
|
||||
|
||||
_text->setSelectable(true);
|
||||
_text->resizeToWidth(st::groupCallMenuAbout.minWidth);
|
||||
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
|
||||
}
|
||||
|
||||
not_null<QAction*> AboutItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool AboutItem::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int AboutItem::contentHeight() const {
|
||||
return _st.itemPadding.top()
|
||||
+ _text->height()
|
||||
+ _st.itemPadding.bottom();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
69
Telegram/SourceFiles/calls/group/calls_cover_item.h
Normal file
69
Telegram/SourceFiles/calls/group/calls_cover_item.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "boxes/peers/peer_short_info_box.h"
|
||||
|
||||
struct PreparedShortInfoUserpic;
|
||||
|
||||
namespace style {
|
||||
struct ShortInfoCover;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
class CoverItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
CoverItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &stMenu,
|
||||
const style::ShortInfoCover &st,
|
||||
rpl::producer<QString> name,
|
||||
rpl::producer<QString> status,
|
||||
PreparedShortInfoUserpic userpic);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
|
||||
const PeerShortInfoCover _cover;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::ShortInfoCover &_st;
|
||||
|
||||
};
|
||||
|
||||
class AboutItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
AboutItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
TextWithEntities &&about);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
|
||||
const style::Menu &_st;
|
||||
const base::unique_qptr<Ui::FlatLabel> _text;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
4357
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
4357
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
File diff suppressed because it is too large
Load Diff
815
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
815
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/bytes.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "mtproto/mtproto_auth_key.h"
|
||||
#include "webrtc/webrtc_device_common.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace tgcalls {
|
||||
class GroupInstanceCustomImpl;
|
||||
struct GroupLevelsUpdate;
|
||||
struct GroupNetworkState;
|
||||
struct GroupParticipantDescription;
|
||||
class VideoCaptureInterface;
|
||||
enum class VideoCodecName;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
class GlobalShortcutManager;
|
||||
class GlobalShortcutValue;
|
||||
} // namespace base
|
||||
|
||||
namespace Webrtc {
|
||||
class MediaDevices;
|
||||
class VideoTrack;
|
||||
enum class VideoState;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Data {
|
||||
struct LastSpokeTimes;
|
||||
struct GroupCallParticipant;
|
||||
class GroupCall;
|
||||
enum class GroupCallOrigin : uchar;
|
||||
} // namespace Data
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
class EncryptDecrypt;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace Calls::Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
struct JoinInfo;
|
||||
struct ConferenceInfo;
|
||||
struct RejoinEvent;
|
||||
struct RtmpInfo;
|
||||
enum class VideoQuality;
|
||||
enum class Error;
|
||||
class Messages;
|
||||
} // namespace Calls::Group
|
||||
|
||||
namespace Calls {
|
||||
|
||||
struct InviteRequest;
|
||||
struct InviteResult;
|
||||
struct StartConferenceInfo;
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
RaisedHand,
|
||||
};
|
||||
|
||||
[[nodiscard]] inline auto MapPushToTalkToActive() {
|
||||
return rpl::map([=](MuteState state) {
|
||||
return (state == MuteState::PushToTalk) ? MuteState::Active : state;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGroupCallAdmin(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PeerData*> participantPeer);
|
||||
|
||||
struct LevelUpdate {
|
||||
uint32 ssrc = 0;
|
||||
float value = 0.;
|
||||
bool voice = false;
|
||||
bool me = false;
|
||||
};
|
||||
|
||||
enum class VideoEndpointType {
|
||||
Camera,
|
||||
Screen,
|
||||
};
|
||||
|
||||
struct VideoEndpoint {
|
||||
VideoEndpoint() = default;
|
||||
VideoEndpoint(
|
||||
VideoEndpointType type,
|
||||
not_null<PeerData*> peer,
|
||||
std::string id)
|
||||
: type(type)
|
||||
, peer(peer)
|
||||
, id(std::move(id)) {
|
||||
}
|
||||
|
||||
VideoEndpointType type = VideoEndpointType::Camera;
|
||||
PeerData *peer = nullptr;
|
||||
std::string id;
|
||||
|
||||
[[nodiscard]] bool rtmp() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
Expects(id.empty() || peer != nullptr);
|
||||
|
||||
return id.empty();
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const noexcept {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
inline bool operator==(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.id == b.id);
|
||||
}
|
||||
|
||||
inline bool operator!=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator<(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.peer < b.peer)
|
||||
|| (a.peer == b.peer && a.id < b.id);
|
||||
}
|
||||
|
||||
inline bool operator>(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (b < a);
|
||||
}
|
||||
|
||||
inline bool operator<=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
inline bool operator>=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct VideoStateToggle {
|
||||
VideoEndpoint endpoint;
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
struct VideoQualityRequest {
|
||||
VideoEndpoint endpoint;
|
||||
Group::VideoQuality quality = Group::VideoQuality();
|
||||
};
|
||||
|
||||
struct ParticipantVideoParams;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ParticipantVideoParams> ParseVideoParams(
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &camera,
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &screen,
|
||||
const std::shared_ptr<ParticipantVideoParams> &existing);
|
||||
|
||||
[[nodiscard]] const std::string &GetCameraEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] const std::string &GetScreenEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsCameraPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsScreenPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] uint32 GetAdditionalAudioSsrc(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
|
||||
class GroupCall final
|
||||
: public base::has_weak_ptr
|
||||
, private Webrtc::CaptureMuteTracker {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
virtual void groupCallFinished(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) = 0;
|
||||
|
||||
enum class GroupCallSound {
|
||||
Started,
|
||||
Connecting,
|
||||
AllowedToSpeak,
|
||||
Ended,
|
||||
RecordingStarted,
|
||||
};
|
||||
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
|
||||
virtual auto groupCallGetVideoCapture(const QString &deviceId)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
[[nodiscard]] virtual FnMut<void()> groupCallAddAsyncWaiter() = 0;
|
||||
};
|
||||
|
||||
using GlobalShortcutManager = base::GlobalShortcutManager;
|
||||
|
||||
struct VideoTrack;
|
||||
|
||||
[[nodiscard]] static not_null<PeerData*> TrackPeer(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
[[nodiscard]] static not_null<Webrtc::VideoTrack*> TrackPointer(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
[[nodiscard]] static rpl::producer<QSize> TrackSizeValue(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
GroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] CallId id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> joinAs() const {
|
||||
return _joinAs.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> joinAsValue() const {
|
||||
return _joinAs.value();
|
||||
}
|
||||
[[nodiscard]] not_null<Group::Messages*> messages() const {
|
||||
return _messages.get();
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> messagesFrom() const;
|
||||
[[nodiscard]] bool showChooseJoinAs() const;
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate;
|
||||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] bool conference() const;
|
||||
[[nodiscard]] bool videoStream() const;
|
||||
[[nodiscard]] Data::GroupCallOrigin origin() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool emptyRtmp() const;
|
||||
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
|
||||
[[nodiscard]] int rtmpVolume() const;
|
||||
|
||||
[[nodiscard]] Group::RtmpInfo rtmpInfo() const;
|
||||
|
||||
void setRtmpInfo(const Group::RtmpInfo &value);
|
||||
|
||||
[[nodiscard]] Data::GroupCall *lookupReal() const;
|
||||
[[nodiscard]] std::shared_ptr<Data::GroupCall> sharedCall() const;
|
||||
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
|
||||
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
|
||||
|
||||
void applyInputCall(const MTPInputGroupCall &inputCall);
|
||||
void startConference();
|
||||
void start(TimeId scheduleDate, bool rtmp);
|
||||
void hangup();
|
||||
void discard();
|
||||
void rejoinAs(Group::JoinInfo info);
|
||||
void rejoinWithHash(const QString &hash);
|
||||
void initialJoin();
|
||||
void initialJoinRequested();
|
||||
void handleUpdate(const MTPUpdate &update);
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void handlePossibleCreateOrJoinResponse(
|
||||
const MTPDupdateGroupCallConnection &data);
|
||||
void handleIncomingMessage(const MTPDupdateGroupCallMessage &data);
|
||||
void handleIncomingMessage(
|
||||
const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
void handleDeleteMessages(const MTPDupdateDeleteGroupCallMessages &data);
|
||||
void handleMessageSent(const MTPDupdateMessageID &data);
|
||||
void changeTitle(const QString &title);
|
||||
void toggleRecording(
|
||||
bool enabled,
|
||||
const QString &title,
|
||||
bool video,
|
||||
bool videoPortrait);
|
||||
void playSoundRecordingStarted() const;
|
||||
[[nodiscard]] bool recordingStoppedByMe() const {
|
||||
return _recordingStoppedByMe;
|
||||
}
|
||||
void startScheduledNow();
|
||||
void toggleScheduleStartSubscribed(bool subscribed);
|
||||
void setNoiseSuppression(bool enabled);
|
||||
void removeConferenceParticipants(
|
||||
const base::flat_set<UserId> userIds,
|
||||
bool removingStale = false);
|
||||
|
||||
bool emitShareScreenError();
|
||||
bool emitShareCameraError();
|
||||
|
||||
void joinDone(
|
||||
int64 serverTimeMs,
|
||||
const MTPUpdates &result,
|
||||
MuteState wasMuteState,
|
||||
bool wasVideoStopped,
|
||||
bool justCreated = false);
|
||||
void joinFail(const QString &error);
|
||||
|
||||
[[nodiscard]] rpl::producer<Group::Error> errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
void addVideoOutput(
|
||||
const std::string &endpoint,
|
||||
not_null<Webrtc::VideoTrack*> track);
|
||||
|
||||
void setMuted(MuteState mute);
|
||||
void setMutedAndUpdate(MuteState mute);
|
||||
[[nodiscard]] MuteState muted() const {
|
||||
return _muted.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<MuteState> mutedValue() const {
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState>;
|
||||
|
||||
enum State {
|
||||
Creating,
|
||||
Waiting,
|
||||
Joining,
|
||||
Connecting,
|
||||
Joined,
|
||||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
Ended,
|
||||
};
|
||||
[[nodiscard]] State state() const {
|
||||
return _state.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
|
||||
enum class InstanceState {
|
||||
Disconnected,
|
||||
TransitionToRtc,
|
||||
Connected,
|
||||
};
|
||||
[[nodiscard]] InstanceState instanceState() const {
|
||||
return _instanceState.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {
|
||||
return _instanceState.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
|
||||
return _levelUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamActiveUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamActiveUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamShownUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamShownUpdates.events();
|
||||
}
|
||||
void requestVideoQuality(
|
||||
const VideoEndpoint &endpoint,
|
||||
Group::VideoQuality quality);
|
||||
|
||||
[[nodiscard]] bool videoEndpointPinned() const {
|
||||
return _videoEndpointPinned.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {
|
||||
return _videoEndpointPinned.value();
|
||||
}
|
||||
void pinVideoEndpoint(VideoEndpoint endpoint);
|
||||
|
||||
void showVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {
|
||||
return _videoEndpointLarge.current();
|
||||
}
|
||||
[[nodiscard]] auto videoEndpointLargeValue() const
|
||||
-> rpl::producer<VideoEndpoint> {
|
||||
return _videoEndpointLarge.value();
|
||||
}
|
||||
[[nodiscard]] auto activeVideoTracks() const
|
||||
-> const base::flat_map<VideoEndpoint, std::unique_ptr<VideoTrack>> & {
|
||||
return _activeVideoTracks;
|
||||
}
|
||||
[[nodiscard]] auto shownVideoTracks() const
|
||||
-> const base::flat_set<VideoEndpoint> & {
|
||||
return _shownVideoTracks;
|
||||
}
|
||||
[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {
|
||||
return _rejoinEvents.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {
|
||||
return _allowedToSpeakNotifications.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> titleChanged() const {
|
||||
return _titleChanged.events();
|
||||
}
|
||||
static constexpr auto kSpeakLevelThreshold = 0.2;
|
||||
|
||||
[[nodiscard]] bool mutedByAdmin() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] rpl::producer<bool> canManageValue() const;
|
||||
[[nodiscard]] bool videoIsWorking() const {
|
||||
return _videoIsWorking.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {
|
||||
return _videoIsWorking.value();
|
||||
}
|
||||
[[nodiscard]] bool messagesEnabled() const {
|
||||
return _messagesEnabled.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {
|
||||
return _messagesEnabled.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;
|
||||
[[nodiscard]] bool isScreenPaused() const;
|
||||
[[nodiscard]] const std::string &screenSharingEndpoint() const;
|
||||
[[nodiscard]] bool isSharingCamera() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingCameraValue() const;
|
||||
[[nodiscard]] bool isCameraPaused() const;
|
||||
[[nodiscard]] const std::string &cameraSharingEndpoint() const;
|
||||
[[nodiscard]] QString screenSharingDeviceId() const;
|
||||
[[nodiscard]] bool screenSharingWithAudio() const;
|
||||
void toggleVideo(bool active);
|
||||
void toggleScreenSharing(
|
||||
std::optional<QString> uniqueId,
|
||||
bool withAudio = false);
|
||||
[[nodiscard]] bool hasVideoWithFrames() const;
|
||||
[[nodiscard]] rpl::producer<bool> hasVideoWithFramesValue() const;
|
||||
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
|
||||
void inviteUsers(
|
||||
const std::vector<InviteRequest> &requests,
|
||||
Fn<void(InviteResult)> done);
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
void setNotRequireARGB32();
|
||||
|
||||
[[nodiscard]] std::function<std::vector<uint8_t>(
|
||||
std::vector<uint8_t> const &,
|
||||
int64_t, bool,
|
||||
int32_t)> e2eEncryptDecrypt() const;
|
||||
void sendMessage(TextWithTags message);
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
class LoadPartTask;
|
||||
class MediaChannelDescriptionsTask;
|
||||
class RequestCurrentTimeTask;
|
||||
using GlobalShortcutValue = base::GlobalShortcutValue;
|
||||
using Error = Group::Error;
|
||||
struct SinkPointer;
|
||||
|
||||
static constexpr uint32 kDisabledSsrc = uint32(-1);
|
||||
static constexpr int kSubChainsCount = 2;
|
||||
|
||||
struct LoadingPart {
|
||||
std::shared_ptr<LoadPartTask> task;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
enum class FinishType {
|
||||
None,
|
||||
Ended,
|
||||
Failed,
|
||||
};
|
||||
enum class InstanceMode {
|
||||
None,
|
||||
Rtc,
|
||||
Stream,
|
||||
};
|
||||
enum class SendUpdateType {
|
||||
Mute = 0x01,
|
||||
RaiseHand = 0x02,
|
||||
CameraStopped = 0x04,
|
||||
CameraPaused = 0x08,
|
||||
ScreenPaused = 0x10,
|
||||
};
|
||||
enum class JoinAction {
|
||||
None,
|
||||
Joining,
|
||||
Leaving,
|
||||
};
|
||||
struct JoinPayload {
|
||||
uint32 ssrc = 0;
|
||||
QByteArray json;
|
||||
};
|
||||
struct JoinState {
|
||||
uint32 ssrc = 0;
|
||||
JoinAction action = JoinAction::None;
|
||||
JoinPayload payload;
|
||||
bool nextActionPending = false;
|
||||
|
||||
void finish(uint32 updatedSsrc = 0) {
|
||||
action = JoinAction::None;
|
||||
ssrc = updatedSsrc;
|
||||
}
|
||||
};
|
||||
struct SubChainPending {
|
||||
QVector<MTPbytes> blocks;
|
||||
int next = 0;
|
||||
};
|
||||
struct SubChainState {
|
||||
std::vector<SubChainPending> pending;
|
||||
mtpRequestId requestId = 0;
|
||||
bool inShortPoll = false;
|
||||
};
|
||||
|
||||
friend inline constexpr bool is_flag_type(SendUpdateType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo join,
|
||||
StartConferenceInfo conference,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
|
||||
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
|
||||
void broadcastPartCancel(not_null<LoadPartTask*> task);
|
||||
void mediaChannelDescriptionsStart(
|
||||
std::shared_ptr<MediaChannelDescriptionsTask> task);
|
||||
void mediaChannelDescriptionsCancel(
|
||||
not_null<MediaChannelDescriptionsTask*> task);
|
||||
void requestCurrentTimeStart(
|
||||
std::shared_ptr<RequestCurrentTimeTask> task);
|
||||
void requestCurrentTimeCancel(
|
||||
not_null<RequestCurrentTimeTask*> task);
|
||||
[[nodiscard]] int64 approximateServerTimeInMs() const;
|
||||
|
||||
[[nodiscard]] bool mediaChannelDescriptionsFill(
|
||||
not_null<MediaChannelDescriptionsTask*> task,
|
||||
Fn<bool(uint32)> resolved = nullptr);
|
||||
void checkMediaChannelDescriptions(Fn<bool(uint32)> resolved = nullptr);
|
||||
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);
|
||||
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
|
||||
void handleUpdate(const MTPDupdateGroupCall &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
|
||||
void applySubChainUpdate(
|
||||
int subchain,
|
||||
const QVector<MTPbytes> &blocks,
|
||||
int next);
|
||||
[[nodiscard]] auto lookupVideoCodecPreferences() const
|
||||
-> std::vector<tgcalls::VideoCodecName>;
|
||||
bool tryCreateController();
|
||||
void destroyController();
|
||||
bool tryCreateScreencast();
|
||||
void destroyScreencast();
|
||||
|
||||
void emitShareCameraError(Error error);
|
||||
void emitShareScreenError(Error error);
|
||||
|
||||
void setState(State state);
|
||||
void finish(FinishType type);
|
||||
void maybeSendMutedUpdate(MuteState previous);
|
||||
void sendSelfUpdate(SendUpdateType type);
|
||||
void updateInstanceMuteState();
|
||||
void updateInstanceVolumes();
|
||||
void updateInstanceVolume(
|
||||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant &now);
|
||||
void applyMeInCallLocally();
|
||||
void startRejoin();
|
||||
void rejoin();
|
||||
void leave();
|
||||
void rejoin(not_null<PeerData*> as);
|
||||
void setJoinAs(not_null<PeerData*> as);
|
||||
void saveDefaultJoinAs(not_null<PeerData*> as);
|
||||
void subscribeToReal(not_null<Data::GroupCall*> real);
|
||||
void setScheduledDate(TimeId date);
|
||||
void setMessagesEnabled(bool enabled);
|
||||
void rejoinPresentation();
|
||||
void leavePresentation();
|
||||
void checkNextJoinAction();
|
||||
void sendJoinRequest();
|
||||
void refreshLastBlockAndJoin();
|
||||
void requestSubchainBlocks(int subchain, int height);
|
||||
void sendOutboundBlock(QByteArray block);
|
||||
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setInstanceMode(InstanceMode mode);
|
||||
void setScreenInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setScreenInstanceMode(InstanceMode mode);
|
||||
void checkLastSpoke();
|
||||
void pushToTalkCancel();
|
||||
|
||||
void checkGlobalShortcutAvailability();
|
||||
void checkJoined();
|
||||
void checkFirstTimeJoined();
|
||||
void notifyAboutAllowedToSpeak();
|
||||
|
||||
void playConnectingSound();
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
void updateRequestedVideoChannels();
|
||||
void updateRequestedVideoChannelsDelayed();
|
||||
void fillActiveVideoEndpoints();
|
||||
|
||||
void editParticipant(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyParticipantLocally(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyQueuedSelfUpdates();
|
||||
void sendPendingSelfUpdates();
|
||||
void applySelfUpdate(const MTPDgroupCallParticipant &data);
|
||||
void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);
|
||||
|
||||
void captureMuteChanged(bool mute) override;
|
||||
rpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;
|
||||
|
||||
void setupMediaDevices();
|
||||
void setupOutgoingVideo();
|
||||
void initConferenceE2E();
|
||||
void setupConferenceCall();
|
||||
void trackParticipantsWithAccess();
|
||||
void setScreenEndpoint(std::string endpoint);
|
||||
void setCameraEndpoint(std::string endpoint);
|
||||
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
|
||||
void setVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
|
||||
void markEndpointActive(
|
||||
VideoEndpoint endpoint,
|
||||
bool active,
|
||||
bool paused);
|
||||
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
|
||||
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
|
||||
|
||||
void processConferenceStart(StartConferenceInfo conference);
|
||||
void inviteToConference(
|
||||
InviteRequest request,
|
||||
Fn<not_null<InviteResult*>()> resultAddress,
|
||||
Fn<void()> finishRequest);
|
||||
|
||||
[[nodiscard]] float64 singleSourceVolumeValue() const;
|
||||
[[nodiscard]] int activeVideoSendersCount() const;
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
std::shared_ptr<Data::GroupCall> _sharedCall;
|
||||
std::unique_ptr<TdE2E::Call> _e2e;
|
||||
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
|
||||
rpl::variable<QByteArray> _emojiHash;
|
||||
QByteArray _pendingOutboundBlock;
|
||||
std::shared_ptr<StartConferenceInfo> _startConferenceInfo;
|
||||
|
||||
not_null<PeerData*> _peer; // Can change in legacy group migration.
|
||||
rpl::event_stream<PeerData*> _peerStream;
|
||||
not_null<History*> _history; // Can change in legacy group migration.
|
||||
MTP::Sender _api;
|
||||
rpl::event_stream<not_null<Data::GroupCall*>> _realChanges;
|
||||
rpl::variable<State> _state = State::Creating;
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
rpl::event_stream<Error> _errors;
|
||||
std::vector<Fn<void()>> _rejoinedCallbacks;
|
||||
const std::unique_ptr<Group::Messages> _messages;
|
||||
bool _recordingStoppedByMe = false;
|
||||
bool _requestedVideoChannelsUpdateScheduled = false;
|
||||
|
||||
MTP::DcId _broadcastDcId = 0;
|
||||
base::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;
|
||||
base::flat_set<
|
||||
std::shared_ptr<MediaChannelDescriptionsTask>,
|
||||
base::pointer_comparator<
|
||||
MediaChannelDescriptionsTask>> _mediaChannelDescriptionses;
|
||||
base::flat_set<
|
||||
std::shared_ptr<RequestCurrentTimeTask>,
|
||||
base::pointer_comparator<
|
||||
RequestCurrentTimeTask>> _requestCurrentTimes;
|
||||
mtpRequestId _requestCurrentTimeRequestId = 0;
|
||||
|
||||
rpl::variable<not_null<PeerData*>> _joinAs;
|
||||
std::vector<not_null<PeerData*>> _possibleJoinAs;
|
||||
QString _joinHash;
|
||||
QString _conferenceLinkSlug;
|
||||
MsgId _conferenceJoinMessageId;
|
||||
int64 _serverTimeMs = 0;
|
||||
crl::time _serverTimeMsGotAt = 0;
|
||||
|
||||
QString _rtmpUrl;
|
||||
QString _rtmpKey;
|
||||
|
||||
rpl::variable<MuteState> _muted = MuteState::Muted;
|
||||
rpl::variable<bool> _canManage = false;
|
||||
rpl::variable<bool> _videoIsWorking = false;
|
||||
rpl::variable<bool> _emptyRtmp = false;
|
||||
rpl::variable<bool> _messagesEnabled = false;
|
||||
bool _initialMuteStateSent = false;
|
||||
bool _acceptFields = false;
|
||||
|
||||
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
|
||||
std::vector<MTPGroupCallParticipant> _queuedSelfUpdates;
|
||||
|
||||
CallId _id = 0;
|
||||
CallId _accessHash = 0;
|
||||
JoinState _joinState;
|
||||
JoinState _screenJoinState;
|
||||
std::string _cameraEndpoint;
|
||||
std::string _screenEndpoint;
|
||||
TimeId _scheduleDate = 0;
|
||||
base::flat_set<uint32> _mySsrcs;
|
||||
mtpRequestId _createRequestId = 0;
|
||||
mtpRequestId _selfUpdateRequestId = 0;
|
||||
|
||||
rpl::variable<InstanceState> _instanceState
|
||||
= InstanceState::Disconnected;
|
||||
bool _instanceTransitioning = false;
|
||||
InstanceMode _instanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;
|
||||
base::has_weak_ptr _instanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _cameraCapture;
|
||||
rpl::variable<Webrtc::VideoState> _cameraState;
|
||||
rpl::variable<bool> _isSharingCamera = false;
|
||||
base::flat_map<std::string, SinkPointer> _pendingVideoOutputs;
|
||||
|
||||
rpl::variable<InstanceState> _screenInstanceState
|
||||
= InstanceState::Disconnected;
|
||||
InstanceMode _screenInstanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _screenInstance;
|
||||
base::has_weak_ptr _screenInstanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _screenCapture;
|
||||
rpl::variable<Webrtc::VideoState> _screenState;
|
||||
rpl::variable<bool> _isSharingScreen = false;
|
||||
QString _screenDeviceId;
|
||||
bool _screenWithAudio = false;
|
||||
|
||||
base::flags<SendUpdateType> _pendingSelfUpdates;
|
||||
bool _requireARGB32 = true;
|
||||
|
||||
rpl::event_stream<LevelUpdate> _levelUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamActiveUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamPausedUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamShownUpdates;
|
||||
base::flat_map<
|
||||
VideoEndpoint,
|
||||
std::unique_ptr<VideoTrack>> _activeVideoTracks;
|
||||
base::flat_set<VideoEndpoint> _shownVideoTracks;
|
||||
rpl::variable<VideoEndpoint> _videoEndpointLarge;
|
||||
rpl::variable<bool> _videoEndpointPinned = false;
|
||||
crl::time _videoLargeTillTime = 0;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
rpl::event_stream<Group::RejoinEvent> _rejoinEvents;
|
||||
rpl::event_stream<> _allowedToSpeakNotifications;
|
||||
rpl::event_stream<> _titleChanged;
|
||||
base::Timer _lastSpokeCheckTimer;
|
||||
base::Timer _checkJoinedTimer;
|
||||
|
||||
crl::time _lastSendProgressUpdate = 0;
|
||||
|
||||
Fn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;
|
||||
Webrtc::DeviceResolver _playbackDeviceId;
|
||||
Webrtc::DeviceResolver _captureDeviceId;
|
||||
Webrtc::DeviceResolver _cameraDeviceId;
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
|
||||
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
|
||||
base::Timer _pushToTalkCancelTimer;
|
||||
base::Timer _connectingSoundTimer;
|
||||
bool _hadJoinedState = false;
|
||||
bool _listenersHidden = false;
|
||||
bool _rtmp = false;
|
||||
bool _reloadedStaleCall = false;
|
||||
int _singleSourceVolume = 0;
|
||||
|
||||
SubChainState _subchains[kSubChainsCount];
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
|
||||
const InviteResult &result);
|
||||
|
||||
} // namespace Calls
|
||||
506
Telegram/SourceFiles/calls/group/calls_group_common.cpp
Normal file
506
Telegram/SourceFiles/calls/group/calls_group_common.cpp
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
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_common.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/random.h"
|
||||
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
|
||||
#include "boxes/share_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "core/local_url_handlers.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "info/bot/starref/info_bot_starref_common.h"
|
||||
#include "tde2e/tde2e_api.h"
|
||||
#include "tde2e/tde2e_integration.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
|
||||
#ifdef Q_OS_MAC
|
||||
if (!Platform::IsMac10_15OrGreater()) {
|
||||
return { nullptr };
|
||||
}
|
||||
return Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
rpl::combine(
|
||||
tr::lng_group_call_mac_screencast_access(),
|
||||
tr::lng_group_call_mac_recording()
|
||||
) | rpl::map([](QString a, QString b) {
|
||||
auto result = tr::rich(a);
|
||||
result.append("\n\n").append(tr::rich(b));
|
||||
return result;
|
||||
}),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
box->addButton(tr::lng_group_call_mac_settings(), [=] {
|
||||
Platform::OpenDesktopCapturePrivacySettings();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
});
|
||||
#else // Q_OS_MAC
|
||||
return { nullptr };
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
|
||||
not_null<QWidget*> parent,
|
||||
const style::icon &icon,
|
||||
const style::margins &padding) {
|
||||
const auto logoSize = icon.size();
|
||||
const auto logoOuter = logoSize.grownBy(padding);
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto logo = result.data();
|
||||
logo->resize(logo->width(), logoOuter.height());
|
||||
logo->paintRequest() | rpl::on_next([=, &icon] {
|
||||
if (logo->width() < logoOuter.width()) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(logo);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto x = (logo->width() - logoOuter.width()) / 2;
|
||||
const auto outer = QRect(QPoint(x, 0), logoOuter);
|
||||
p.setBrush(st::windowBgActive);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(outer);
|
||||
icon.paintInCenter(p, outer);
|
||||
}, logo->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
|
||||
return MakeRoundActiveLogo(
|
||||
parent,
|
||||
st::confcallJoinLogo,
|
||||
st::confcallJoinLogoPadding);
|
||||
}
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join) {
|
||||
box->setStyle(st::confcallJoinBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
box->addRow(
|
||||
MakeJoinCallLogo(box),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_join_title(),
|
||||
st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
const auto wrapName = [&](not_null<PeerData*> peer) {
|
||||
return rpl::single(tr::bold(peer->shortName()));
|
||||
};
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
(maybeInviter
|
||||
? tr::lng_confcall_join_text_inviter(
|
||||
lt_user,
|
||||
wrapName(maybeInviter),
|
||||
tr::rich)
|
||||
: tr::lng_confcall_join_text(tr::rich)),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
const auto known = int(participants.size());
|
||||
if (known) {
|
||||
const auto sep = box->addRow(
|
||||
object_ptr<Ui::RpWidget>(box),
|
||||
st::boxRowPadding + st::confcallJoinSepPadding);
|
||||
sep->resize(sep->width(), st::normalFont->height);
|
||||
sep->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.2);
|
||||
p.fillRect(0, top, sep->width(), line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
auto peers = std::vector<not_null<PeerData*>>();
|
||||
for (const auto &participant : participants) {
|
||||
peers.push_back(participant.peer);
|
||||
if (peers.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
box->addRow(
|
||||
CreateUserpicsWithMoreBadge(
|
||||
box,
|
||||
rpl::single(peers),
|
||||
st::confcallJoinUserpics,
|
||||
known),
|
||||
st::boxRowPadding + st::confcallJoinUserpicsPadding);
|
||||
|
||||
const auto wrapByIndex = [&](int index) {
|
||||
Expects(index >= 0 && index < known);
|
||||
|
||||
return wrapName(participants[index].peer);
|
||||
};
|
||||
auto text = (known == 1)
|
||||
? tr::lng_confcall_already_joined_one(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
tr::rich)
|
||||
: (known == 2)
|
||||
? tr::lng_confcall_already_joined_two(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
tr::rich)
|
||||
: (known == 3)
|
||||
? tr::lng_confcall_already_joined_three(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
lt_third,
|
||||
wrapByIndex(2),
|
||||
tr::rich)
|
||||
: tr::lng_confcall_already_joined_many(
|
||||
lt_count,
|
||||
rpl::single(1. * (std::max(known, call->fullCount()) - 2)),
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
tr::rich);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
}
|
||||
box->addButton(tr::lng_confcall_join_button(), [=] {
|
||||
join([weak = base::make_weak(box)] {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {
|
||||
return {
|
||||
.box = &st::groupCallLinkBox,
|
||||
.menuToggle = &st::groupCallLinkMenu,
|
||||
.menu = &st::groupCallPopupMenuWithIcons,
|
||||
.close = &st::storiesStealthBoxClose,
|
||||
.centerLabel = &st::groupCallLinkCenteredText,
|
||||
.linkPreview = &st::groupCallLinkPreview,
|
||||
.contextRevoke = &st::mediaMenuIconRemove,
|
||||
.shareBox = std::make_shared<ShareBoxStyleOverrides>(
|
||||
DarkShareBoxStyle()),
|
||||
};
|
||||
}
|
||||
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args) {
|
||||
const auto st = args.st;
|
||||
const auto initial = args.initial;
|
||||
const auto link = call->conferenceInviteLink();
|
||||
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
bool resetting = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
box->setStyle(st.box
|
||||
? *st.box
|
||||
: initial
|
||||
? st::confcallLinkBoxInitial
|
||||
: st::confcallLinkBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
const auto close = box->addTopButton(
|
||||
st.close ? *st.close : st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
|
||||
if (!args.initial && call->canManage()) {
|
||||
const auto toggle = Ui::CreateChild<Ui::IconButton>(
|
||||
close->parentWidget(),
|
||||
st.menuToggle ? *st.menuToggle : st::boxTitleMenu);
|
||||
const auto handler = [=] {
|
||||
if (state->resetting) {
|
||||
return;
|
||||
}
|
||||
state->resetting = true;
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
const auto weak = base::make_weak(box);
|
||||
call->session().api().request(
|
||||
MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_reset_invite_hash),
|
||||
call->input(),
|
||||
MTPBool(), // join_muted
|
||||
MTPBool(), // messages_enabled
|
||||
MTPlong()) // send_paid_messages_stars
|
||||
).done([=](const MTPUpdates &result) {
|
||||
call->session().api().applyUpdates(result);
|
||||
ShowConferenceCallLinkBox(show, call, args);
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
show->showToast({
|
||||
.title = tr::lng_confcall_link_revoked_title(
|
||||
tr::now),
|
||||
.text = {
|
||||
tr::lng_confcall_link_revoked_text(tr::now),
|
||||
},
|
||||
});
|
||||
}).send();
|
||||
};
|
||||
toggle->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
toggle,
|
||||
st.menu ? *st.menu : st::popupMenuWithIcons);
|
||||
state->menu->addAction(
|
||||
tr::lng_confcall_link_revoke(tr::now),
|
||||
handler,
|
||||
(st.contextRevoke
|
||||
? st.contextRevoke
|
||||
: &st::menuIconRemove));
|
||||
state->menu->popup(QCursor::pos());
|
||||
});
|
||||
|
||||
close->geometryValue(
|
||||
) | rpl::on_next([=](QRect geometry) {
|
||||
toggle->moveToLeft(
|
||||
geometry.x() - toggle->width(),
|
||||
geometry.y());
|
||||
}, close->lifetime());
|
||||
}
|
||||
|
||||
box->addRow(
|
||||
Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_title(),
|
||||
st.box ? st.box->title : st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_about(),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText)),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
|
||||
const auto preview = box->addRow(
|
||||
Info::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
const auto copyCallback = [=] {
|
||||
QApplication::clipboard()->setText(link);
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
};
|
||||
const auto shareCallback = [=] {
|
||||
FastShareLink(
|
||||
show,
|
||||
link,
|
||||
st.shareBox ? *st.shareBox : ShareBoxStyleOverrides());
|
||||
};
|
||||
preview->setClickedCallback(copyCallback);
|
||||
const auto share = box->addButton(
|
||||
tr::lng_group_invite_share(),
|
||||
shareCallback,
|
||||
st::confcallLinkShareButton);
|
||||
const auto copy = box->addButton(
|
||||
tr::lng_group_invite_copy(),
|
||||
copyCallback,
|
||||
st::confcallLinkCopyButton);
|
||||
|
||||
rpl::combine(
|
||||
box->widthValue(),
|
||||
copy->widthValue(),
|
||||
share->widthValue()
|
||||
) | rpl::on_next([=] {
|
||||
const auto width = st::boxWideWidth;
|
||||
const auto padding = st::confcallLinkBox.buttonPadding;
|
||||
const auto available = width - 2 * padding.right();
|
||||
const auto buttonWidth = (available - padding.left()) / 2;
|
||||
copy->resizeToWidth(buttonWidth);
|
||||
share->resizeToWidth(buttonWidth);
|
||||
copy->moveToLeft(padding.right(), copy->y(), width);
|
||||
share->moveToRight(padding.right(), share->y(), width);
|
||||
}, box->lifetime());
|
||||
|
||||
if (!initial) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sep = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_or(),
|
||||
st::confcallLinkFooterOr);
|
||||
sep->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto text = sep->textMaxWidth();
|
||||
const auto white = (sep->width() - 2 * text) / 2;
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.4);
|
||||
p.fillRect(0, top, white, line, fg);
|
||||
p.fillRect(sep->width() - white, top, white, line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
const auto footer = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_join(
|
||||
lt_link,
|
||||
tr::lng_confcall_link_join_link(
|
||||
lt_arrow,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
|
||||
[](QString v) { return tr::link(v); }),
|
||||
tr::marked),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText));
|
||||
footer->setTryMakeSimilarLines(true);
|
||||
footer->setClickHandlerFilter([=](const auto &...) {
|
||||
if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.call = call,
|
||||
.linkSlug = std::move(slug),
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
copy->geometryValue() | rpl::on_next([=](QRect geometry) {
|
||||
const auto width = st::boxWideWidth
|
||||
- st::boxRowPadding.left()
|
||||
- st::boxRowPadding.right();
|
||||
footer->resizeToWidth(width);
|
||||
const auto top = geometry.y()
|
||||
+ geometry.height()
|
||||
+ st::confcallLinkFooterOrTop;
|
||||
sep->resizeToWidth(width / 2);
|
||||
sep->move(
|
||||
st::boxRowPadding.left() + (width - sep->width()) / 2,
|
||||
top);
|
||||
footer->moveToLeft(
|
||||
st::boxRowPadding.left(),
|
||||
top + sep->height() + st::confcallLinkFooterOrSkip);
|
||||
}, footer->lifetime());
|
||||
}));
|
||||
}
|
||||
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args) {
|
||||
const auto show = std::move(args.show);
|
||||
const auto finished = std::move(args.finished);
|
||||
const auto session = &show->session();
|
||||
const auto fail = [=](QString error) {
|
||||
show->showToast(error);
|
||||
if (const auto onstack = finished) {
|
||||
onstack(false);
|
||||
}
|
||||
};
|
||||
session->api().request(MTPphone_CreateConferenceCall(
|
||||
MTP_flags(0),
|
||||
MTP_int(base::RandomValue<int32>()),
|
||||
MTPint256(), // public_key
|
||||
MTPbytes(), // block
|
||||
MTPDataJSON() // params
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
auto call = session->data().sharedConferenceCallFind(result);
|
||||
if (!call) {
|
||||
fail(u"Call not found!"_q);
|
||||
return;
|
||||
}
|
||||
session->api().applyUpdates(result);
|
||||
|
||||
const auto link = call ? call->conferenceInviteLink() : QString();
|
||||
if (link.isEmpty()) {
|
||||
fail(u"Call link not found!"_q);
|
||||
return;
|
||||
}
|
||||
Calls::Group::ShowConferenceCallLinkBox(
|
||||
show,
|
||||
call,
|
||||
{ .initial = true });
|
||||
if (const auto onstack = finished) {
|
||||
finished(true);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
fail(error.type());
|
||||
}).send();
|
||||
}
|
||||
|
||||
QString ExtractConferenceSlug(const QString &link) {
|
||||
const auto local = Core::TryConvertUrlToLocal(link);
|
||||
const auto parts1 = QStringView(local).split('#');
|
||||
if (!parts1.isEmpty()) {
|
||||
const auto parts2 = parts1.front().split('&');
|
||||
if (!parts2.isEmpty()) {
|
||||
const auto parts3 = parts2.front().split(u"slug="_q);
|
||||
if (parts3.size() > 1) {
|
||||
return parts3.back().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
209
Telegram/SourceFiles/calls/group/calls_group_common.h
Normal file
209
Telegram/SourceFiles/calls/group/calls_group_common.h
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class UserData;
|
||||
struct ShareBoxStyleOverrides;
|
||||
|
||||
namespace style {
|
||||
struct Box;
|
||||
struct FlatLabel;
|
||||
struct IconButton;
|
||||
struct InputField;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Window;
|
||||
|
||||
struct InviteRequest {
|
||||
not_null<UserData*> user;
|
||||
bool video = false;
|
||||
};
|
||||
|
||||
struct InviteResult {
|
||||
std::vector<not_null<UserData*>> invited;
|
||||
std::vector<not_null<UserData*>> alreadyIn;
|
||||
std::vector<not_null<UserData*>> privacyRestricted;
|
||||
std::vector<not_null<UserData*>> kicked;
|
||||
std::vector<not_null<UserData*>> failed;
|
||||
};
|
||||
|
||||
struct StartConferenceInfo {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
std::shared_ptr<Data::GroupCall> call;
|
||||
QString linkSlug;
|
||||
MsgId joinMessageId;
|
||||
std::vector<InviteRequest> invite;
|
||||
bool sharingLink = false;
|
||||
bool migrating = false;
|
||||
bool muted = false;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;
|
||||
QString videoCaptureScreenId;
|
||||
};
|
||||
|
||||
struct ConferencePanelMigration {
|
||||
std::shared_ptr<Window> window;
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
constexpr auto kDefaultVolume = 10000;
|
||||
constexpr auto kMaxVolume = 20000;
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
|
||||
struct MuteRequest {
|
||||
not_null<PeerData*> peer;
|
||||
bool mute = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct VolumeRequest {
|
||||
not_null<PeerData*> peer;
|
||||
int volume = kDefaultVolume;
|
||||
bool finalized = true;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct ParticipantState {
|
||||
not_null<PeerData*> peer;
|
||||
std::optional<int> volume;
|
||||
bool mutedByMe = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct RejoinEvent {
|
||||
not_null<PeerData*> wasJoinAs;
|
||||
not_null<PeerData*> nowJoinAs;
|
||||
};
|
||||
|
||||
struct RtmpInfo {
|
||||
QString url;
|
||||
QString key;
|
||||
};
|
||||
|
||||
struct JoinInfo {
|
||||
not_null<PeerData*> peer;
|
||||
not_null<PeerData*> joinAs;
|
||||
std::vector<not_null<PeerData*>> possibleJoinAs;
|
||||
QString joinHash;
|
||||
RtmpInfo rtmpInfo;
|
||||
TimeId scheduleDate = 0;
|
||||
bool rtmp = false;
|
||||
};
|
||||
|
||||
enum class PanelMode {
|
||||
Default,
|
||||
Wide,
|
||||
VideoStream,
|
||||
};
|
||||
|
||||
enum class VideoQuality {
|
||||
Thumbnail,
|
||||
Medium,
|
||||
Full,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
NoCamera,
|
||||
CameraFailed,
|
||||
ScreenFailed,
|
||||
MutedNoCamera,
|
||||
MutedNoScreen,
|
||||
DisabledNoCamera,
|
||||
DisabledNoScreen,
|
||||
};
|
||||
|
||||
enum class StickedTooltip {
|
||||
Camera = 0x01,
|
||||
Microphone = 0x02,
|
||||
};
|
||||
constexpr inline bool is_flag_type(StickedTooltip) {
|
||||
return true;
|
||||
}
|
||||
using StickedTooltips = base::flags<StickedTooltip>;
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
|
||||
not_null<QWidget*> parent,
|
||||
const style::icon &icon,
|
||||
const style::margins &padding);
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
|
||||
not_null<QWidget*> parent);
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join);
|
||||
|
||||
struct ConferenceCallLinkStyleOverrides {
|
||||
const style::Box *box = nullptr;
|
||||
const style::IconButton *menuToggle = nullptr;
|
||||
const style::PopupMenu *menu = nullptr;
|
||||
const style::IconButton *close = nullptr;
|
||||
const style::FlatLabel *centerLabel = nullptr;
|
||||
const style::InputField *linkPreview = nullptr;
|
||||
const style::icon *contextRevoke = nullptr;
|
||||
std::shared_ptr<ShareBoxStyleOverrides> shareBox;
|
||||
};
|
||||
[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();
|
||||
|
||||
struct ConferenceCallLinkArgs {
|
||||
ConferenceCallLinkStyleOverrides st;
|
||||
bool initial = false;
|
||||
};
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args);
|
||||
|
||||
struct ConferenceFactoryArgs {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
Fn<void(bool)> finished;
|
||||
bool joining = false;
|
||||
StartConferenceInfo info;
|
||||
};
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args);
|
||||
|
||||
[[nodiscard]] QString ExtractConferenceSlug(const QString &link);
|
||||
|
||||
} // namespace Calls::Group
|
||||
1213
Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp
Normal file
1213
Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
105
Telegram/SourceFiles/calls/group/calls_group_invite_controller.h
Normal file
105
Telegram/SourceFiles/calls/group/calls_group_invite_controller.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/add_participants_box.h"
|
||||
|
||||
namespace Calls {
|
||||
class Call;
|
||||
class GroupCall;
|
||||
struct InviteRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
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]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast,
|
||||
Fn<void()> shareConferenceLink = nullptr);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<Call*> call,
|
||||
Fn<void(std::vector<InviteRequest>)> inviteUsers,
|
||||
Fn<void()> shareLink);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
MsgId inviteMsgId,
|
||||
std::vector<not_null<UserData*>> prioritize);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(
|
||||
not_null<::Window::SessionController*> window,
|
||||
Fn<void()> created = nullptr,
|
||||
MsgId discardedInviteMsgId = 0,
|
||||
std::vector<not_null<UserData*>> prioritize = {});
|
||||
|
||||
} // namespace Calls::Group
|
||||
2225
Telegram/SourceFiles/calls/group/calls_group_members.cpp
Normal file
2225
Telegram/SourceFiles/calls/group/calls_group_members.cpp
Normal file
File diff suppressed because it is too large
Load Diff
131
Telegram/SourceFiles/calls/group/calls_group_members.h
Normal file
131
Telegram/SourceFiles/calls/group/calls_group_members.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class ScrollArea;
|
||||
class VerticalLayout;
|
||||
class SettingsButton;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct FingerprintBadgeState;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport;
|
||||
class MembersRow;
|
||||
struct VolumeRequest;
|
||||
struct MuteRequest;
|
||||
enum class PanelMode;
|
||||
|
||||
class Members final
|
||||
: public Ui::RpWidget
|
||||
, private PeerListContentDelegate {
|
||||
public:
|
||||
Members(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<GroupCall*> call,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend);
|
||||
~Members();
|
||||
|
||||
[[nodiscard]] not_null<Viewport*> viewport() const;
|
||||
[[nodiscard]] int desiredHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] auto toggleMuteRequests() const
|
||||
-> rpl::producer<Group::MuteRequest>;
|
||||
[[nodiscard]] auto changeVolumeRequests() const
|
||||
-> rpl::producer<Group::VolumeRequest>;
|
||||
[[nodiscard]] auto kickParticipantRequests() const
|
||||
-> rpl::producer<not_null<PeerData*>>;
|
||||
[[nodiscard]] rpl::producer<> addMembersRequests() const {
|
||||
return _addMemberRequests.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> shareLinkRequests() const {
|
||||
return _shareLinkRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(
|
||||
not_null<PeerData*> peer) const;
|
||||
|
||||
void setMode(PanelMode mode);
|
||||
[[nodiscard]] QRect getInnerGeometry() const;
|
||||
|
||||
private:
|
||||
class Controller;
|
||||
struct VideoTile;
|
||||
using ListWidget = PeerListContent;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
// PeerListContentDelegate interface.
|
||||
void peerListSetTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetHideEmpty(bool hide) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
void peerListAddSelectedRowInBunch(
|
||||
not_null<PeerListRow*> row) override;
|
||||
void peerListFinishSelectedRowsBunch() override;
|
||||
void peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) override;
|
||||
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
|
||||
|
||||
void setupAddMember(not_null<GroupCall*> call);
|
||||
void resizeToList();
|
||||
void setupList();
|
||||
void setupFingerprint();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void trackViewportGeometry();
|
||||
void updateControlsGeometry();
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
rpl::variable<PanelMode> _mode = PanelMode();
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<Controller> _listController;
|
||||
not_null<Ui::VerticalLayout*> _layout;
|
||||
Ui::RpWidget *_fingerprint = nullptr;
|
||||
rpl::event_stream<> _fingerprintRepaints;
|
||||
const FingerprintBadgeState *_fingerprintState = nullptr;
|
||||
const not_null<Ui::RpWidget*> _videoWrap;
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
|
||||
rpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;
|
||||
RpWidget *_topSkip = nullptr;
|
||||
RpWidget *_bottomSkip = nullptr;
|
||||
ListWidget *_list = nullptr;
|
||||
rpl::event_stream<> _addMemberRequests;
|
||||
rpl::event_stream<> _shareLinkRequests;
|
||||
|
||||
mutable std::unique_ptr<MembersRow> _rtmpFakeRow;
|
||||
|
||||
rpl::variable<bool> _canInviteByLink;
|
||||
rpl::variable<bool> _canAddMembers;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
822
Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
Normal file
822
Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
Normal file
@@ -0,0 +1,822 @@
|
||||
/*
|
||||
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_members_row.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/paint/arcs.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kArcsStrokeRatio = 0.8;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
Group::kDefaultVolume * 0.1f / Group::kMaxVolume,
|
||||
Group::kDefaultVolume * 0.9f / Group::kMaxVolume };
|
||||
|
||||
auto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::groupCallRowBlobMinRadius,
|
||||
.maxRadius = (float)st::groupCallRowBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
[[nodiscard]] QString StatusPercentString(float volume) {
|
||||
return QString::number(int(base::SafeRound(volume * 200))) + '%';
|
||||
}
|
||||
|
||||
[[nodiscard]] int StatusPercentWidth(const QString &percent) {
|
||||
return st::normalFont->width(percent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MembersRow::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel);
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSoundingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
|
||||
QImage userpicCache;
|
||||
InMemoryKey userpicKey;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
struct MembersRow::StatusIcon {
|
||||
StatusIcon(bool shown, float volume);
|
||||
|
||||
const style::icon &speaker;
|
||||
Ui::Paint::ArcsAnimation arcs;
|
||||
Ui::Animations::Simple arcsAnimation;
|
||||
Ui::Animations::Simple shownAnimation;
|
||||
QString percent;
|
||||
int percentWidth = 0;
|
||||
int arcsWidth = 0;
|
||||
int wasArcsWidth = 0;
|
||||
bool shown = true;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
MembersRow::BlobsAnimation::BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
userpicCache = QImage();
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
MembersRow::StatusIcon::StatusIcon(bool shown, float volume)
|
||||
: speaker(st::groupCallStatusSpeakerIcon)
|
||||
, arcs(
|
||||
st::groupCallStatusSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
volume,
|
||||
Ui::Paint::ArcsAnimation::Direction::Right)
|
||||
, percent(StatusPercentString(volume))
|
||||
, percentWidth(StatusPercentWidth(percent))
|
||||
, shown(shown) {
|
||||
}
|
||||
|
||||
MembersRow::MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer)
|
||||
: PeerListRow(participantPeer)
|
||||
, _delegate(delegate) {
|
||||
refreshStatus();
|
||||
_about.setText(st::defaultTextStyle, participantPeer->about());
|
||||
}
|
||||
|
||||
MembersRow::~MembersRow() = default;
|
||||
|
||||
void MembersRow::setSkipLevelUpdate(bool value) {
|
||||
_skipLevelUpdate = value;
|
||||
}
|
||||
|
||||
void MembersRow::updateStateInvited(bool calling) {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(calling ? State::Calling : State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateStateWithAccess() {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(State::WithAccess);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateState(const Data::GroupCallParticipant &participant) {
|
||||
setVolume(participant.volume);
|
||||
if (!participant.muted
|
||||
|| (participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {
|
||||
setState(State::Active);
|
||||
setSounding((participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
setSpeaking((participant.speaking && participant.ssrc != 0)
|
||||
|| (participant.additionalSpeaking
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else if (participant.canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else {
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = participant.raisedHandRating;
|
||||
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
|
||||
}
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::setSpeaking(bool speaking) {
|
||||
if (_speaking == speaking) {
|
||||
return;
|
||||
}
|
||||
_speaking = speaking;
|
||||
_speakingAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
_speaking ? 0. : 1.,
|
||||
_speaking ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
|
||||
if (!_speaking
|
||||
|| _mutedByMe
|
||||
|| (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand)) {
|
||||
if (_statusIcon) {
|
||||
_statusIcon = nullptr;
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
} else if (!_statusIcon) {
|
||||
_statusIcon = std::make_unique<StatusIcon>(
|
||||
(_volume != Group::kDefaultVolume),
|
||||
(float)_volume / Group::kMaxVolume);
|
||||
_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);
|
||||
_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();
|
||||
_statusIcon->arcs.startUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
if (!_statusIcon->arcsAnimation.animating()) {
|
||||
_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;
|
||||
}
|
||||
auto callback = [=](float64 value) {
|
||||
_statusIcon->arcs.update(crl::now());
|
||||
_statusIcon->arcsWidth = anim::interpolate(
|
||||
_statusIcon->wasArcsWidth,
|
||||
_statusIcon->arcs.finishedWidth(),
|
||||
value);
|
||||
_delegate->rowUpdateRow(this);
|
||||
};
|
||||
_statusIcon->arcsAnimation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}, _statusIcon->lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setSounding(bool sounding) {
|
||||
if (_sounding == sounding) {
|
||||
return;
|
||||
}
|
||||
_sounding = sounding;
|
||||
if (!_sounding) {
|
||||
_blobsAnimation = nullptr;
|
||||
} else if (!_blobsAnimation) {
|
||||
_blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
RowBlobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
_blobsAnimation->lastTime = crl::now();
|
||||
updateLevel(GroupCall::kSpeakLevelThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::clearRaisedHandStatus() {
|
||||
if (!_raisedHandStatus) {
|
||||
return;
|
||||
}
|
||||
_raisedHandStatus = false;
|
||||
refreshStatus();
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::setState(State state) {
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
const auto wasActive = (_state == State::Active);
|
||||
const auto wasMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto wasRaisedHand = (_state == State::RaisedHand);
|
||||
_state = state;
|
||||
const auto nowActive = (_state == State::Active);
|
||||
const auto nowMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto nowRaisedHand = (_state == State::RaisedHand);
|
||||
if (!wasRaisedHand && nowRaisedHand) {
|
||||
_raisedHandStatus = true;
|
||||
_delegate->rowScheduleRaisedHandStatusRemove(this);
|
||||
}
|
||||
if (nowActive != wasActive) {
|
||||
_activeAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowActive ? 0. : 1.,
|
||||
nowActive ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
if (nowMuted != wasMuted) {
|
||||
_mutedAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowMuted ? 0. : 1.,
|
||||
nowMuted ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setVolume(int volume) {
|
||||
_volume = volume;
|
||||
if (_statusIcon) {
|
||||
const auto floatVolume = (float)volume / Group::kMaxVolume;
|
||||
_statusIcon->arcs.setValue(floatVolume);
|
||||
_statusIcon->percent = StatusPercentString(floatVolume);
|
||||
_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);
|
||||
|
||||
const auto shown = (volume != Group::kDefaultVolume);
|
||||
if (_statusIcon->shown != shown) {
|
||||
_statusIcon->shown = shown;
|
||||
_statusIcon->shownAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::updateLevel(float level) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto spoke = (level >= GroupCall::kSpeakLevelThreshold)
|
||||
? crl::now()
|
||||
: crl::time();
|
||||
if (spoke && _speaking) {
|
||||
_speakingLastTime = spoke;
|
||||
}
|
||||
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spoke) {
|
||||
_blobsAnimation->lastSoundingUpdateTime = spoke;
|
||||
}
|
||||
_blobsAnimation->blobs.setLevel(level);
|
||||
}
|
||||
|
||||
void MembersRow::updateBlobAnimation(crl::time now) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
|
||||
+ Data::GroupCall::kSoundStatusKeptFor;
|
||||
const auto soundingStartsFinishing = soundingFinishesAt
|
||||
- kBlobsEnterDuration;
|
||||
const auto soundingFinishes = (soundingStartsFinishing < now);
|
||||
if (soundingFinishes) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
|
||||
0.,
|
||||
1.);
|
||||
} else if (_blobsAnimation->enter < 1.) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(_blobsAnimation->enter
|
||||
+ ((now - _blobsAnimation->lastTime)
|
||||
/ float64(kBlobsEnterDuration))),
|
||||
0.,
|
||||
1.);
|
||||
}
|
||||
_blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime);
|
||||
_blobsAnimation->lastTime = now;
|
||||
}
|
||||
|
||||
void MembersRow::ensureUserpicCache(
|
||||
Ui::PeerUserpicView &view,
|
||||
int size) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto user = peer();
|
||||
const auto key = user->userpicUniqueKey(view);
|
||||
const auto full = QSize(size, size)
|
||||
* kWideScale
|
||||
* style::DevicePixelRatio();
|
||||
auto &cache = _blobsAnimation->userpicCache;
|
||||
if (cache.isNull()) {
|
||||
cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
} else if (_blobsAnimation->userpicKey == key
|
||||
&& cache.size() == full) {
|
||||
return;
|
||||
}
|
||||
_blobsAnimation->userpicKey = key;
|
||||
cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
if (!_blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
auto size = sizew;
|
||||
const auto shift = QPointF(x + size / 2., y + size / 2.);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.translate(shift);
|
||||
const auto brush = _mutedByMe
|
||||
? st::groupCallMemberMutedIcon->b
|
||||
: anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
_blobsAnimation->blobs.paint(p, brush);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
void MembersRow::paintScaledUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
auto size = sizew;
|
||||
if (!_blobsAnimation) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
const auto enter = _blobsAnimation->enter;
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
const auto scaleUserpic = minScale
|
||||
+ (1. - minScale) * _blobsAnimation->blobs.currentLevel();
|
||||
const auto scale = scaleUserpic * enter + 1. * (1. - enter);
|
||||
if (scale == 1.) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
ensureUserpicCache(userpic, size);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
auto target = QRect(
|
||||
x + (1 - kWideScale) / 2 * size,
|
||||
y + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(
|
||||
target.marginsAdded(margins),
|
||||
_blobsAnimation->userpicCache);
|
||||
}
|
||||
|
||||
void MembersRow::paintMuteIcon(
|
||||
QPainter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style) {
|
||||
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
|
||||
}
|
||||
|
||||
QString MembersRow::generateName() {
|
||||
const auto result = peer()->name();
|
||||
if (result.isEmpty()) {
|
||||
DEBUG_LOG(("UnknownParticipant: %1, Loaded: %2, Name Version: %3"
|
||||
).arg(peerToUser(peer()->id).bare
|
||||
).arg(peer()->isLoaded() ? "TRUE" : "FALSE"
|
||||
).arg(peer()->nameVersion()));
|
||||
}
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
QString MembersRow::generateShortName() {
|
||||
const auto result = peer()->shortName();
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
auto MembersRow::generatePaintUserpicCallback(bool forceRound)
|
||||
-> PaintRoundImageCallback {
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) {
|
||||
const auto outer = outerWidth;
|
||||
paintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default);
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected) {
|
||||
paintBlobs(p, x, y, sizew, sizeh, mode);
|
||||
paintScaledUserpic(
|
||||
p,
|
||||
ensureUserpicView(),
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
sizew,
|
||||
sizeh,
|
||||
mode);
|
||||
}
|
||||
|
||||
int MembersRow::statusIconWidth(bool skipIcon) const {
|
||||
if (!_statusIcon || !_speaking) {
|
||||
return 0;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (_statusIcon->speaker.width() + _statusIcon->arcsWidth);
|
||||
const auto full = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
return int(base::SafeRound(shown * full));
|
||||
}
|
||||
|
||||
int MembersRow::statusIconHeight() const {
|
||||
return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon) {
|
||||
if (!_statusIcon) {
|
||||
return;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.setFont(font);
|
||||
const auto color = (_speaking
|
||||
? st.statusFgActive
|
||||
: (selected ? st.statusFgOver : st.statusFg))->c;
|
||||
p.setPen(color);
|
||||
|
||||
const auto speakerRect = QRect(
|
||||
QPoint(x, y + (font->height - statusIconHeight()) / 2),
|
||||
_statusIcon->speaker.size());
|
||||
const auto arcPosition = speakerRect.topLeft()
|
||||
+ QPoint(
|
||||
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
|
||||
speakerRect.height() / 2);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (speakerRect.width() + _statusIcon->arcsWidth);
|
||||
const auto fullWidth = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
|
||||
p.save();
|
||||
if (shown < 1.) {
|
||||
const auto centerx = speakerRect.x() + fullWidth / 2;
|
||||
const auto centery = speakerRect.y() + speakerRect.height() / 2;
|
||||
p.translate(centerx, centery);
|
||||
p.scale(shown, shown);
|
||||
p.translate(-centerx, -centery);
|
||||
}
|
||||
if (!skipIcon) {
|
||||
_statusIcon->speaker.paint(
|
||||
p,
|
||||
speakerRect.topLeft(),
|
||||
speakerRect.width(),
|
||||
color);
|
||||
p.translate(arcPosition);
|
||||
_statusIcon->arcs.paint(p, color);
|
||||
p.translate(-arcPosition);
|
||||
}
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st.statusFgActive);
|
||||
p.drawTextLeft(
|
||||
x + iconWidth,
|
||||
y,
|
||||
fullWidth,
|
||||
_statusIcon->percent);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void MembersRow::setAbout(const QString &about) {
|
||||
if (_about.toString() == about) {
|
||||
return;
|
||||
}
|
||||
_about.setText(st::defaultTextStyle, about);
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
paintComplexStatusText(
|
||||
p,
|
||||
st,
|
||||
x,
|
||||
y,
|
||||
availableWidth,
|
||||
outerWidth,
|
||||
selected,
|
||||
MembersRowStyle::Default);
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style) {
|
||||
const auto skip = (style == MembersRowStyle::Default)
|
||||
? _delegate->rowPaintStatusIcon(
|
||||
p,
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
this,
|
||||
computeIconState(MembersRowStyle::Narrow))
|
||||
: 0;
|
||||
const auto narrowMode = (skip > 0);
|
||||
x += skip;
|
||||
availableWidth -= skip;
|
||||
const auto &font = st::normalFont;
|
||||
const auto useAbout = !_about.isEmpty()
|
||||
&& (_state != State::WithAccess)
|
||||
&& (_state != State::Invited)
|
||||
&& (_state != State::Calling)
|
||||
&& (style != MembersRowStyle::Video)
|
||||
&& ((_state == State::RaisedHand && !_raisedHandStatus)
|
||||
|| (_state != State::RaisedHand && !_speaking));
|
||||
if (!useAbout
|
||||
&& _state != State::Invited
|
||||
&& _state != State::Calling
|
||||
&& _state != State::WithAccess
|
||||
&& !_mutedByMe) {
|
||||
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
|
||||
|
||||
const auto translatedWidth = statusIconWidth(narrowMode);
|
||||
p.translate(translatedWidth, 0);
|
||||
const auto guard = gsl::finally([&] {
|
||||
p.translate(-translatedWidth, 0);
|
||||
});
|
||||
|
||||
const auto &style = (!narrowMode
|
||||
|| (_state == State::RaisedHand && _raisedHandStatus))
|
||||
? st
|
||||
: st::groupCallNarrowMembersListItem;
|
||||
PeerListRow::paintStatusText(
|
||||
p,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
availableWidth - translatedWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
return;
|
||||
}
|
||||
p.setPen((style == MembersRowStyle::Video)
|
||||
? st::groupCallVideoSubTextFg
|
||||
: _mutedByMe
|
||||
? st::groupCallMemberMutedIcon
|
||||
: st::groupCallMemberNotJoinedStatus);
|
||||
if (!_mutedByMe && useAbout) {
|
||||
return _about.draw(p, {
|
||||
.position = QPoint(x, y),
|
||||
.outerWidth = outerWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
} else {
|
||||
p.setFont(font);
|
||||
p.drawTextLeft(
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
(_mutedByMe
|
||||
? tr::lng_group_call_muted_by_me_status(tr::now)
|
||||
: _delegate->rowIsMe(peer())
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: (_state == State::WithAccess)
|
||||
? tr::lng_group_call_blockchain_only_status(tr::now)
|
||||
: (_state == State::Calling)
|
||||
? tr::lng_group_call_calling_status(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
}
|
||||
|
||||
QSize MembersRow::rightActionSize() const {
|
||||
return _delegate->rowIsNarrow() ? QSize() : QSize(
|
||||
st::groupCallActiveButton.width,
|
||||
st::groupCallActiveButton.height);
|
||||
}
|
||||
|
||||
bool MembersRow::rightActionDisabled() const {
|
||||
return _delegate->rowIsMe(peer())
|
||||
|| (_state == State::Invited)
|
||||
|| (_state == State::Calling)
|
||||
|| !_delegate->rowCanMuteMembers();
|
||||
}
|
||||
|
||||
QMargins MembersRow::rightActionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
0,
|
||||
st::groupCallMemberButtonSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void MembersRow::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = rightActionSize();
|
||||
const auto iconRect = style::rtlrect(
|
||||
x,
|
||||
y,
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
if (_state == State::Invited
|
||||
|| _state == State::Calling
|
||||
|| _state == State::WithAccess) {
|
||||
_actionRipple = nullptr;
|
||||
}
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
x + st::groupCallActiveButton.rippleAreaPosition.x(),
|
||||
y + st::groupCallActiveButton.rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
paintMuteIcon(p, iconRect);
|
||||
}
|
||||
|
||||
MembersRowDelegate::IconState MembersRow::computeIconState(
|
||||
MembersRowStyle style) const {
|
||||
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
|
||||
const auto active = _activeAnimation.value(
|
||||
(_state == State::Active) ? 1. : 0.);
|
||||
const auto muted = _mutedAnimation.value(
|
||||
(_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.);
|
||||
return {
|
||||
.speaking = speaking,
|
||||
.active = active,
|
||||
.muted = muted,
|
||||
.mutedByMe = _mutedByMe,
|
||||
.raisedHand = (_state == State::RaisedHand),
|
||||
.invited = (_state == State::Invited),
|
||||
.calling = (_state == State::Calling),
|
||||
.style = style,
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::showContextMenu() {
|
||||
return _delegate->rowShowContextMenu(this);
|
||||
}
|
||||
|
||||
void MembersRow::refreshStatus() {
|
||||
setCustomStatus(
|
||||
(_speaking
|
||||
? tr::lng_group_call_active(tr::now)
|
||||
: _raisedHandStatus
|
||||
? tr::lng_group_call_raised_hand_status(tr::now)
|
||||
: tr::lng_group_call_inactive(tr::now)),
|
||||
_speaking);
|
||||
}
|
||||
|
||||
void MembersRow::rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
|
||||
st::groupCallActiveButton.rippleAreaSize,
|
||||
st::groupCallActiveButton.rippleAreaSize));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::groupCallActiveButton.ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition);
|
||||
}
|
||||
|
||||
void MembersRow::refreshName(const style::PeerListItem &st) {
|
||||
PeerListRow::refreshName(st);
|
||||
//_narrowName = Ui::Text::String();
|
||||
}
|
||||
|
||||
void MembersRow::rightActionStopLastRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
230
Telegram/SourceFiles/calls/group/calls_group_members_row.h
Normal file
230
Telegram/SourceFiles/calls/group/calls_group_members_row.h
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
class PeerData;
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
struct GroupCallParticipant;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
struct PeerUserpicView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class MembersRowStyle : uchar {
|
||||
Default,
|
||||
Narrow,
|
||||
Video,
|
||||
};
|
||||
|
||||
class MembersRow;
|
||||
class MembersRowDelegate {
|
||||
public:
|
||||
struct IconState {
|
||||
float64 speaking = 0.;
|
||||
float64 active = 0.;
|
||||
float64 muted = 0.;
|
||||
bool mutedByMe = false;
|
||||
bool raisedHand = false;
|
||||
bool invited = false;
|
||||
bool calling = false;
|
||||
MembersRowStyle style = MembersRowStyle::Default;
|
||||
};
|
||||
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
|
||||
virtual bool rowCanMuteMembers() = 0;
|
||||
virtual void rowUpdateRow(not_null<MembersRow*> row) = 0;
|
||||
virtual void rowScheduleRaisedHandStatusRemove(
|
||||
not_null<MembersRow*> row) = 0;
|
||||
virtual void rowPaintIcon(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const IconState &state) = 0;
|
||||
virtual int rowPaintStatusIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<MembersRow*> row,
|
||||
const IconState &state) = 0;
|
||||
virtual bool rowIsNarrow() = 0;
|
||||
virtual void rowShowContextMenu(not_null<PeerListRow*> row) = 0;
|
||||
};
|
||||
|
||||
class MembersRow final : public PeerListRow {
|
||||
public:
|
||||
MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer);
|
||||
~MembersRow();
|
||||
|
||||
enum class State {
|
||||
Active,
|
||||
Inactive,
|
||||
Muted,
|
||||
RaisedHand,
|
||||
Invited,
|
||||
Calling,
|
||||
WithAccess,
|
||||
};
|
||||
|
||||
void setAbout(const QString &about);
|
||||
void setSkipLevelUpdate(bool value);
|
||||
void updateState(const Data::GroupCallParticipant &participant);
|
||||
void updateStateInvited(bool calling);
|
||||
void updateStateWithAccess();
|
||||
void updateLevel(float level);
|
||||
void updateBlobAnimation(crl::time now);
|
||||
void clearRaisedHandStatus();
|
||||
[[nodiscard]] State state() const {
|
||||
return _state;
|
||||
}
|
||||
[[nodiscard]] bool sounding() const {
|
||||
return _sounding;
|
||||
}
|
||||
[[nodiscard]] bool speaking() const {
|
||||
return _speaking;
|
||||
}
|
||||
[[nodiscard]] bool mutedByMe() const {
|
||||
return _mutedByMe;
|
||||
}
|
||||
[[nodiscard]] crl::time speakingLastTime() const {
|
||||
return _speakingLastTime;
|
||||
}
|
||||
[[nodiscard]] int volume() const {
|
||||
return _volume;
|
||||
}
|
||||
[[nodiscard]] uint64 raisedHandRating() const {
|
||||
return _raisedHandRating;
|
||||
}
|
||||
|
||||
void refreshName(const style::PeerListItem &st) override;
|
||||
|
||||
void rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void rightActionStopLastRipple() override;
|
||||
QSize rightActionSize() const override;
|
||||
bool rightActionDisabled() const override;
|
||||
QMargins rightActionMargins() const override;
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
QString generateName() override;
|
||||
QString generateShortName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(
|
||||
bool forceRound) override;
|
||||
void paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected = false);
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
void paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style);
|
||||
void paintMuteIcon(
|
||||
QPainter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style = MembersRowStyle::Default);
|
||||
[[nodiscard]] MembersRowDelegate::IconState computeIconState(
|
||||
MembersRowStyle style = MembersRowStyle::Default) const;
|
||||
|
||||
void showContextMenu();
|
||||
|
||||
private:
|
||||
struct BlobsAnimation;
|
||||
struct StatusIcon;
|
||||
|
||||
int statusIconWidth(bool skipIcon) const;
|
||||
int statusIconHeight() const;
|
||||
void paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon);
|
||||
|
||||
void refreshStatus() override;
|
||||
void setSounding(bool sounding);
|
||||
void setSpeaking(bool speaking);
|
||||
void setState(State state);
|
||||
void setVolume(int volume);
|
||||
|
||||
void ensureUserpicCache(
|
||||
Ui::PeerUserpicView &view,
|
||||
int size);
|
||||
void paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh, PanelMode mode);
|
||||
void paintScaledUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode);
|
||||
|
||||
const not_null<MembersRowDelegate*> _delegate;
|
||||
State _state = State::Inactive;
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
std::unique_ptr<BlobsAnimation> _blobsAnimation;
|
||||
std::unique_ptr<StatusIcon> _statusIcon;
|
||||
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
|
||||
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
|
||||
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
|
||||
Ui::Text::String _about;
|
||||
crl::time _speakingLastTime = 0;
|
||||
uint64 _raisedHandRating = 0;
|
||||
int _volume = Group::kDefaultVolume;
|
||||
bool _sounding : 1 = false;
|
||||
bool _speaking : 1 = false;
|
||||
bool _raisedHandStatus : 1 = false;
|
||||
bool _skipLevelUpdate : 1 = false;
|
||||
bool _mutedByMe : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
645
Telegram/SourceFiles/calls/group/calls_group_menu.cpp
Normal file
645
Telegram/SourceFiles/calls/group/calls_group_menu.cpp
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
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_menu.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "calls/group/ui/calls_group_recording_box.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
|
||||
#include "ui/widgets/dropdown_menu.h"
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
class JoinAsAction final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
JoinAsAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void prepare();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const not_null<PeerData*> _peer;
|
||||
Ui::PeerUserpicView _userpicView;
|
||||
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::String _name;
|
||||
int _textWidth = 0;
|
||||
int _nameWidth = 0;
|
||||
const int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
class RecordingAction final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
RecordingAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<TimeId> startAtValues,
|
||||
Fn<void()> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void prepare(rpl::producer<QString> text);
|
||||
void refreshElapsedText();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
TimeId _startAt = 0;
|
||||
crl::time _startedAt = 0;
|
||||
base::Timer _refreshTimer;
|
||||
|
||||
Ui::Text::String _text;
|
||||
int _textWidth = 0;
|
||||
QString _elapsedText;
|
||||
const int _smallHeight = 0;
|
||||
const int _bigHeight = 0;
|
||||
|
||||
};
|
||||
|
||||
TextParseOptions MenuTextOptions = {
|
||||
TextParseLinks, // flags
|
||||
0, // maxw
|
||||
0, // maxh
|
||||
Qt::LayoutDirectionAuto, // dir
|
||||
};
|
||||
|
||||
JoinAsAction::JoinAsAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback)
|
||||
: ItemBase(parent, st)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _peer(peer)
|
||||
, _height(st::groupCallJoinAsPadding.top()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ st::groupCallJoinAsPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback(std::move(callback));
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
prepare();
|
||||
}
|
||||
|
||||
void JoinAsAction::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
const auto height = contentHeight();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
const auto &padding = st::groupCallJoinAsPadding;
|
||||
_peer->paintUserpic(
|
||||
p,
|
||||
_userpicView,
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
st::groupCallJoinAsPhotoSize);
|
||||
const auto textLeft = padding.left()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ padding.left();
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
textLeft,
|
||||
st::groupCallJoinAsTextTop,
|
||||
_textWidth,
|
||||
width());
|
||||
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
textLeft,
|
||||
st::groupCallJoinAsNameTop,
|
||||
_nameWidth,
|
||||
width());
|
||||
}
|
||||
|
||||
void JoinAsAction::prepare() {
|
||||
rpl::combine(
|
||||
tr::lng_group_call_display_as_header(),
|
||||
Info::Profile::NameValue(_peer)
|
||||
) | rpl::on_next([=](QString text, QString name) {
|
||||
const auto &padding = st::groupCallJoinAsPadding;
|
||||
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
|
||||
_name.setMarkedText(_st.itemStyle, { name }, MenuTextOptions);
|
||||
const auto textWidth = _text.maxWidth();
|
||||
const auto nameWidth = _name.maxWidth();
|
||||
const auto textLeft = padding.left()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ padding.left();
|
||||
const auto w = std::clamp(
|
||||
(textLeft
|
||||
+ std::max(textWidth, nameWidth)
|
||||
+ padding.right()),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
setMinWidth(w);
|
||||
_textWidth = w - textLeft - padding.right();
|
||||
_nameWidth = w - textLeft - padding.right();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
bool JoinAsAction::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> JoinAsAction::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint JoinAsAction::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage JoinAsAction::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int JoinAsAction::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void JoinAsAction::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
RecordingAction::RecordingAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<TimeId> startAtValues,
|
||||
Fn<void()> callback)
|
||||
: ItemBase(parent, st)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _refreshTimer([=] { refreshElapsedText(); })
|
||||
, _smallHeight(st.itemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st.itemPadding.bottom())
|
||||
, _bigHeight(st::groupCallRecordingTimerPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st::groupCallRecordingTimerFont->height
|
||||
+ st::groupCallRecordingTimerPadding.bottom()) {
|
||||
std::move(
|
||||
startAtValues
|
||||
) | rpl::on_next([=](TimeId startAt) {
|
||||
_startAt = startAt;
|
||||
_startedAt = crl::now();
|
||||
_refreshTimer.cancel();
|
||||
refreshElapsedText();
|
||||
resize(width(), contentHeight());
|
||||
}, lifetime());
|
||||
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback(std::move(callback));
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
prepare(std::move(text));
|
||||
}
|
||||
|
||||
void RecordingAction::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
const auto height = contentHeight();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
const auto smallTop = st::groupCallRecordingTimerPadding.top();
|
||||
const auto textTop = _startAt ? smallTop : _st.itemPadding.top();
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
textTop,
|
||||
_textWidth,
|
||||
width());
|
||||
if (_startAt) {
|
||||
p.setFont(st::groupCallRecordingTimerFont);
|
||||
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
|
||||
p.drawTextLeft(
|
||||
_st.itemPadding.left(),
|
||||
smallTop + _st.itemStyle.font->height,
|
||||
width(),
|
||||
_elapsedText);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordingAction::refreshElapsedText() {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto elapsed = std::max(now - _startAt, 0);
|
||||
const auto text = !_startAt
|
||||
? QString()
|
||||
: (elapsed >= 3600)
|
||||
? QString("%1:%2:%3"
|
||||
).arg(elapsed / 3600
|
||||
).arg((elapsed % 3600) / 60, 2, 10, QChar('0')
|
||||
).arg(elapsed % 60, 2, 10, QChar('0'))
|
||||
: QString("%1:%2"
|
||||
).arg(elapsed / 60
|
||||
).arg(elapsed % 60, 2, 10, QChar('0'));
|
||||
if (_elapsedText != text) {
|
||||
_elapsedText = text;
|
||||
update();
|
||||
}
|
||||
|
||||
const auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500);
|
||||
_refreshTimer.callOnce(nextCall);
|
||||
}
|
||||
|
||||
void RecordingAction::prepare(rpl::producer<QString> text) {
|
||||
refreshElapsedText();
|
||||
|
||||
const auto &padding = _st.itemPadding;
|
||||
const auto textWidth1 = _st.itemStyle.font->width(
|
||||
tr::lng_group_call_recording_start(tr::now));
|
||||
const auto textWidth2 = _st.itemStyle.font->width(
|
||||
tr::lng_group_call_recording_stop(tr::now));
|
||||
const auto maxWidth = st::groupCallRecordingTimerFont->width("23:59:59");
|
||||
const auto w = std::clamp(
|
||||
(padding.left()
|
||||
+ std::max({ textWidth1, textWidth2, maxWidth })
|
||||
+ padding.right()),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
setMinWidth(w);
|
||||
|
||||
std::move(text) | rpl::on_next([=](QString text) {
|
||||
const auto &padding = _st.itemPadding;
|
||||
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
|
||||
_textWidth = w - padding.left() - padding.right();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
bool RecordingAction::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> RecordingAction::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint RecordingAction::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage RecordingAction::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int RecordingAction::contentHeight() const {
|
||||
return _startAt ? _bigHeight : _smallHeight;
|
||||
}
|
||||
|
||||
void RecordingAction::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeJoinAsAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback) {
|
||||
return base::make_unique_q<JoinAsAction>(
|
||||
menu,
|
||||
menu->st(),
|
||||
peer,
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
rpl::producer<TimeId> startDate,
|
||||
Fn<void()> callback) {
|
||||
using namespace rpl::mappers;
|
||||
return base::make_unique_q<RecordingAction>(
|
||||
menu,
|
||||
menu->st(),
|
||||
rpl::conditional(
|
||||
rpl::duplicate(startDate) | rpl::map(!!_1),
|
||||
tr::lng_group_call_recording_stop(),
|
||||
tr::lng_group_call_recording_start()),
|
||||
rpl::duplicate(startDate),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LeaveBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call,
|
||||
bool discardChecked,
|
||||
BoxContext context) {
|
||||
const auto conference = call->conference();
|
||||
const auto livestream = call->peer()->isBroadcast();
|
||||
const auto scheduled = (call->scheduleDate() != 0);
|
||||
if (!scheduled) {
|
||||
box->setTitle(conference
|
||||
? tr::lng_group_call_leave_title_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_title_channel()
|
||||
: tr::lng_group_call_leave_title());
|
||||
}
|
||||
const auto inCall = (context == BoxContext::GroupCallPanel);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
(scheduled
|
||||
? (livestream
|
||||
? tr::lng_group_call_close_sure_channel()
|
||||
: tr::lng_group_call_close_sure())
|
||||
: (conference
|
||||
? tr::lng_group_call_leave_sure_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_sure_channel()
|
||||
: tr::lng_group_call_leave_sure())),
|
||||
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
|
||||
scheduled ? st::boxPadding : st::boxRowPadding);
|
||||
const auto discard = call->canManage()
|
||||
? box->addRow(object_ptr<Ui::Checkbox>(
|
||||
box.get(),
|
||||
(scheduled
|
||||
? (livestream
|
||||
? tr::lng_group_call_also_cancel_channel()
|
||||
: tr::lng_group_call_also_cancel())
|
||||
: (livestream
|
||||
? tr::lng_group_call_also_end_channel()
|
||||
: tr::lng_group_call_also_end())),
|
||||
discardChecked,
|
||||
(inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),
|
||||
(inCall ? st::groupCallCheck : st::defaultCheck)),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxRowPadding.bottom()))
|
||||
: nullptr;
|
||||
const auto weak = base::make_weak(call);
|
||||
auto label = scheduled
|
||||
? tr::lng_group_call_close()
|
||||
: tr::lng_group_call_leave();
|
||||
box->addButton(std::move(label), [=] {
|
||||
const auto discardCall = (discard && discard->checked());
|
||||
box->closeBox();
|
||||
|
||||
if (!weak) {
|
||||
return;
|
||||
} else if (discardCall) {
|
||||
call->discard();
|
||||
} else {
|
||||
call->hangup();
|
||||
}
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
object_ptr<Ui::GenericBox> ConfirmBox(Ui::ConfirmBoxArgs &&args) {
|
||||
auto copy = std::move(args);
|
||||
copy.labelStyle = &st::groupCallBoxLabel;
|
||||
return Ui::MakeConfirmBox(std::move(copy));
|
||||
}
|
||||
|
||||
void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
|
||||
const auto weak = base::make_weak(call);
|
||||
const auto resolveReal = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
if (const auto real = strong->lookupReal()) {
|
||||
return real;
|
||||
}
|
||||
}
|
||||
return (Data::GroupCall*)nullptr;
|
||||
};
|
||||
const auto real = resolveReal();
|
||||
if (!real) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto conference = call->conference();
|
||||
const auto addEditJoinAs = call->showChooseJoinAs();
|
||||
const auto addEditTitle = !conference && call->canManage();
|
||||
const auto addEditRecording = !conference
|
||||
&& call->canManage()
|
||||
&& !real->scheduleDate();
|
||||
const auto addScreenCast = call->videoIsWorking()
|
||||
&& !real->scheduleDate();
|
||||
if (addEditJoinAs) {
|
||||
menu->addAction(MakeJoinAsAction(
|
||||
menu->menu(),
|
||||
call->joinAs(),
|
||||
chooseJoinAs));
|
||||
menu->addSeparator();
|
||||
}
|
||||
if (addEditTitle) {
|
||||
const auto livestream = call->peer()->isBroadcast();
|
||||
const auto text = (livestream
|
||||
? tr::lng_group_call_edit_title_channel
|
||||
: tr::lng_group_call_edit_title)(tr::now);
|
||||
menu->addAction(text, [=] {
|
||||
const auto done = [=](const QString &title) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->changeTitle(title);
|
||||
}
|
||||
};
|
||||
if (const auto real = resolveReal()) {
|
||||
showBox(Box(
|
||||
EditGroupCallTitleBox,
|
||||
peer->name(),
|
||||
real->title(),
|
||||
livestream,
|
||||
done));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (addEditRecording) {
|
||||
const auto handler = [=] {
|
||||
const auto real = resolveReal();
|
||||
if (!real) {
|
||||
return;
|
||||
}
|
||||
const auto type = std::make_shared<RecordingType>();
|
||||
const auto recordStartDate = real->recordStartDate();
|
||||
const auto done = [=](QString title) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->toggleRecording(
|
||||
!recordStartDate,
|
||||
title,
|
||||
(*type) != RecordingType::AudioOnly,
|
||||
(*type) == RecordingType::VideoPortrait);
|
||||
}
|
||||
};
|
||||
if (recordStartDate) {
|
||||
showBox(Box(
|
||||
StopGroupCallRecordingBox,
|
||||
done));
|
||||
} else {
|
||||
const auto typeDone = [=](RecordingType newType) {
|
||||
*type = newType;
|
||||
showBox(Box(
|
||||
AddTitleGroupCallRecordingBox,
|
||||
real->title(),
|
||||
done));
|
||||
};
|
||||
showBox(Box(StartGroupCallRecordingBox, typeDone));
|
||||
}
|
||||
};
|
||||
menu->addAction(MakeRecordingAction(
|
||||
menu->menu(),
|
||||
real->recordStartDateValue(),
|
||||
handler));
|
||||
}
|
||||
if (addScreenCast) {
|
||||
const auto sharing = call->isSharingScreen();
|
||||
const auto toggle = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
if (sharing) {
|
||||
strong->toggleScreenSharing(std::nullopt);
|
||||
} else {
|
||||
chooseShareScreenSource();
|
||||
}
|
||||
}
|
||||
};
|
||||
menu->addAction(
|
||||
(call->isSharingScreen()
|
||||
? tr::lng_group_call_screen_share_stop(tr::now)
|
||||
: tr::lng_group_call_screen_share_start(tr::now)),
|
||||
toggle);
|
||||
}
|
||||
menu->addAction(tr::lng_group_call_settings(tr::now), [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
showBox(Box(SettingsBox, strong));
|
||||
}
|
||||
});
|
||||
const auto finish = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
showBox(Box(
|
||||
LeaveBox,
|
||||
strong,
|
||||
true,
|
||||
BoxContext::GroupCallPanel));
|
||||
}
|
||||
};
|
||||
const auto livestream = real->peer()->isBroadcast();
|
||||
menu->addAction(MakeAttentionAction(
|
||||
menu->menu(),
|
||||
(!call->canManage()
|
||||
? tr::lng_group_call_leave
|
||||
: real->scheduleDate()
|
||||
? (livestream
|
||||
? tr::lng_group_call_cancel_channel
|
||||
: tr::lng_group_call_cancel)
|
||||
: (livestream
|
||||
? tr::lng_group_call_end_channel
|
||||
: tr::lng_group_call_end))(tr::now),
|
||||
finish));
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
const QString &text,
|
||||
Fn<void()> callback) {
|
||||
return base::make_unique_q<Ui::Menu::Action>(
|
||||
menu,
|
||||
st::groupCallFinishMenu,
|
||||
Ui::Menu::CreateAction(
|
||||
menu,
|
||||
text,
|
||||
std::move(callback)),
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
63
Telegram/SourceFiles/calls/group/calls_group_menu.h
Normal file
63
Telegram/SourceFiles/calls/group/calls_group_menu.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class DropdownMenu;
|
||||
class GenericBox;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
class ItemBase;
|
||||
class Menu;
|
||||
} // namespace Ui::Menu
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class BoxContext {
|
||||
GroupCallPanel,
|
||||
MainWindow,
|
||||
};
|
||||
|
||||
void LeaveBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call,
|
||||
bool discardChecked,
|
||||
BoxContext context);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> ConfirmBox(
|
||||
Ui::ConfirmBoxArgs &&args);
|
||||
|
||||
void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox);
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
const QString &text,
|
||||
Fn<void()> callback);
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
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_message_encryption.h"
|
||||
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
//[[nodiscard]] MTPJSONValue String(const QByteArray &value) {
|
||||
// return MTP_jsonString(MTP_bytes(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Int(int value) {
|
||||
// return MTP_jsonNumber(MTP_double(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONObjectValue Value(
|
||||
// const QByteArray &name,
|
||||
// const MTPJSONValue &value) {
|
||||
// return MTP_jsonObjectValue(MTP_bytes(name), value);
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Object(
|
||||
// const QByteArray &cons,
|
||||
// QVector<MTPJSONObjectValue> &&values) {
|
||||
// values.insert(values.begin(), Value("_", String(cons)));
|
||||
// return MTP_jsonObject(MTP_vector<MTPJSONObjectValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Array(QVector<MTPJSONValue> &&values) {
|
||||
// return MTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//template <typename MTPD>
|
||||
//[[nodiscard]] MTPJSONValue SimpleEntity(
|
||||
// const QByteArray &name,
|
||||
// const MTPD &data) {
|
||||
// return Object(name, {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Entity(const MTPMessageEntity &entity) {
|
||||
// return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
// return SimpleEntity("messageEntityBold", data);
|
||||
// }, [](const MTPDmessageEntityItalic &data) {
|
||||
// return SimpleEntity("messageEntityItalic", data);
|
||||
// }, [](const MTPDmessageEntityUnderline &data) {
|
||||
// return SimpleEntity("messageEntityUnderline", data);
|
||||
// }, [](const MTPDmessageEntityStrike &data) {
|
||||
// return SimpleEntity("messageEntityStrike", data);
|
||||
// }, [](const MTPDmessageEntitySpoiler &data) {
|
||||
// return SimpleEntity("messageEntitySpoiler", data);
|
||||
// }, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
// return Object("messageEntityCustomEmoji", {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// Value(
|
||||
// "document_id",
|
||||
// String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
// });
|
||||
// }, [](const auto &data) {
|
||||
// return MTP_jsonNull();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QVector<MTPJSONValue> Entities(
|
||||
// const QVector<MTPMessageEntity> &list) {
|
||||
// auto result = QVector<MTPJSONValue>();
|
||||
// result.reserve(list.size());
|
||||
// for (const auto &entity : list) {
|
||||
// if (const auto e = Entity(entity); e.type() != mtpc_jsonNull) {
|
||||
// result.push_back(e);
|
||||
// }
|
||||
// }
|
||||
// return result;
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QByteArray Serialize(const MTPJSONValue &value) {
|
||||
// auto counter = ::tl::details::LengthCounter();
|
||||
// value.write(counter);
|
||||
// auto buffer = mtpBuffer();
|
||||
// buffer.reserve(counter.length);
|
||||
// value.write(buffer);
|
||||
// return QByteArray(
|
||||
// reinterpret_cast<const char*>(buffer.constData()),
|
||||
// buffer.size() * sizeof(buffer.front()));
|
||||
//}
|
||||
|
||||
[[nodiscard]] QJsonValue String(const QByteArray &value) {
|
||||
return QJsonValue(QString::fromUtf8(value));
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Int(int value) {
|
||||
return QJsonValue(double(value));
|
||||
}
|
||||
|
||||
struct JsonObjectValue {
|
||||
const char *name = nullptr;
|
||||
QJsonValue value;
|
||||
};
|
||||
|
||||
[[nodiscard]] JsonObjectValue Value(
|
||||
const char *name,
|
||||
const QJsonValue &value) {
|
||||
return JsonObjectValue{ name, value };
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Object(
|
||||
const char *cons,
|
||||
QVector<JsonObjectValue> &&values) {
|
||||
auto result = QJsonObject();
|
||||
result.insert("_", cons);
|
||||
for (const auto &value : values) {
|
||||
result.insert(value.name, value.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Array(QVector<QJsonValue> &&values) {
|
||||
auto result = QJsonArray();
|
||||
for (const auto &value : values) {
|
||||
result.push_back(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename MTPD>
|
||||
[[nodiscard]] QJsonValue SimpleEntity(
|
||||
const char *name,
|
||||
const MTPD &data) {
|
||||
return Object(name, {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Entity(const MTPMessageEntity &entity) {
|
||||
return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
return SimpleEntity("messageEntityBold", data);
|
||||
}, [](const MTPDmessageEntityItalic &data) {
|
||||
return SimpleEntity("messageEntityItalic", data);
|
||||
}, [](const MTPDmessageEntityUnderline &data) {
|
||||
return SimpleEntity("messageEntityUnderline", data);
|
||||
}, [](const MTPDmessageEntityStrike &data) {
|
||||
return SimpleEntity("messageEntityStrike", data);
|
||||
}, [](const MTPDmessageEntitySpoiler &data) {
|
||||
return SimpleEntity("messageEntitySpoiler", data);
|
||||
}, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
return Object("messageEntityCustomEmoji", {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
Value(
|
||||
"document_id",
|
||||
String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
});
|
||||
}, [](const auto &data) {
|
||||
return QJsonValue(QJsonValue::Null);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<QJsonValue> Entities(
|
||||
const QVector<MTPMessageEntity> &list) {
|
||||
auto result = QVector<QJsonValue>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entity : list) {
|
||||
if (const auto e = Entity(entity); !e.isNull()) {
|
||||
result.push_back(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray Serialize(const QJsonValue &value) {
|
||||
return QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<QJsonValue> GetValue(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto i = object.find(name);
|
||||
return (i != object.end()) ? *i : std::optional<QJsonValue>();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<int> GetInt(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
return int(base::SafeRound(maybeValue->toDouble()));
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toInt(&ok);
|
||||
return ok ? result : std::optional<int>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<uint64> GetLong(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
const auto value = maybeValue->toDouble();
|
||||
return (value >= 0.)
|
||||
? uint64(base::SafeRound(value))
|
||||
: std::optional<uint64>();
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toLongLong(&ok);
|
||||
return ok ? uint64(result) : std::optional<uint64>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetString(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto maybeValue = GetValue(object, name);
|
||||
return (maybeValue && maybeValue->isString())
|
||||
? maybeValue->toString()
|
||||
: std::optional<QString>();
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetCons(const QJsonObject &object) {
|
||||
return GetString(object, "_");
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Unsupported(
|
||||
const QJsonObject &object,
|
||||
const QString &cons = QString()) {
|
||||
const auto maybeMinLayer = GetInt(object, "_min_layer");
|
||||
const auto layer = int(MTP::details::kCurrentLayer);
|
||||
if (maybeMinLayer.value_or(layer) > layer) {
|
||||
LOG(("E2E Error: _min_layer too large: %1 > %2").arg(*maybeMinLayer).arg(layer));
|
||||
return true;
|
||||
} else if (!cons.isEmpty() && GetCons(object) != cons) {
|
||||
LOG(("E2E Error: Expected %1 here.").arg(cons));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MTPMessageEntity> GetEntity(
|
||||
const QString &text,
|
||||
const QJsonObject &object) {
|
||||
const auto cons = GetCons(object).value_or(QString());
|
||||
const auto offset = GetInt(object, "offset").value_or(-1);
|
||||
const auto length = GetInt(object, "length").value_or(0);
|
||||
if (Unsupported(object)
|
||||
|| (offset < 0)
|
||||
|| (length <= 0)
|
||||
|| (offset >= text.size())
|
||||
|| (length > text.size())
|
||||
|| (offset + length > text.size())) {
|
||||
return {};
|
||||
}
|
||||
const auto simple = [&](const auto &make) {
|
||||
return make(MTP_int(offset), MTP_int(length));
|
||||
};
|
||||
if (cons == "messageEntityBold") {
|
||||
return simple(MTP_messageEntityBold);
|
||||
} else if (cons == "messageEntityItalic") {
|
||||
return simple(MTP_messageEntityItalic);
|
||||
} else if (cons == "messageEntityUnderline") {
|
||||
return simple(MTP_messageEntityUnderline);
|
||||
} else if (cons == "messageEntityStrike") {
|
||||
return simple(MTP_messageEntityStrike);
|
||||
} else if (cons == "messageEntitySpoiler") {
|
||||
return simple(MTP_messageEntitySpoiler);
|
||||
} else if (cons == "messageEntityCustomEmoji") {
|
||||
const auto maybeDocumentId = GetLong(object, "document_id");
|
||||
if (const auto documentId = maybeDocumentId.value_or(0)) {
|
||||
return MTP_messageEntityCustomEmoji(
|
||||
MTP_int(offset),
|
||||
MTP_int(length),
|
||||
MTP_long(documentId));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<MTPMessageEntity> GetEntities(
|
||||
const QString &text,
|
||||
const QJsonArray &list) {
|
||||
auto result = QVector<MTPMessageEntity>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entry : list) {
|
||||
if (const auto entity = GetEntity(text, entry.toObject())) {
|
||||
result.push_back(*entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QByteArray SerializeMessage(const PreparedMessage &data) {
|
||||
return Serialize(Object("groupCallMessage", {
|
||||
Value(
|
||||
"random_id",
|
||||
String(QByteArray::number(int64(data.randomId)))),
|
||||
Value(
|
||||
"message",
|
||||
Object("textWithEntities", {
|
||||
Value("text", String(data.message.data().vtext().v)),
|
||||
Value(
|
||||
"entities",
|
||||
Array(Entities(data.message.data().ventities().v))),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data) {
|
||||
auto error = QJsonParseError();
|
||||
auto document = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError
|
||||
|| !document.isObject()) {
|
||||
LOG(("E2E Error: Bad json in Calls::Group::DeserializeMessage."));
|
||||
return {};
|
||||
}
|
||||
const auto groupCallMessage = document.object();
|
||||
if (Unsupported(groupCallMessage, "groupCallMessage")) {
|
||||
return {};
|
||||
}
|
||||
const auto randomId = GetLong(groupCallMessage, "random_id").value_or(0);
|
||||
if (!randomId) {
|
||||
return {};
|
||||
}
|
||||
const auto message = groupCallMessage["message"].toObject();
|
||||
if (Unsupported(message, "textWithEntities")) {
|
||||
return {};
|
||||
}
|
||||
const auto maybeText = GetString(message, "text");
|
||||
if (!maybeText) {
|
||||
return {};
|
||||
}
|
||||
const auto &text = *maybeText;
|
||||
const auto maybeEntities = GetValue(message, "entities");
|
||||
if (!maybeEntities || !maybeEntities->isArray()) {
|
||||
return {};
|
||||
}
|
||||
const auto entities = GetEntities(text, maybeEntities->toArray());
|
||||
return PreparedMessage{
|
||||
.randomId = randomId,
|
||||
.message = MTP_textWithEntities(
|
||||
MTP_string(text),
|
||||
MTP_vector<MTPMessageEntity>(entities)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct PreparedMessage {
|
||||
uint64 randomId = 0;
|
||||
MTPTextWithEntities message;
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray SerializeMessage(const PreparedMessage &data);
|
||||
[[nodiscard]] std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data);
|
||||
|
||||
} // namespace Calls::Group
|
||||
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
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_message_field.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "history/view/reactions/history_view_reactions_strip.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorLimit = 99;
|
||||
|
||||
using Chosen = HistoryView::Reactions::ChosenReaction;
|
||||
|
||||
} // namespace
|
||||
|
||||
class ReactionPanel final {
|
||||
public:
|
||||
ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry);
|
||||
~ReactionPanel();
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> chosen() const;
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void raise();
|
||||
void hideIfCollapsed();
|
||||
void collapse();
|
||||
|
||||
private:
|
||||
struct Hiding;
|
||||
|
||||
void create();
|
||||
void updateShowState();
|
||||
void fadeOutSelector();
|
||||
void startAnimation();
|
||||
|
||||
const not_null<QWidget*> _outer;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
std::unique_ptr<Ui::RpWidget> _parent;
|
||||
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
|
||||
std::vector<std::unique_ptr<Hiding>> _hiding;
|
||||
rpl::event_stream<Chosen> _chosen;
|
||||
Ui::Animations::Simple _showing;
|
||||
rpl::variable<float64> _shownValue;
|
||||
rpl::variable<QRect> _fieldGeometry;
|
||||
rpl::variable<bool> _expanded;
|
||||
rpl::variable<bool> _shown = false;
|
||||
|
||||
};
|
||||
|
||||
struct ReactionPanel::Hiding {
|
||||
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
|
||||
}
|
||||
|
||||
Ui::RpWidget widget;
|
||||
Ui::Animations::Simple animation;
|
||||
QImage frame;
|
||||
};
|
||||
|
||||
ReactionPanel::ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry)
|
||||
: _outer(outer)
|
||||
, _show(std::move(show))
|
||||
, _fieldGeometry(std::move(fieldGeometry)) {
|
||||
}
|
||||
|
||||
ReactionPanel::~ReactionPanel() = default;
|
||||
|
||||
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
|
||||
return _chosen.events();
|
||||
}
|
||||
|
||||
void ReactionPanel::show() {
|
||||
if (_shown.current()) {
|
||||
return;
|
||||
}
|
||||
create();
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
const auto duration = st::defaultPanelAnimation.heightDuration
|
||||
* st::defaultPopupMenu.showDuration;
|
||||
_shown = true;
|
||||
_showing.start([=] { updateShowState(); }, 0., 1., duration);
|
||||
updateShowState();
|
||||
_parent->show();
|
||||
}
|
||||
|
||||
void ReactionPanel::hide() {
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
_selector->beforeDestroy();
|
||||
if (!anim::Disabled()) {
|
||||
fadeOutSelector();
|
||||
}
|
||||
_shown = false;
|
||||
_expanded = false;
|
||||
_showing.stop();
|
||||
_selector = nullptr;
|
||||
_parent = nullptr;
|
||||
}
|
||||
|
||||
void ReactionPanel::raise() {
|
||||
if (_parent) {
|
||||
_parent->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::hideIfCollapsed() {
|
||||
if (!_expanded.current()) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::collapse() {
|
||||
if (_expanded.current()) {
|
||||
hide();
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::create() {
|
||||
auto reactions = Data::LookupPossibleReactions(&_show->session());
|
||||
if (reactions.recent.empty()) {
|
||||
return;
|
||||
}
|
||||
_parent = std::make_unique<Ui::RpWidget>(_outer);
|
||||
_parent->show();
|
||||
|
||||
_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(event->pos())) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _parent->lifetime());
|
||||
|
||||
_selector = std::make_unique<HistoryView::Reactions::Selector>(
|
||||
_parent.get(),
|
||||
st::storiesReactionsPan,
|
||||
_show,
|
||||
std::move(reactions),
|
||||
TextWithEntities(),
|
||||
[=](bool fast) { hide(); },
|
||||
nullptr, // iconFactory
|
||||
nullptr, // paused
|
||||
true);
|
||||
|
||||
_selector->chosen(
|
||||
) | rpl::on_next([=](Chosen reaction) {
|
||||
if (reaction.id.custom() && !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
_chosen.fire(std::move(reaction));
|
||||
hide();
|
||||
}
|
||||
}, _selector->lifetime());
|
||||
|
||||
const auto desiredWidth = st::storiesReactionsWidth;
|
||||
const auto maxWidth = desiredWidth * 2;
|
||||
const auto width = _selector->countWidth(desiredWidth, maxWidth);
|
||||
const auto margins = _selector->marginsForShadow();
|
||||
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
|
||||
width);
|
||||
const auto full = margins.left() + width + margins.right();
|
||||
|
||||
_shownValue = 0.;
|
||||
rpl::combine(
|
||||
_fieldGeometry.value(),
|
||||
_shownValue.value(),
|
||||
_expanded.value()
|
||||
) | rpl::on_next([=](QRect field, float64 shown, bool expanded) {
|
||||
const auto width = margins.left()
|
||||
+ _selector->countAppearedWidth(shown)
|
||||
+ margins.right();
|
||||
const auto available = field.y();
|
||||
const auto min = st::storiesReactionsBottomSkip
|
||||
+ st::reactStripHeight;
|
||||
const auto max = min
|
||||
+ margins.top()
|
||||
+ categoriesTop
|
||||
+ st::storiesReactionsAddedTop;
|
||||
const auto height = expanded ? std::min(available, max) : min;
|
||||
const auto top = field.y() - height;
|
||||
const auto shift = (width / 2);
|
||||
const auto right = (field.x() + field.width() / 2 + shift);
|
||||
_parent->setGeometry(QRect((right - width), top, full, height));
|
||||
const auto innerTop = height
|
||||
- st::storiesReactionsBottomSkip
|
||||
- st::reactStripHeight;
|
||||
const auto maxAdded = innerTop - margins.top() - categoriesTop;
|
||||
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
|
||||
_selector->setSpecialExpandTopSkip(added);
|
||||
_selector->initGeometry(innerTop);
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->willExpand(
|
||||
) | rpl::on_next([=] {
|
||||
_expanded = true;
|
||||
|
||||
const auto raw = _parent.get();
|
||||
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(
|
||||
_parent->mapFromGlobal(event->globalPos()))) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->escapes() | rpl::on_next([=] {
|
||||
collapse();
|
||||
}, _selector->lifetime());
|
||||
}
|
||||
|
||||
void ReactionPanel::fadeOutSelector() {
|
||||
const auto geometry = Ui::MapFrom(
|
||||
_outer,
|
||||
_parent.get(),
|
||||
_selector->geometry());
|
||||
_hiding.push_back(std::make_unique<Hiding>(_outer));
|
||||
const auto raw = _hiding.back().get();
|
||||
raw->frame = Ui::GrabWidgetToImage(_selector.get());
|
||||
raw->widget.setGeometry(geometry);
|
||||
raw->widget.show();
|
||||
raw->widget.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
if (const auto opacity = raw->animation.value(0.)) {
|
||||
auto p = QPainter(&raw->widget);
|
||||
p.setOpacity(opacity);
|
||||
p.drawImage(0, 0, raw->frame);
|
||||
}
|
||||
}, raw->widget.lifetime());
|
||||
Ui::PostponeCall(&raw->widget, [=] {
|
||||
raw->animation.start([=] {
|
||||
if (raw->animation.animating()) {
|
||||
raw->widget.update();
|
||||
} else {
|
||||
const auto i = ranges::find(
|
||||
_hiding,
|
||||
raw,
|
||||
&std::unique_ptr<Hiding>::get);
|
||||
if (i != end(_hiding)) {
|
||||
_hiding.erase(i);
|
||||
}
|
||||
}
|
||||
}, 1., 0., st::slideWrapDuration);
|
||||
});
|
||||
}
|
||||
|
||||
void ReactionPanel::updateShowState() {
|
||||
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
|
||||
const auto opacity = 1.;
|
||||
const auto appearing = _showing.animating();
|
||||
const auto toggling = false;
|
||||
_shownValue = progress;
|
||||
_selector->updateShowState(progress, opacity, appearing, toggling);
|
||||
}
|
||||
|
||||
MessageField::MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer)
|
||||
: _parent(parent)
|
||||
, _show(std::move(show))
|
||||
, _wrap(std::make_unique<Ui::RpWidget>(_parent))
|
||||
, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {
|
||||
createControls(peer);
|
||||
}
|
||||
|
||||
MessageField::~MessageField() = default;
|
||||
|
||||
void MessageField::createControls(PeerData *peer) {
|
||||
setupBackground();
|
||||
|
||||
const auto &st = st::storiesComposeControls;
|
||||
_field = Ui::CreateChild<Ui::InputField>(
|
||||
_wrap.get(),
|
||||
st.field,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_message_ph());
|
||||
_field->setMaxLength(_limit + kErrorLimit);
|
||||
_field->setMinHeight(
|
||||
st::historySendSize.height() - 2 * st::historySendPadding);
|
||||
_field->setMaxHeight(st::historyComposeFieldMaxHeight);
|
||||
_field->setDocumentMargin(4.);
|
||||
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
|
||||
|
||||
_reactionPanel = std::make_unique<ReactionPanel>(
|
||||
_parent,
|
||||
_show,
|
||||
_wrap->geometryValue());
|
||||
_fieldFocused = _field->focusedChanges();
|
||||
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
|
||||
return field->getLastText().trimmed().isEmpty();
|
||||
});
|
||||
rpl::combine(
|
||||
_fieldFocused.value(),
|
||||
_fieldEmpty.value()
|
||||
) | rpl::on_next([=](bool focused, bool empty) {
|
||||
if (!focused) {
|
||||
_reactionPanel->hideIfCollapsed();
|
||||
} else if (empty) {
|
||||
_reactionPanel->show();
|
||||
} else {
|
||||
_reactionPanel->hide();
|
||||
}
|
||||
}, _field->lifetime());
|
||||
|
||||
_reactionPanel->chosen(
|
||||
) | rpl::on_next([=](Chosen reaction) {
|
||||
if (const auto customId = reaction.id.custom()) {
|
||||
const auto document = _show->session().data().document(customId);
|
||||
if (const auto sticker = document->sticker()) {
|
||||
if (const auto alt = sticker->alt; !alt.isEmpty()) {
|
||||
const auto length = int(alt.size());
|
||||
const auto data = Data::SerializeCustomEmojiId(customId);
|
||||
const auto tag = Ui::InputField::CustomEmojiLink(data);
|
||||
_submitted.fire({ alt, { { 0, length, tag } } });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_submitted.fire({ reaction.id.emoji() });
|
||||
}
|
||||
_reactionPanel->hide();
|
||||
}, _field->lifetime());
|
||||
|
||||
const auto show = _show;
|
||||
const auto allow = [=](not_null<DocumentData*> emoji) {
|
||||
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
InitMessageFieldHandlers({
|
||||
.session = &show->session(),
|
||||
.show = show,
|
||||
.field = _field,
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(ChatHelpers::PauseReason::Layer);
|
||||
},
|
||||
.allowPremiumEmoji = allow,
|
||||
.fieldStyle = &st.files.caption,
|
||||
.allowMarkdownTags = {
|
||||
Ui::InputField::kTagBold,
|
||||
Ui::InputField::kTagItalic,
|
||||
Ui::InputField::kTagUnderline,
|
||||
Ui::InputField::kTagStrikeOut,
|
||||
Ui::InputField::kTagSpoiler,
|
||||
},
|
||||
});
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
_parent,
|
||||
_field,
|
||||
&_show->session(),
|
||||
{
|
||||
.suggestCustomEmoji = true,
|
||||
.allowCustomWithoutPremium = allow,
|
||||
.st = &st.suggestions,
|
||||
});
|
||||
|
||||
_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);
|
||||
_send->show();
|
||||
|
||||
using Selector = ChatHelpers::TabbedSelector;
|
||||
_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(
|
||||
_parent,
|
||||
ChatHelpers::TabbedPanelDescriptor{
|
||||
.ownedSelector = object_ptr<Selector>(
|
||||
nullptr,
|
||||
ChatHelpers::TabbedSelectorDescriptor{
|
||||
.show = _show,
|
||||
.st = st.tabbed,
|
||||
.level = ChatHelpers::PauseReason::Layer,
|
||||
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
|
||||
.features = {
|
||||
.stickersSettings = false,
|
||||
.openStickerSets = false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const auto panel = _emojiPanel.get();
|
||||
panel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
panel->hide();
|
||||
panel->selector()->setCurrentPeer(peer);
|
||||
panel->selector()->emojiChosen(
|
||||
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
|
||||
Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
|
||||
}, lifetime());
|
||||
panel->selector()->customEmojiChosen(
|
||||
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
||||
const auto info = data.document->sticker();
|
||||
if (info
|
||||
&& info->setType == Data::StickersType::Emoji
|
||||
&& !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
Data::InsertCustomEmoji(_field, data.document);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);
|
||||
_emojiToggle->show();
|
||||
|
||||
_emojiToggle->installEventFilter(panel);
|
||||
_emojiToggle->addClickHandler([=] {
|
||||
panel->toggleAnimated();
|
||||
});
|
||||
|
||||
_width.value(
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_1 > 0
|
||||
) | rpl::on_next([=](int newWidth) {
|
||||
const auto fieldWidth = newWidth
|
||||
- st::historySendPadding
|
||||
- _emojiToggle->width()
|
||||
- _send->width();
|
||||
_field->resizeToWidth(fieldWidth);
|
||||
_field->moveToLeft(
|
||||
st::historySendPadding,
|
||||
st::historySendPadding,
|
||||
newWidth);
|
||||
updateWrapSize(newWidth);
|
||||
}, _lifetime);
|
||||
|
||||
rpl::combine(
|
||||
_width.value(),
|
||||
_field->heightValue()
|
||||
) | rpl::on_next([=](int width, int height) {
|
||||
if (width <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto minHeight = st::historySendSize.height()
|
||||
- 2 * st::historySendPadding;
|
||||
_send->moveToRight(0, height - minHeight, width);
|
||||
_emojiToggle->moveToRight(_send->width(), height - minHeight, width);
|
||||
updateWrapSize();
|
||||
}, _lifetime);
|
||||
|
||||
_field->cancelled() | rpl::on_next([=] {
|
||||
_closeRequests.fire({});
|
||||
}, _lifetime);
|
||||
|
||||
const auto updateLimitPosition = [=](QSize parent, QSize label) {
|
||||
const auto skip = st::historySendPadding;
|
||||
return QPoint(parent.width() - label.width() - skip, skip);
|
||||
};
|
||||
Ui::AddLengthLimitLabel(_field, _limit, {
|
||||
.customParent = _wrap.get(),
|
||||
.customUpdatePosition = updateLimitPosition,
|
||||
});
|
||||
|
||||
rpl::merge(
|
||||
_field->submits() | rpl::to_empty,
|
||||
_send->clicks() | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
auto text = _field->getTextWithTags();
|
||||
if (text.text.size() <= _limit) {
|
||||
_submitted.fire(std::move(text));
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::updateEmojiPanelGeometry() {
|
||||
const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
|
||||
const auto local = _parent->mapFromGlobal(global);
|
||||
_emojiPanel->moveBottomRight(
|
||||
local.y(),
|
||||
local.x() + _emojiToggle->width() * 3);
|
||||
}
|
||||
|
||||
void MessageField::setupBackground() {
|
||||
_wrap->paintRequest() | rpl::on_next([=] {
|
||||
const auto radius = st::historySendSize.height() / 2.;
|
||||
auto p = QPainter(_wrap.get());
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::storiesComposeBg);
|
||||
p.drawRoundedRect(_wrap->rect(), radius, radius);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::resizeToWidth(int newWidth) {
|
||||
_width = newWidth;
|
||||
if (_wrap->isHidden()) {
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
}
|
||||
updateEmojiPanelGeometry();
|
||||
}
|
||||
|
||||
void MessageField::move(int x, int y) {
|
||||
_wrap->move(x, y);
|
||||
if (_cache) {
|
||||
_cache->move(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::toggle(bool shown) {
|
||||
if (_shown == shown) {
|
||||
return;
|
||||
} else if (shown) {
|
||||
Assert(_width.current() > 0);
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
} else if (Ui::InFocusChain(_field)) {
|
||||
_parent->setFocus();
|
||||
}
|
||||
_shown = shown;
|
||||
if (!anim::Disabled()) {
|
||||
if (!_cache) {
|
||||
auto image = Ui::GrabWidgetToImage(_wrap.get());
|
||||
_cache = std::make_unique<Ui::RpWidget>(_parent);
|
||||
const auto raw = _cache.get();
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto scale = raw->height() / float64(_wrap->height());
|
||||
const auto target = _wrap->rect();
|
||||
const auto center = target.center();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
p.drawImage(target, image);
|
||||
}, raw->lifetime());
|
||||
raw->show();
|
||||
raw->move(_wrap->pos());
|
||||
raw->resize(_wrap->width(), 0);
|
||||
|
||||
_wrap->hide();
|
||||
}
|
||||
_shownAnimation.start(
|
||||
[=] { shownAnimationCallback(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration,
|
||||
anim::easeOutCirc);
|
||||
}
|
||||
shownAnimationCallback();
|
||||
}
|
||||
|
||||
void MessageField::raise() {
|
||||
_wrap->raise();
|
||||
if (_cache) {
|
||||
_cache->raise();
|
||||
}
|
||||
if (_reactionPanel) {
|
||||
_reactionPanel->raise();
|
||||
}
|
||||
if (_emojiPanel) {
|
||||
_emojiPanel->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::updateWrapSize(int widthOverride) {
|
||||
const auto width = widthOverride ? widthOverride : _wrap->width();
|
||||
const auto height = _field->height() + 2 * st::historySendPadding;
|
||||
_wrap->resize(width, height);
|
||||
updateHeight();
|
||||
}
|
||||
|
||||
void MessageField::updateHeight() {
|
||||
_height = int(base::SafeRound(
|
||||
_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));
|
||||
}
|
||||
|
||||
void MessageField::shownAnimationCallback() {
|
||||
updateHeight();
|
||||
if (_shownAnimation.animating()) {
|
||||
Assert(_cache != nullptr);
|
||||
_cache->resize(_cache->width(), _height.current());
|
||||
_cache->update();
|
||||
} else if (_shown) {
|
||||
_cache = nullptr;
|
||||
_wrap->show();
|
||||
_field->setFocusFast();
|
||||
} else {
|
||||
_closed.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
int MessageField::height() const {
|
||||
return _height.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> MessageField::heightValue() const {
|
||||
return _height.value();
|
||||
}
|
||||
|
||||
rpl::producer<TextWithTags> MessageField::submitted() const {
|
||||
return _submitted.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closeRequests() const {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closed() const {
|
||||
return _closed.events();
|
||||
}
|
||||
|
||||
rpl::lifetime &MessageField::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class ReactionPanel;
|
||||
|
||||
class MessageField final {
|
||||
public:
|
||||
MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer);
|
||||
~MessageField();
|
||||
|
||||
void resizeToWidth(int newWidth);
|
||||
void move(int x, int y);
|
||||
void toggle(bool shown);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<TextWithTags> submitted() const;
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> closed() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void createControls(PeerData *peer);
|
||||
void setupBackground();
|
||||
void shownAnimationCallback();
|
||||
void updateEmojiPanelGeometry();
|
||||
void updateWrapSize(int widthOverride = 0);
|
||||
void updateHeight();
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const std::unique_ptr<Ui::RpWidget> _wrap;
|
||||
|
||||
int _limit = 0;
|
||||
Ui::InputField *_field = nullptr;
|
||||
Ui::SendButton *_send = nullptr;
|
||||
Ui::EmojiButton *_emojiToggle = nullptr;
|
||||
std::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
std::unique_ptr<ReactionPanel> _reactionPanel;
|
||||
rpl::variable<bool> _fieldFocused;
|
||||
rpl::variable<bool> _fieldEmpty = true;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
rpl::variable<int> _height;
|
||||
|
||||
bool _shown = false;
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
std::unique_ptr<Ui::RpWidget> _cache;
|
||||
|
||||
rpl::event_stream<TextWithTags> _submitted;
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::event_stream<> _closed;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
731
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
731
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
@@ -0,0 +1,731 @@
|
||||
/*
|
||||
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_messages.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_blocked_peers.h"
|
||||
#include "api/api_chat_participants.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "calls/group/ui/calls_group_stars_coloring.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_message_encryption.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxShownVideoStreamMessages = 100;
|
||||
constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] StarsTop ParseStarsTop(
|
||||
not_null<Data::Session*> owner,
|
||||
const MTPphone_GroupCallStars &stars) {
|
||||
const auto &data = stars.data();
|
||||
const auto &list = data.vtop_donors().v;
|
||||
auto result = StarsTop{ .total = int(data.vtotal_stars().v) };
|
||||
result.topDonors.reserve(list.size());
|
||||
for (const auto &entry : list) {
|
||||
const auto &fields = entry.data();
|
||||
result.topDonors.push_back({
|
||||
.peer = (fields.vpeer_id()
|
||||
? owner->peer(peerFromMTP(*fields.vpeer_id())).get()
|
||||
: nullptr),
|
||||
.stars = int(fields.vstars().v),
|
||||
.my = fields.is_my(),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId PinFinishDate(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId date,
|
||||
int stars) {
|
||||
if (!date || !stars) {
|
||||
return 0;
|
||||
}
|
||||
const auto &colorings = peer->session().appConfig().groupCallColorings();
|
||||
return date + Ui::StarsColoringForCount(colorings, stars).secondsPin;
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId PinFinishDate(const Message &message) {
|
||||
return PinFinishDate(message.peer, message.date, message.stars);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
|
||||
: _call(call)
|
||||
, _session(&call->peer()->session())
|
||||
, _api(api)
|
||||
, _destroyTimer([=] { checkDestroying(); })
|
||||
, _ttl(_session->appConfig().groupCallMessageTTL())
|
||||
, _starsStatsTimer([=] { requestStarsStats(); }) {
|
||||
Ui::PostponeCall(_call, [=] {
|
||||
_call->real(
|
||||
) | rpl::on_next([=](not_null<Data::GroupCall*> call) {
|
||||
_real = call;
|
||||
if (ready()) {
|
||||
sendPending();
|
||||
} else {
|
||||
Unexpected("Not ready call.");
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
requestStarsStats();
|
||||
});
|
||||
}
|
||||
|
||||
Messages::~Messages() {
|
||||
if (_paid.sending > 0) {
|
||||
finishPaidSending({
|
||||
.count = int(_paid.sending),
|
||||
.valid = true,
|
||||
.shownPeer = _paid.sendingShownPeer,
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::requestStarsStats() {
|
||||
if (!_call->videoStream()) {
|
||||
return;
|
||||
}
|
||||
_starsStatsTimer.cancel();
|
||||
_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(
|
||||
_call->inputCall()
|
||||
)).done([=](const MTPphone_GroupCallStars &result) {
|
||||
const auto &data = result.data();
|
||||
|
||||
const auto owner = &_session->data();
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
|
||||
_paid.top = ParseStarsTop(owner, result);
|
||||
_paidChanges.fire({});
|
||||
|
||||
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
[[maybe_unused]] const auto &type = error.type();
|
||||
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
|
||||
}).send();
|
||||
|
||||
}
|
||||
|
||||
bool Messages::ready() const {
|
||||
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
|
||||
}
|
||||
|
||||
void Messages::send(TextWithTags text, int stars) {
|
||||
if (text.empty() && !stars) {
|
||||
return;
|
||||
} else if (!ready()) {
|
||||
_pending.push_back({ std::move(text), stars });
|
||||
return;
|
||||
}
|
||||
|
||||
auto prepared = TextWithEntities{
|
||||
text.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(text.tags)
|
||||
};
|
||||
auto serialized = MTPTextWithEntities(MTP_textWithEntities(
|
||||
MTP_string(prepared.text),
|
||||
Api::EntitiesToMTP(
|
||||
&_real->session(),
|
||||
prepared.entities,
|
||||
Api::ConvertOption::SkipLocal)));
|
||||
|
||||
const auto localId = _call->peer()->owner().nextLocalMessageId();
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
_sendingIdByRandomId.emplace(randomId, localId);
|
||||
|
||||
const auto from = _call->messagesFrom();
|
||||
const auto creator = _real->creator();
|
||||
const auto skip = skipMessage(prepared, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(localId);
|
||||
} else {
|
||||
_messages.push_back({
|
||||
.id = localId,
|
||||
.peer = from,
|
||||
.text = std::move(prepared),
|
||||
.stars = stars,
|
||||
.admin = (from == _call->peer()) || (creator && from->isSelf()),
|
||||
.mine = true,
|
||||
});
|
||||
}
|
||||
if (!_call->conference()) {
|
||||
using Flag = MTPphone_SendGroupCallMessage::Flag;
|
||||
_api->request(MTPphone_SendGroupCallMessage(
|
||||
MTP_flags(Flag::f_send_as
|
||||
| (stars ? Flag::f_allow_paid_stars : Flag())),
|
||||
_call->inputCall(),
|
||||
MTP_long(randomId),
|
||||
serialized,
|
||||
MTP_long(stars),
|
||||
from->input()
|
||||
)).done([=](
|
||||
const MTPUpdates &result,
|
||||
const MTP::Response &response) {
|
||||
_session->api().applyUpdates(result, randomId);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
} else {
|
||||
const auto bytes = SerializeMessage({ randomId, serialized });
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(from->id).bare;
|
||||
const auto encrypt = _call->e2eEncryptDecrypt();
|
||||
const auto encrypted = encrypt(v, int64_t(userId), true, 0);
|
||||
|
||||
_api->request(MTPphone_SendGroupCallEncryptedMessage(
|
||||
_call->inputCall(),
|
||||
MTP_bytes(bytes::make_span(encrypted))
|
||||
)).done([=](const MTPBool &, const MTP::Response &response) {
|
||||
sent(randomId, response);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
}
|
||||
|
||||
addStars(from, stars, true);
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::setApplyingInitial(bool value) {
|
||||
_applyingInitial = value;
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
const auto &fields = data.vmessage().data();
|
||||
received(
|
||||
fields.vid().v,
|
||||
fields.vfrom_id(),
|
||||
fields.vmessage(),
|
||||
fields.vdate().v,
|
||||
fields.vpaid_message_stars().value_or_empty(),
|
||||
fields.is_from_admin());
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
const auto fromId = data.vfrom_id();
|
||||
const auto &bytes = data.vencrypted_message().v;
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(peerFromMTP(fromId)).bare;
|
||||
const auto decrypt = _call->e2eEncryptDecrypt();
|
||||
const auto decrypted = decrypt(v, int64_t(userId), false, 0);
|
||||
|
||||
const auto deserialized = DeserializeMessage(QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(decrypted.data()),
|
||||
decrypted.size()));
|
||||
if (!deserialized) {
|
||||
LOG(("API Error: Can't parse decrypted message"));
|
||||
return;
|
||||
}
|
||||
const auto realId = ++_conferenceIdAutoIncrement;
|
||||
const auto randomId = deserialized->randomId;
|
||||
if (!_conferenceIdByRandomId.emplace(randomId, realId).second) {
|
||||
// Already received.
|
||||
return;
|
||||
}
|
||||
received(
|
||||
realId,
|
||||
fromId,
|
||||
deserialized->message,
|
||||
base::unixtime::now(), // date
|
||||
0, // stars
|
||||
false,
|
||||
true); // checkCustomEmoji
|
||||
}
|
||||
|
||||
void Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {
|
||||
const auto was = _messages.size();
|
||||
for (const auto &id : data.vmessages().v) {
|
||||
const auto i = ranges::find(_messages, id.v, &Message::id);
|
||||
if (i != end(_messages)) {
|
||||
_messages.erase(i);
|
||||
}
|
||||
}
|
||||
if (_messages.size() < was) {
|
||||
pushChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::sent(const MTPDupdateMessageID &data) {
|
||||
sent(data.vrandom_id().v, data.vid().v);
|
||||
}
|
||||
|
||||
void Messages::sent(uint64 randomId, const MTP::Response &response) {
|
||||
const auto realId = ++_conferenceIdAutoIncrement;
|
||||
_conferenceIdByRandomId.emplace(randomId, realId);
|
||||
sent(randomId, realId);
|
||||
|
||||
const auto i = ranges::find(_messages, realId, &Message::id);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::sent(uint64 randomId, MsgId realId) {
|
||||
const auto i = _sendingIdByRandomId.find(randomId);
|
||||
if (i == end(_sendingIdByRandomId)) {
|
||||
return;
|
||||
}
|
||||
const auto localId = i->second;
|
||||
_sendingIdByRandomId.erase(i);
|
||||
|
||||
const auto j = ranges::find(_messages, localId, &Message::id);
|
||||
if (j == end(_messages)) {
|
||||
_skippedIds.emplace(realId);
|
||||
return;
|
||||
}
|
||||
j->id = realId;
|
||||
crl::on_main(this, [=] {
|
||||
const auto i = ranges::find(_messages, realId, &Message::id);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = base::unixtime::now();
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
});
|
||||
_idUpdates.fire({ .localId = localId, .realId = realId });
|
||||
}
|
||||
|
||||
void Messages::received(
|
||||
MsgId id,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji) {
|
||||
const auto peer = _call->peer();
|
||||
const auto i = ranges::find(_messages, id, &Message::id);
|
||||
if (i != end(_messages)) {
|
||||
const auto fromId = peerFromMTP(from);
|
||||
const auto me1 = peer->session().userPeerId();
|
||||
const auto me2 = _call->messagesFrom()->id;
|
||||
if (((fromId == me1) || (fromId == me2)) && !i->date) {
|
||||
i->date = date;
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
return;
|
||||
} else if (_skippedIds.contains(id)) {
|
||||
return;
|
||||
}
|
||||
auto allowedEntityTypes = std::vector<EntityType>{
|
||||
EntityType::Code,
|
||||
EntityType::Bold,
|
||||
EntityType::Semibold,
|
||||
EntityType::Spoiler,
|
||||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::CustomEmoji,
|
||||
};
|
||||
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
|
||||
allowedEntityTypes.pop_back();
|
||||
}
|
||||
const auto author = peer->owner().peer(peerFromMTP(from));
|
||||
|
||||
auto text = Ui::Text::Filtered(
|
||||
Api::ParseTextWithEntities(&author->session(), message),
|
||||
allowedEntityTypes);
|
||||
const auto mine = author->isSelf()
|
||||
|| (author->isChannel() && author->asChannel()->amCreator());
|
||||
const auto skip = skipMessage(text, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(id);
|
||||
} else {
|
||||
// Should check by sendAsPeers() list instead, but it may not be
|
||||
// loaded here yet.
|
||||
_messages.push_back({
|
||||
.id = id,
|
||||
.date = date,
|
||||
.pinFinishDate = PinFinishDate(author, date, stars),
|
||||
.peer = author,
|
||||
.text = std::move(text),
|
||||
.stars = stars,
|
||||
.admin = fromAdmin,
|
||||
.mine = mine,
|
||||
});
|
||||
ranges::sort(_messages, ranges::less(), &Message::id);
|
||||
}
|
||||
if (!_applyingInitial) {
|
||||
addStars(author, stars, mine);
|
||||
}
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool Messages::skipMessage(const TextWithEntities &text, int stars) const {
|
||||
const auto real = _call->lookupReal();
|
||||
return text.empty()
|
||||
&& real
|
||||
&& (stars < real->messagesMinPrice());
|
||||
}
|
||||
|
||||
void Messages::checkDestroying(bool afterChanges) {
|
||||
auto next = TimeId();
|
||||
const auto now = base::unixtime::now();
|
||||
const auto initial = int(_messages.size());
|
||||
if (_call->videoStream()) {
|
||||
if (initial > kMaxShownVideoStreamMessages) {
|
||||
const auto remove = initial - kMaxShownVideoStreamMessages;
|
||||
auto i = begin(_messages);
|
||||
for (auto k = 0; k != remove; ++k) {
|
||||
if (i->date && i->pinFinishDate <= now) {
|
||||
i = _messages.erase(i);
|
||||
} else if (!next || next > i->pinFinishDate - now) {
|
||||
next = i->pinFinishDate - now;
|
||||
++i;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else for (auto i = begin(_messages); i != end(_messages);) {
|
||||
const auto date = i->date;
|
||||
//const auto ttl = i->stars
|
||||
// ? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)
|
||||
// : _ttl;
|
||||
const auto ttl = _ttl;
|
||||
if (!date) {
|
||||
if (i->id < 0) {
|
||||
++i;
|
||||
} else {
|
||||
i = _messages.erase(i);
|
||||
}
|
||||
} else if (date + ttl <= now) {
|
||||
i = _messages.erase(i);
|
||||
} else if (!next || next > date + ttl - now) {
|
||||
next = date + ttl - now;
|
||||
++i;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
_destroyTimer.cancel();
|
||||
} else {
|
||||
const auto delay = next * crl::time(1000);
|
||||
if (!_destroyTimer.isActive()
|
||||
|| (_destroyTimer.remainingTime() > delay)) {
|
||||
_destroyTimer.callOnce(delay);
|
||||
}
|
||||
}
|
||||
if (afterChanges || (_messages.size() < initial)) {
|
||||
pushChanges();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<Message>> Messages::listValue() const {
|
||||
return _changes.events_starting_with_copy(_messages);
|
||||
}
|
||||
|
||||
rpl::producer<MessageIdUpdate> Messages::idUpdates() const {
|
||||
return _idUpdates.events();
|
||||
}
|
||||
|
||||
void Messages::sendPending() {
|
||||
Expects(_real != nullptr);
|
||||
|
||||
for (auto &pending : base::take(_pending)) {
|
||||
send(std::move(pending.text), pending.stars);
|
||||
}
|
||||
if (_paidSendingPending) {
|
||||
reactionsPaidSend();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::pushChanges() {
|
||||
if (_changesScheduled) {
|
||||
return;
|
||||
}
|
||||
_changesScheduled = true;
|
||||
Ui::PostponeCall(this, [=] {
|
||||
_changesScheduled = false;
|
||||
_changes.fire_copy(_messages);
|
||||
});
|
||||
}
|
||||
|
||||
void Messages::failed(uint64 randomId, const MTP::Response &response) {
|
||||
const auto i = _sendingIdByRandomId.find(randomId);
|
||||
if (i == end(_sendingIdByRandomId)) {
|
||||
return;
|
||||
}
|
||||
const auto localId = i->second;
|
||||
_sendingIdByRandomId.erase(i);
|
||||
|
||||
const auto j = ranges::find(_messages, localId, &Message::id);
|
||||
if (j != end(_messages) && !j->date) {
|
||||
j->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
j->stars = 0;
|
||||
j->failed = true;
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
int Messages::reactionsPaidScheduled() const {
|
||||
return _paid.scheduled;
|
||||
}
|
||||
|
||||
PeerId Messages::reactionsLocalShownPeer() const {
|
||||
const auto minePaidShownPeer = [&] {
|
||||
for (const auto &entry : _paid.top.topDonors) {
|
||||
if (entry.my) {
|
||||
return entry.peer ? entry.peer->id : PeerId();
|
||||
}
|
||||
}
|
||||
return _call->messagesFrom()->id;
|
||||
//const auto api = &_session->api();
|
||||
//return api->globalPrivacy().paidReactionShownPeerCurrent();
|
||||
};
|
||||
return _paid.scheduledFlag
|
||||
? _paid.scheduledShownPeer
|
||||
: _paid.sendingFlag
|
||||
? _paid.sendingShownPeer
|
||||
: minePaidShownPeer();
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidAdd(int count) {
|
||||
Expects(count >= 0);
|
||||
|
||||
_paid.scheduled += count;
|
||||
_paid.scheduledFlag = 1;
|
||||
_paid.scheduledShownPeer = _call->messagesFrom()->id;
|
||||
if (count > 0) {
|
||||
_session->credits().lock(CreditsAmount(count));
|
||||
}
|
||||
_call->peer()->owner().reactions().schedulePaid(_call);
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidScheduledCancel() {
|
||||
if (!_paid.scheduledFlag) {
|
||||
return;
|
||||
}
|
||||
if (const auto amount = int(_paid.scheduled)) {
|
||||
_session->credits().unlock(
|
||||
CreditsAmount(amount));
|
||||
}
|
||||
_paid.scheduled = 0;
|
||||
_paid.scheduledFlag = 0;
|
||||
_paid.scheduledShownPeer = 0;
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
|
||||
Data::PaidReactionSend Messages::startPaidReactionSending() {
|
||||
_paidSendingPending = false;
|
||||
if (!_paid.scheduledFlag || !_paid.scheduled) {
|
||||
return {};
|
||||
} else if (_paid.sendingFlag || !ready()) {
|
||||
_paidSendingPending = true;
|
||||
return {};
|
||||
}
|
||||
_paid.sending = _paid.scheduled;
|
||||
_paid.sendingFlag = _paid.scheduledFlag;
|
||||
_paid.sendingShownPeer = _paid.scheduledShownPeer;
|
||||
_paid.scheduled = 0;
|
||||
_paid.scheduledFlag = 0;
|
||||
_paid.scheduledShownPeer = 0;
|
||||
return {
|
||||
.count = int(_paid.sending),
|
||||
.valid = true,
|
||||
.shownPeer = _paid.sendingShownPeer,
|
||||
};
|
||||
}
|
||||
|
||||
void Messages::finishPaidSending(
|
||||
Data::PaidReactionSend send,
|
||||
bool success) {
|
||||
Expects(send.count == _paid.sending);
|
||||
Expects(send.valid == (_paid.sendingFlag == 1));
|
||||
Expects(send.shownPeer == _paid.sendingShownPeer);
|
||||
|
||||
_paid.sending = 0;
|
||||
_paid.sendingFlag = 0;
|
||||
_paid.sendingShownPeer = 0;
|
||||
if (const auto amount = send.count) {
|
||||
if (success) {
|
||||
const auto from = _session->data().peer(*send.shownPeer);
|
||||
_session->credits().withdrawLocked(CreditsAmount(amount));
|
||||
|
||||
auto &donors = _paid.top.topDonors;
|
||||
const auto i = ranges::find(donors, true, &StarsDonor::my);
|
||||
if (i != end(donors)) {
|
||||
i->peer = from;
|
||||
i->stars += amount;
|
||||
} else {
|
||||
donors.push_back({
|
||||
.peer = from,
|
||||
.stars = amount,
|
||||
.my = true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_session->credits().unlock(CreditsAmount(amount));
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
}
|
||||
if (_paidSendingPending) {
|
||||
reactionsPaidSend();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidSend() {
|
||||
const auto send = startPaidReactionSending();
|
||||
if (!send.valid || !send.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto localId = _call->peer()->owner().nextLocalMessageId();
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
_sendingIdByRandomId.emplace(randomId, localId);
|
||||
|
||||
const auto from = _session->data().peer(*send.shownPeer);
|
||||
const auto stars = int(send.count);
|
||||
const auto skip = skipMessage({}, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(localId);
|
||||
} else {
|
||||
_messages.push_back({
|
||||
.id = localId,
|
||||
.peer = from,
|
||||
.stars = stars,
|
||||
.admin = (from == _call->peer()),
|
||||
.mine = true,
|
||||
});
|
||||
}
|
||||
using Flag = MTPphone_SendGroupCallMessage::Flag;
|
||||
_api->request(MTPphone_SendGroupCallMessage(
|
||||
MTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),
|
||||
_call->inputCall(),
|
||||
MTP_long(randomId),
|
||||
MTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),
|
||||
MTP_long(stars),
|
||||
from->input()
|
||||
)).done([=](
|
||||
const MTPUpdates &result,
|
||||
const MTP::Response &response) {
|
||||
finishPaidSending(send, true);
|
||||
_session->api().applyUpdates(result, randomId);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
finishPaidSending(send, false);
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
|
||||
addStars(from, stars, true);
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::undoScheduledPaidOnDestroy() {
|
||||
_call->peer()->owner().reactions().undoScheduledPaid(_call);
|
||||
}
|
||||
|
||||
Messages::PaidLocalState Messages::starsLocalState() const {
|
||||
const auto &donors = _paid.top.topDonors;
|
||||
const auto i = ranges::find(donors, true, &StarsDonor::my);
|
||||
const auto local = int(_paid.scheduled);
|
||||
const auto my = (i != end(donors) ? i->stars : 0) + local;
|
||||
const auto total = _paid.top.total + local;
|
||||
return { .total = total, .my = my };
|
||||
}
|
||||
|
||||
void Messages::deleteConfirmed(MessageDeleteRequest request) {
|
||||
const auto eraseFrom = [&](auto iterator) {
|
||||
if (iterator != end(_messages)) {
|
||||
_messages.erase(iterator, end(_messages));
|
||||
pushChanges();
|
||||
}
|
||||
};
|
||||
const auto peer = _call->peer();
|
||||
if (const auto from = request.deleteAllFrom) {
|
||||
using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;
|
||||
_api->request(MTPphone_DeleteGroupCallParticipantMessages(
|
||||
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
|
||||
_call->inputCall(),
|
||||
from->input()
|
||||
)).send();
|
||||
eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));
|
||||
} else {
|
||||
using Flag = MTPphone_DeleteGroupCallMessages::Flag;
|
||||
_api->request(MTPphone_DeleteGroupCallMessages(
|
||||
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
|
||||
_call->inputCall(),
|
||||
MTP_vector<MTPint>(1, MTP_int(request.id.bare))
|
||||
)).send();
|
||||
eraseFrom(ranges::remove(_messages, request.id, &Message::id));
|
||||
}
|
||||
if (const auto ban = request.ban) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
ban->session().api().chatParticipants().kick(
|
||||
channel,
|
||||
ban,
|
||||
ChatRestrictionsInfo());
|
||||
} else {
|
||||
ban->session().api().blockedPeers().block(ban);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {
|
||||
if (stars <= 0) {
|
||||
return;
|
||||
}
|
||||
_paid.top.total += stars;
|
||||
const auto i = ranges::find(
|
||||
_paid.top.topDonors,
|
||||
from.get(),
|
||||
&StarsDonor::peer);
|
||||
if (i != end(_paid.top.topDonors)) {
|
||||
i->stars += stars;
|
||||
} else {
|
||||
_paid.top.topDonors.push_back({
|
||||
.peer = from,
|
||||
.stars = stars,
|
||||
.my = mine,
|
||||
});
|
||||
}
|
||||
ranges::stable_sort(
|
||||
_paid.top.topDonors,
|
||||
ranges::greater(),
|
||||
&StarsDonor::stars);
|
||||
_paidChanges.fire({ .peer = from, .stars = stars });
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
199
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
199
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
struct PaidReactionSend;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace MTP {
|
||||
class Sender;
|
||||
struct Response;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message {
|
||||
MsgId id = 0;
|
||||
TimeId date = 0;
|
||||
TimeId pinFinishDate = 0;
|
||||
not_null<PeerData*> peer;
|
||||
TextWithEntities text;
|
||||
int stars = 0;
|
||||
bool failed = false;
|
||||
bool admin = false;
|
||||
bool mine = false;
|
||||
};
|
||||
|
||||
struct MessageIdUpdate {
|
||||
MsgId localId = 0;
|
||||
MsgId realId = 0;
|
||||
};
|
||||
|
||||
struct MessageDeleteRequest {
|
||||
MsgId id = 0;
|
||||
PeerData *deleteAllFrom = nullptr;
|
||||
PeerData *ban = nullptr;
|
||||
bool reportSpam = false;
|
||||
};
|
||||
|
||||
struct StarsDonor {
|
||||
PeerData *peer = nullptr;
|
||||
int stars = 0;
|
||||
bool my = false;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StarsDonor &,
|
||||
const StarsDonor &) = default;
|
||||
};
|
||||
|
||||
struct StarsTop {
|
||||
std::vector<StarsDonor> topDonors;
|
||||
int total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StarsTop &,
|
||||
const StarsTop &) = default;
|
||||
};
|
||||
|
||||
class Messages final : public base::has_weak_ptr {
|
||||
public:
|
||||
Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);
|
||||
~Messages();
|
||||
|
||||
void send(TextWithTags text, int stars);
|
||||
|
||||
void setApplyingInitial(bool value);
|
||||
void received(const MTPDupdateGroupCallMessage &data);
|
||||
void received(const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
void deleted(const MTPDupdateDeleteGroupCallMessages &data);
|
||||
void sent(const MTPDupdateMessageID &data);
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;
|
||||
[[nodiscard]] rpl::producer<MessageIdUpdate> idUpdates() const;
|
||||
|
||||
[[nodiscard]] int reactionsPaidScheduled() const;
|
||||
[[nodiscard]] PeerId reactionsLocalShownPeer() const;
|
||||
void reactionsPaidAdd(int count);
|
||||
void reactionsPaidScheduledCancel();
|
||||
void reactionsPaidSend();
|
||||
void undoScheduledPaidOnDestroy();
|
||||
|
||||
struct PaidLocalState {
|
||||
int total = 0;
|
||||
int my = 0;
|
||||
};
|
||||
[[nodiscard]] PaidLocalState starsLocalState() const;
|
||||
[[nodiscard]] rpl::producer<StarsDonor> starsValueChanges() const {
|
||||
return _paidChanges.events();
|
||||
}
|
||||
[[nodiscard]] const StarsTop &starsTop() const {
|
||||
return _paid.top;
|
||||
}
|
||||
|
||||
void requestHiddenShow() {
|
||||
_hiddenShowRequests.fire({});
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> hiddenShowRequested() const {
|
||||
return _hiddenShowRequests.events();
|
||||
}
|
||||
|
||||
void deleteConfirmed(MessageDeleteRequest request);
|
||||
|
||||
private:
|
||||
struct Pending {
|
||||
TextWithTags text;
|
||||
int stars = 0;
|
||||
};
|
||||
struct Paid {
|
||||
StarsTop top;
|
||||
PeerId scheduledShownPeer = 0;
|
||||
PeerId sendingShownPeer = 0;
|
||||
uint32 scheduled : 30 = 0;
|
||||
uint32 scheduledFlag : 1 = 0;
|
||||
uint32 scheduledPrivacySet : 1 = 0;
|
||||
uint32 sending : 30 = 0;
|
||||
uint32 sendingFlag : 1 = 0;
|
||||
uint32 sendingPrivacySet : 1 = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool ready() const;
|
||||
void sendPending();
|
||||
void pushChanges();
|
||||
void checkDestroying(bool afterChanges = false);
|
||||
|
||||
void received(
|
||||
MsgId id,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji = false);
|
||||
void sent(uint64 randomId, const MTP::Response &response);
|
||||
void sent(uint64 randomId, MsgId realId);
|
||||
void failed(uint64 randomId, const MTP::Response &response);
|
||||
|
||||
[[nodiscard]] bool skipMessage(
|
||||
const TextWithEntities &text,
|
||||
int stars) const;
|
||||
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
|
||||
void finishPaidSending(
|
||||
Data::PaidReactionSend send,
|
||||
bool success);
|
||||
void addStars(not_null<PeerData*> from, int stars, bool mine);
|
||||
void requestStarsStats();
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<MTP::Sender*> _api;
|
||||
|
||||
MsgId _conferenceIdAutoIncrement = 0;
|
||||
base::flat_map<uint64, MsgId> _conferenceIdByRandomId;
|
||||
|
||||
base::flat_map<uint64, MsgId> _sendingIdByRandomId;
|
||||
|
||||
Data::GroupCall *_real = nullptr;
|
||||
|
||||
std::vector<Pending> _pending;
|
||||
|
||||
base::Timer _destroyTimer;
|
||||
std::vector<Message> _messages;
|
||||
base::flat_set<MsgId> _skippedIds;
|
||||
rpl::event_stream<std::vector<Message>> _changes;
|
||||
rpl::event_stream<MessageIdUpdate> _idUpdates;
|
||||
bool _applyingInitial = false;
|
||||
|
||||
mtpRequestId _starsTopRequestId = 0;
|
||||
Paid _paid;
|
||||
rpl::event_stream<StarsDonor> _paidChanges;
|
||||
bool _paidSendingPending = false;
|
||||
|
||||
TimeId _ttl = 0;
|
||||
bool _changesScheduled = false;
|
||||
|
||||
rpl::event_stream<> _hiddenShowRequests;
|
||||
base::Timer _starsStatsTimer;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
1913
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
1913
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
File diff suppressed because it is too large
Load Diff
202
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
202
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
#include "ui/round_rect.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class ElasticScroll;
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class PopupMenu;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
struct StarsColoring;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message;
|
||||
struct MessageIdUpdate;
|
||||
struct MessageDeleteRequest;
|
||||
|
||||
enum class MessagesMode {
|
||||
GroupCall,
|
||||
VideoStream,
|
||||
};
|
||||
|
||||
class MessagesUi final {
|
||||
public:
|
||||
MessagesUi(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
MessagesMode mode,
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,
|
||||
rpl::producer<MessageIdUpdate> idUpdates,
|
||||
rpl::producer<bool> canManageValue,
|
||||
rpl::producer<bool> shown);
|
||||
~MessagesUi();
|
||||
|
||||
void move(int left, int bottom, int width, int availableHeight);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] rpl::producer<> hiddenShowRequested() const;
|
||||
[[nodiscard]] rpl::producer<MessageDeleteRequest> deleteRequests() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct MessageView;
|
||||
struct PinnedView;
|
||||
struct PayedBg {
|
||||
explicit PayedBg(const Ui::StarsColoring &coloring);
|
||||
|
||||
style::owned_color light;
|
||||
style::owned_color dark;
|
||||
Ui::RoundRect pinnedLight;
|
||||
Ui::RoundRect pinnedDark;
|
||||
Ui::RoundRect messageLight;
|
||||
Ui::RoundRect priceDark;
|
||||
Ui::RoundRect badgeDark;
|
||||
};
|
||||
|
||||
void setupBadges();
|
||||
void setupList(
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown);
|
||||
void showList(const std::vector<Message> &list);
|
||||
void handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates);
|
||||
void toggleMessage(MessageView &entry, bool shown);
|
||||
void setContentFailed(MessageView &entry);
|
||||
void setContent(MessageView &entry);
|
||||
void setContent(PinnedView &entry);
|
||||
void updateMessageSize(MessageView &entry);
|
||||
bool updateMessageHeight(MessageView &entry);
|
||||
void updatePinnedSize(PinnedView &entry);
|
||||
bool updatePinnedWidth(PinnedView &entry);
|
||||
void animateMessageSent(MessageView &entry);
|
||||
void repaintMessage(MsgId id);
|
||||
void highlightMessage(MsgId id);
|
||||
void startHighlight(MsgId id);
|
||||
void recountHeights(std::vector<MessageView>::iterator i, int top);
|
||||
void appendMessage(const Message &data);
|
||||
void checkReactionContent(
|
||||
MessageView &entry,
|
||||
const TextWithEntities &text);
|
||||
void startReactionAnimation(MessageView &entry);
|
||||
void updateReactionPosition(MessageView &entry);
|
||||
void removeReaction(not_null<Ui::RpWidget*> widget);
|
||||
void setupMessagesWidget();
|
||||
|
||||
void togglePinned(PinnedView &entry, bool shown);
|
||||
void repaintPinned(MsgId id);
|
||||
void recountWidths(std::vector<PinnedView>::iterator i, int left);
|
||||
void appendPinned(const Message &data, TimeId now);
|
||||
void setupPinnedWidget();
|
||||
|
||||
void applyGeometry();
|
||||
void applyGeometryToPinned();
|
||||
void updateGeometries();
|
||||
[[nodiscard]] int countPinnedScrollSkip(const PinnedView &entry) const;
|
||||
void setPinnedScrollSkip(int skip);
|
||||
|
||||
void updateTopFade();
|
||||
void updateBottomFade();
|
||||
void updateLeftFade();
|
||||
void updateRightFade();
|
||||
|
||||
void receiveSomeMouseEvents();
|
||||
void receiveAllMouseEvents();
|
||||
void handleClick(const MessageView &entry, QPoint point);
|
||||
void showContextMenu(const MessageView &entry, QPoint globalPoint);
|
||||
|
||||
[[nodiscard]] int donorPlace(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] TextWithEntities nameText(
|
||||
not_null<PeerData*> peer,
|
||||
int place);
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const MessagesMode _mode;
|
||||
std::unique_ptr<Ui::ElasticScroll> _scroll;
|
||||
Ui::Animations::Simple _scrollToAnimation;
|
||||
Ui::RpWidget *_messages = nullptr;
|
||||
QImage _canvas;
|
||||
|
||||
std::unique_ptr<Ui::ElasticScroll> _pinnedScroll;
|
||||
Ui::RpWidget *_pinned = nullptr;
|
||||
QImage _pinnedCanvas;
|
||||
int _pinnedScrollSkip = 0;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
rpl::variable<bool> _canManage;
|
||||
rpl::event_stream<> _hiddenShowRequested;
|
||||
rpl::event_stream<MessageDeleteRequest> _deleteRequests;
|
||||
std::optional<std::vector<Message>> _hidden;
|
||||
std::vector<MessageView> _views;
|
||||
style::complex_color _messageBg;
|
||||
Ui::RoundRect _messageBgRect;
|
||||
MsgId _delayedHighlightId = 0;
|
||||
MsgId _highlightId = 0;
|
||||
Ui::Animations::Simple _highlightAnimation;
|
||||
|
||||
std::vector<PinnedView> _pinnedViews;
|
||||
base::flat_map<uint64, std::unique_ptr<PayedBg>> _bgs;
|
||||
|
||||
QPoint _reactionBasePosition;
|
||||
rpl::lifetime _effectsLifetime;
|
||||
|
||||
Ui::Text::String _liveBadge;
|
||||
Ui::Text::String _adminBadge;
|
||||
|
||||
Ui::Text::CustomEmojiHelper _crownHelper;
|
||||
base::flat_map<int, QString> _crownEmojiDataCache;
|
||||
rpl::variable<std::vector<not_null<PeerData*>>> _topDonors;
|
||||
//Ui::Animations::Simple _topFadeAnimation;
|
||||
//Ui::Animations::Simple _bottomFadeAnimation;
|
||||
//Ui::Animations::Simple _leftFadeAnimation;
|
||||
//Ui::Animations::Simple _rightFadeAnimation;
|
||||
int _fadeHeight = 0;
|
||||
int _fadeWidth = 0;
|
||||
bool _topFadeShown = false;
|
||||
bool _bottomFadeShown = false;
|
||||
bool _leftFadeShown = false;
|
||||
bool _rightFadeShown = false;
|
||||
bool _streamMode = false;
|
||||
|
||||
int _left = 0;
|
||||
int _bottom = 0;
|
||||
int _width = 0;
|
||||
int _availableHeight = 0;
|
||||
|
||||
MsgId _revealedSpoilerId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
2944
Telegram/SourceFiles/calls/group/calls_group_panel.cpp
Normal file
2944
Telegram/SourceFiles/calls/group/calls_group_panel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
281
Telegram/SourceFiles/calls/group/calls_group_panel.h
Normal file
281
Telegram/SourceFiles/calls/group/calls_group_panel.h
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Image;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class AbstractButton;
|
||||
class ImportantTooltip;
|
||||
class DropdownMenu;
|
||||
class CallButton;
|
||||
class CallMuteButton;
|
||||
class IconButton;
|
||||
class FlatLabel;
|
||||
class RpWidget;
|
||||
class RpWindow;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
class ScrollArea;
|
||||
class GenericBox;
|
||||
class GroupCallScheduledLeft;
|
||||
struct CallButtonColors;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
class Instance;
|
||||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
struct CallBodyLayout;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
struct InviteRequest;
|
||||
struct ConferencePanelMigration;
|
||||
class Window;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Toasts;
|
||||
class Members;
|
||||
class Viewport;
|
||||
enum class PanelMode;
|
||||
enum class StickedTooltip;
|
||||
class MicLevelTester;
|
||||
class MessageField;
|
||||
class MessagesUi;
|
||||
|
||||
class Panel final
|
||||
: public base::has_weak_ptr
|
||||
, private Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
explicit Panel(not_null<GroupCall*> call);
|
||||
Panel(not_null<GroupCall*> call, ConferencePanelMigration info);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
[[nodiscard]] not_null<GroupCall*> call() const;
|
||||
[[nodiscard]] bool isVisible() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
void migrationShowShareLink();
|
||||
void migrationInviteUsers(std::vector<InviteRequest> users);
|
||||
|
||||
void minimize();
|
||||
void toggleFullScreen();
|
||||
void toggleFullScreen(bool fullscreen);
|
||||
void close();
|
||||
void showAndActivate();
|
||||
void closeBeforeDestroy();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
|
||||
[[nodiscard]] not_null<Window*> callWindow() const;
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
using State = GroupCall::State;
|
||||
struct ControlsBackgroundNarrow;
|
||||
|
||||
enum class NiceTooltipType {
|
||||
Normal,
|
||||
Sticked,
|
||||
};
|
||||
enum class StickedTooltipHide {
|
||||
Unavailable,
|
||||
Activated,
|
||||
Discarded,
|
||||
};
|
||||
|
||||
[[nodiscard]] PanelMode mode() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
|
||||
void initWindow();
|
||||
void initWidget();
|
||||
void initControls();
|
||||
void initShareAction();
|
||||
void initLayout(ConferencePanelMigration info);
|
||||
void initGeometry(ConferencePanelMigration info);
|
||||
void setupScheduledLabels(rpl::producer<TimeId> date);
|
||||
void setupMembers();
|
||||
void setupVideo(not_null<Viewport*> viewport);
|
||||
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
|
||||
[[nodiscard]] rpl::producer<QString> titleText();
|
||||
|
||||
bool handleClose();
|
||||
void startScheduledNow();
|
||||
void trackControls(bool track, bool force = false);
|
||||
void raiseControls();
|
||||
void enlargeVideo();
|
||||
|
||||
void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime);
|
||||
void trackControlOver(not_null<Ui::RpWidget*> control, bool over);
|
||||
void showNiceTooltip(
|
||||
not_null<Ui::RpWidget*> control,
|
||||
NiceTooltipType type = NiceTooltipType::Normal);
|
||||
void showStickedTooltip();
|
||||
void hideStickedTooltip(StickedTooltipHide hide);
|
||||
void hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide);
|
||||
void hideNiceTooltip();
|
||||
|
||||
bool updateMode();
|
||||
void updateControlsGeometry();
|
||||
void updateButtonsGeometry();
|
||||
void updateTooltipGeometry();
|
||||
void updateButtonsStyles();
|
||||
void updateMembersGeometry();
|
||||
void refreshControlsBackground();
|
||||
void refreshTitleBackground();
|
||||
void setupControlsBackgroundWide();
|
||||
void setupControlsBackgroundNarrow();
|
||||
void showControls();
|
||||
void createMessageButton();
|
||||
void refreshLeftButton();
|
||||
void refreshVideoButtons(
|
||||
std::optional<bool> overrideWideMode = std::nullopt);
|
||||
void refreshTopButton();
|
||||
void createPinOnTop();
|
||||
void setupEmptyRtmp();
|
||||
void toggleWideControls(bool shown);
|
||||
void updateWideControlsVisibility();
|
||||
[[nodiscard]] bool videoButtonInNarrowMode() const;
|
||||
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
|
||||
void toggleMessageTyping();
|
||||
[[nodiscard]] rpl::producer<Ui::CallButtonColors> toggleableOverrides(
|
||||
rpl::producer<bool> active);
|
||||
|
||||
void endCall();
|
||||
|
||||
void showMainMenu();
|
||||
void chooseJoinAs();
|
||||
void chooseShareScreenSource();
|
||||
void screenSharingPrivacyRequest();
|
||||
void addMembers();
|
||||
void kickParticipant(not_null<PeerData*> participantPeer);
|
||||
void kickParticipantSure(not_null<PeerData*> participantPeer);
|
||||
[[nodiscard]] QRect computeTitleRect() const;
|
||||
void refreshTitle();
|
||||
void refreshTitleGeometry();
|
||||
void refreshTitleColors();
|
||||
void setupRealCallViewers();
|
||||
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
||||
|
||||
void migrate(not_null<ChannelData*> channel);
|
||||
void subscribeToPeerChanges();
|
||||
|
||||
QWidget *chooseSourceParent() override;
|
||||
QString chooseSourceActiveDeviceId() override;
|
||||
bool chooseSourceActiveWithAudio() override;
|
||||
bool chooseSourceWithAudioSupported() override;
|
||||
rpl::lifetime &chooseSourceInstanceLifetime() override;
|
||||
void chooseSourceAccepted(
|
||||
const QString &deviceId,
|
||||
bool withAudio) override;
|
||||
void chooseSourceStop() override;
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
not_null<PeerData*> _peer;
|
||||
|
||||
std::shared_ptr<Window> _window;
|
||||
rpl::variable<PanelMode> _mode;
|
||||
rpl::variable<bool> _fullScreenOrMaximized = false;
|
||||
bool _unpinnedMaximized = false;
|
||||
bool _rtmpFull = false;
|
||||
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
object_ptr<Ui::RpWidget> _titleBackground = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _title = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _titleSeparator = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _viewers = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
|
||||
object_ptr<Ui::AbstractButton> _recordingMark = { nullptr };
|
||||
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
|
||||
object_ptr<Ui::IconButton> _pinOnTop = { nullptr };
|
||||
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
|
||||
rpl::variable<bool> _wideMenuShown = false;
|
||||
rpl::variable<bool> _messageTyping = false;
|
||||
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
|
||||
object_ptr<Members> _members = { nullptr };
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::lifetime _trackControlsOverStateLifetime;
|
||||
rpl::lifetime _trackControlsMenuLifetime;
|
||||
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _countdown = { nullptr };
|
||||
std::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;
|
||||
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _emptyRtmp = { nullptr };
|
||||
ChooseJoinAsProcess _joinAsProcess;
|
||||
std::optional<QRect> _lastSmallGeometry;
|
||||
std::optional<QRect> _lastLargeGeometry;
|
||||
bool _lastLargeMaximized = false;
|
||||
bool _showWideControls = false;
|
||||
bool _trackControls = false;
|
||||
bool _wideControlsShown = false;
|
||||
Ui::Animations::Simple _wideControlsAnimation;
|
||||
|
||||
object_ptr<Ui::RpWidget> _controlsBackgroundWide = { nullptr };
|
||||
std::unique_ptr<ControlsBackgroundNarrow> _controlsBackgroundNarrow;
|
||||
object_ptr<Ui::CallButton> _settings = { nullptr };
|
||||
object_ptr<Ui::CallButton> _wideMenu = { nullptr };
|
||||
object_ptr<Ui::CallButton> _callShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _video = { nullptr };
|
||||
object_ptr<Ui::CallButton> _screenShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _message = { nullptr };
|
||||
std::unique_ptr<Ui::CallMuteButton> _mute;
|
||||
object_ptr<Ui::CallButton> _hangup;
|
||||
object_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };
|
||||
QPointer<Ui::IconButton> _stickedTooltipClose;
|
||||
QPointer<Ui::RpWidget> _niceTooltipControl;
|
||||
StickedTooltips _stickedTooltipsShown;
|
||||
Fn<void()> _callShareLinkCallback;
|
||||
|
||||
std::shared_ptr<ChatHelpers::Show> _cachedShow;
|
||||
std::unique_ptr<MessageField> _messageField;
|
||||
std::unique_ptr<MessagesUi> _messages;
|
||||
|
||||
const std::unique_ptr<Toasts> _toasts;
|
||||
|
||||
std::unique_ptr<MicLevelTester> _micLevelTester;
|
||||
|
||||
style::complex_color _controlsBackgroundColor;
|
||||
base::Timer _hideControlsTimer;
|
||||
rpl::lifetime _hideControlsTimerLifetime;
|
||||
|
||||
rpl::lifetime _peerLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
356
Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp
Normal file
356
Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
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_rtmp.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QStyle>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPasswordCharAmount = 24;
|
||||
|
||||
void StartWithBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void()> done,
|
||||
Fn<void()> revoke,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
{
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box->verticalLayout(),
|
||||
{
|
||||
.name = u"rtmp"_q,
|
||||
.sizeOverride = st::normalBoxLottieSize,
|
||||
},
|
||||
{});
|
||||
box->verticalLayout()->add(std::move(icon.widget), {}, style::al_top);
|
||||
box->setShowFinishedCallback([animate = icon.animate] {
|
||||
animate(anim::repeat::loop);
|
||||
});
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
}
|
||||
|
||||
StartRtmpProcess::FillRtmpRows(
|
||||
box->verticalLayout(),
|
||||
true,
|
||||
std::move(show),
|
||||
std::move(data),
|
||||
&st::boxLabel,
|
||||
&st::groupCallRtmpShowButton,
|
||||
&st::defaultSubsectionTitle,
|
||||
&st::attentionBoxButton,
|
||||
&st::defaultPopupMenu);
|
||||
|
||||
box->setTitle(tr::lng_group_call_rtmp_title());
|
||||
|
||||
Ui::AddDividerText(
|
||||
box->verticalLayout(),
|
||||
tr::lng_group_call_rtmp_info());
|
||||
|
||||
box->addButton(tr::lng_group_call_rtmp_start(), done);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
box->setWidth(st::boxWideWidth);
|
||||
{
|
||||
const auto top = box->addTopButton(st::infoTopBarMenu);
|
||||
top->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
top,
|
||||
st::popupMenuWithIcons);
|
||||
state->menu->addAction(
|
||||
tr::lng_group_invite_context_revoke(tr::now),
|
||||
revoke,
|
||||
&st::menuIconRemove);
|
||||
state->menu->setForcedOrigin(
|
||||
Ui::PanelAnimation::Origin::TopRight);
|
||||
top->setForceRippled(true);
|
||||
const auto raw = state->menu.get();
|
||||
raw->setDestroyedCallback([=] {
|
||||
if ((state->menu == raw) && top) {
|
||||
top->setForceRippled(false);
|
||||
}
|
||||
});
|
||||
state->menu->popup(top->mapToGlobal(top->rect().center()));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StartRtmpProcess::~StartRtmpProcess() {
|
||||
close();
|
||||
}
|
||||
|
||||
void StartRtmpProcess::start(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
Expects(done != nullptr);
|
||||
|
||||
const auto session = &peer->session();
|
||||
if (_request) {
|
||||
if (_request->peer == peer) {
|
||||
_request->show = std::move(show);
|
||||
_request->done = std::move(done);
|
||||
return;
|
||||
}
|
||||
session->api().request(_request->id).cancel();
|
||||
_request = nullptr;
|
||||
}
|
||||
_request = std::make_unique<RtmpRequest>(
|
||||
RtmpRequest{
|
||||
.peer = peer,
|
||||
.show = std::move(show),
|
||||
.done = std::move(done),
|
||||
});
|
||||
session->account().sessionChanges(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
requestUrl(false);
|
||||
}
|
||||
|
||||
void StartRtmpProcess::close() {
|
||||
if (_request) {
|
||||
_request->peer->session().api().request(_request->id).cancel();
|
||||
if (const auto strong = _request->box.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
_request = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void StartRtmpProcess::requestUrl(bool revoke) {
|
||||
const auto session = &_request->peer->session();
|
||||
_request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl(
|
||||
MTP_flags(0),
|
||||
_request->peer->input(),
|
||||
MTP_bool(revoke)
|
||||
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
|
||||
auto data = result.match([&](
|
||||
const MTPDphone_groupCallStreamRtmpUrl &data) {
|
||||
return RtmpInfo{ .url = qs(data.vurl()), .key = qs(data.vkey()) };
|
||||
});
|
||||
processUrl(std::move(data));
|
||||
}).fail([=] {
|
||||
_request->show->showToast(Lang::Hard::ServerError());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void StartRtmpProcess::processUrl(RtmpInfo data) {
|
||||
if (!_request->box) {
|
||||
createBox();
|
||||
}
|
||||
_request->data = std::move(data);
|
||||
}
|
||||
|
||||
void StartRtmpProcess::finish(JoinInfo info) {
|
||||
info.rtmpInfo = _request->data.current();
|
||||
_request->done(std::move(info));
|
||||
}
|
||||
|
||||
void StartRtmpProcess::createBox() {
|
||||
auto done = [=] {
|
||||
const auto peer = _request->peer;
|
||||
const auto joinAs = (peer->isChat() && peer->asChat()->amCreator())
|
||||
? peer
|
||||
: (peer->isChannel() && peer->asChannel()->amCreator())
|
||||
? peer
|
||||
: peer->session().user();
|
||||
finish({ .peer = peer, .joinAs = joinAs, .rtmp = true });
|
||||
};
|
||||
auto revoke = [=] {
|
||||
const auto guard = base::make_weak(&_request->guard);
|
||||
_request->show->showBox(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_revoke_sure(),
|
||||
.confirmed = crl::guard(guard, [=](Fn<void()> &&close) {
|
||||
requestUrl(true);
|
||||
close();
|
||||
}),
|
||||
.confirmText = tr::lng_group_invite_context_revoke(),
|
||||
}));
|
||||
};
|
||||
auto object = Box(
|
||||
StartWithBox,
|
||||
std::move(done),
|
||||
std::move(revoke),
|
||||
_request->show,
|
||||
_request->data.value());
|
||||
object->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
_request->box = base::make_weak(object.data());
|
||||
_request->show->showBox(std::move(object));
|
||||
}
|
||||
|
||||
void StartRtmpProcess::FillRtmpRows(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
bool divider,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data,
|
||||
const style::FlatLabel *labelStyle,
|
||||
const style::IconButton *showButtonStyle,
|
||||
const style::FlatLabel *subsectionTitleStyle,
|
||||
const style::RoundButton *attentionButtonStyle,
|
||||
const style::PopupMenu *popupMenuStyle) {
|
||||
struct State {
|
||||
rpl::variable<QString> key;
|
||||
rpl::variable<QString> url;
|
||||
bool warned = false;
|
||||
};
|
||||
|
||||
const auto &rowPadding = st::boxRowPadding;
|
||||
|
||||
const auto state = container->lifetime().make_state<State>();
|
||||
state->key = rpl::duplicate(
|
||||
data
|
||||
) | rpl::map([=](const auto &d) { return d.key; });
|
||||
state->url = std::move(
|
||||
data
|
||||
) | rpl::map([=](const auto &d) { return d.url; });
|
||||
|
||||
const auto showToast = [=](const QString &text) {
|
||||
show->showToast(text);
|
||||
};
|
||||
const auto addButton = [&](
|
||||
bool key,
|
||||
rpl::producer<QString> &&text) {
|
||||
auto wrap = object_ptr<Ui::RpWidget>(container);
|
||||
auto button = Ui::CreateChild<Ui::RoundButton>(
|
||||
wrap.data(),
|
||||
rpl::duplicate(text),
|
||||
st::groupCallRtmpCopyButton);
|
||||
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
button->setClickedCallback(key
|
||||
? Fn<void()>([=] {
|
||||
QGuiApplication::clipboard()->setText(state->key.current());
|
||||
showToast(tr::lng_group_call_rtmp_key_copied(tr::now));
|
||||
})
|
||||
: Fn<void()>([=] {
|
||||
QGuiApplication::clipboard()->setText(state->url.current());
|
||||
showToast(tr::lng_group_call_rtmp_url_copied(tr::now));
|
||||
}));
|
||||
Ui::AddSkip(container, st::groupCallRtmpCopyButtonTopSkip);
|
||||
const auto weak = container->add(std::move(wrap), rowPadding);
|
||||
Ui::AddSkip(container, st::groupCallRtmpCopyButtonBottomSkip);
|
||||
button->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
weak->resize(weak->width(), height);
|
||||
}, container->lifetime());
|
||||
return weak;
|
||||
};
|
||||
|
||||
const auto addLabel = [&](v::text::data &&text) {
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
v::text::take_marked(std::move(text)),
|
||||
*labelStyle,
|
||||
*popupMenuStyle),
|
||||
st::boxRowPadding + QMargins(0, 0, showButtonStyle->width, 0));
|
||||
label->setSelectable(true);
|
||||
label->setBreakEverywhere(true);
|
||||
return label;
|
||||
};
|
||||
|
||||
// Server URL.
|
||||
Ui::AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_group_call_rtmp_url_subtitle(),
|
||||
st::groupCallRtmpSubsectionTitleAddPadding,
|
||||
subsectionTitleStyle);
|
||||
|
||||
auto urlLabelContent = state->url.value();
|
||||
addLabel(std::move(urlLabelContent));
|
||||
Ui::AddSkip(container, st::groupCallRtmpUrlSkip);
|
||||
addButton(false, tr::lng_group_call_rtmp_url_copy());
|
||||
//
|
||||
|
||||
if (divider) {
|
||||
Ui::AddDivider(container);
|
||||
}
|
||||
|
||||
// Stream Key.
|
||||
Ui::AddSkip(container, st::groupCallRtmpKeySubsectionTitleSkip);
|
||||
|
||||
Ui::AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_group_call_rtmp_key_subtitle(),
|
||||
st::groupCallRtmpSubsectionTitleAddPadding,
|
||||
subsectionTitleStyle);
|
||||
|
||||
auto keyLabelContent = state->key.value(
|
||||
) | rpl::map([](const QString &key) {
|
||||
const auto size = int(key.size());
|
||||
auto result = TextWithEntities{ key };
|
||||
if (size > 0) {
|
||||
result.entities.push_back({ EntityType::Spoiler, 0, size });
|
||||
}
|
||||
return result;
|
||||
}) | rpl::after_next([=] {
|
||||
container->resizeToWidth(container->widthNoMargins());
|
||||
});
|
||||
const auto streamKeyLabel = addLabel(std::move(keyLabelContent));
|
||||
streamKeyLabel->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton button) {
|
||||
if (button == Qt::LeftButton) {
|
||||
show->showBox(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_key_warning(
|
||||
tr::rich),
|
||||
.confirmed = [=](Fn<void()> &&close) {
|
||||
handler->onClick({});
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_from_request_understand(),
|
||||
.cancelText = tr::lng_cancel(),
|
||||
.confirmStyle = attentionButtonStyle,
|
||||
.labelStyle = labelStyle,
|
||||
}));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
addButton(true, tr::lng_group_call_rtmp_key_copy());
|
||||
//
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
75
Telegram/SourceFiles/calls/group/calls_group_rtmp.h
Normal file
75
Telegram/SourceFiles/calls/group/calls_group_rtmp.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
struct RoundButton;
|
||||
struct IconButton;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct JoinInfo;
|
||||
|
||||
class StartRtmpProcess final {
|
||||
public:
|
||||
StartRtmpProcess() = default;
|
||||
~StartRtmpProcess();
|
||||
|
||||
void start(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done);
|
||||
void close();
|
||||
|
||||
static void FillRtmpRows(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
bool divider,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data,
|
||||
const style::FlatLabel *labelStyle,
|
||||
const style::IconButton *showButtonStyle,
|
||||
const style::FlatLabel *subsectionTitleStyle,
|
||||
const style::RoundButton *attentionButtonStyle,
|
||||
const style::PopupMenu *popupMenuStyle);
|
||||
|
||||
private:
|
||||
void requestUrl(bool revoke);
|
||||
void processUrl(RtmpInfo data);
|
||||
void createBox();
|
||||
void finish(JoinInfo info);
|
||||
|
||||
struct RtmpRequest {
|
||||
not_null<PeerData*> peer;
|
||||
rpl::variable<RtmpInfo> data;
|
||||
std::shared_ptr<Ui::Show> show;
|
||||
Fn<void(JoinInfo)> done;
|
||||
base::has_weak_ptr guard;
|
||||
base::weak_qptr<Ui::BoxContent> box;
|
||||
rpl::lifetime lifetime;
|
||||
mtpRequestId id = 0;
|
||||
};
|
||||
std::unique_ptr<RtmpRequest> _request;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
988
Telegram/SourceFiles/calls/group/calls_group_settings.cpp
Normal file
988
Telegram/SourceFiles/calls/group/calls_group_settings.cpp
Normal file
@@ -0,0 +1,988 @@
|
||||
/*
|
||||
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_settings.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h" // LeaveBox.
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_volume_item.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "ui/widgets/level_meter.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "history/view/history_view_schedule_box.h"
|
||||
#include "history/history_item_helpers.h" // GetErrorForSending.
|
||||
#include "history/history.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/global_shortcuts.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_user.h"
|
||||
#include "calls/group/calls_group_rtmp.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "webrtc/webrtc_audio_input_tester.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
#include "settings/settings_calls.h"
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDelaysCount = 201;
|
||||
constexpr auto kMicrophoneTooltipAfterLoudCount = 3;
|
||||
constexpr auto kDropLoudAfterQuietCount = 5;
|
||||
constexpr auto kMicrophoneTooltipLevelThreshold = 0.2;
|
||||
constexpr auto kMicrophoneTooltipCheckInterval = crl::time(500);
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
constexpr auto kCheckAccessibilityInterval = crl::time(500);
|
||||
#endif // Q_OS_MAC
|
||||
|
||||
void SaveCallJoinMuted(
|
||||
not_null<PeerData*> peer,
|
||||
CallId callId,
|
||||
bool joinMuted) {
|
||||
const auto call = peer->groupCall();
|
||||
if (!call
|
||||
|| call->id() != callId
|
||||
|| peer->isUser()
|
||||
|| !peer->canManageGroupCall()
|
||||
|| !call->canChangeJoinMuted()
|
||||
|| call->joinMuted() == joinMuted) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setJoinMutedLocally(joinMuted);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_join_muted),
|
||||
call->input(),
|
||||
MTP_bool(joinMuted),
|
||||
MTPBool(), // messages_enabled
|
||||
MTPlong() // send_paid_messages_stars
|
||||
)).send();
|
||||
}
|
||||
|
||||
void SaveCallMessagesEnabled(
|
||||
not_null<PeerData*> peer,
|
||||
CallId callId,
|
||||
bool messagesEnabled) {
|
||||
const auto call = peer->groupCall();
|
||||
if (!call
|
||||
|| call->id() != callId
|
||||
|| !peer->canManageGroupCall()
|
||||
|| !call->canChangeMessagesEnabled()
|
||||
|| call->messagesEnabled() == messagesEnabled) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setMessagesEnabledLocally(messagesEnabled);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_messages_enabled),
|
||||
call->input(),
|
||||
MTPBool(), // join_muted
|
||||
MTP_bool(messagesEnabled),
|
||||
MTPlong() // send_paid_messages_stars
|
||||
)).send();
|
||||
}
|
||||
|
||||
[[nodiscard]] crl::time DelayByIndex(int index) {
|
||||
return index * crl::time(10);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString FormatDelay(crl::time delay) {
|
||||
return (delay < crl::time(1000))
|
||||
? tr::lng_group_call_ptt_delay_ms(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
QString::number(delay))
|
||||
: tr::lng_group_call_ptt_delay_s(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
QString::number(delay / 1000., 'f', 2));
|
||||
}
|
||||
|
||||
object_ptr<ShareBox> ShareInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &linkSpeaker,
|
||||
const QString &linkListener,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
const auto sending = std::make_shared<bool>();
|
||||
const auto box = std::make_shared<base::weak_qptr<ShareBox>>();
|
||||
|
||||
auto bottom = linkSpeaker.isEmpty()
|
||||
? nullptr
|
||||
: object_ptr<Ui::PaddingWrap<Ui::Checkbox>>(
|
||||
nullptr,
|
||||
object_ptr<Ui::Checkbox>(
|
||||
nullptr,
|
||||
tr::lng_group_call_share_speaker(tr::now),
|
||||
true,
|
||||
st::groupCallCheckbox),
|
||||
st::groupCallShareMutedMargin);
|
||||
const auto speakerCheckbox = bottom ? bottom->entity() : nullptr;
|
||||
const auto currentLink = [=] {
|
||||
return (!speakerCheckbox || !speakerCheckbox->checked())
|
||||
? linkListener
|
||||
: linkSpeaker;
|
||||
};
|
||||
auto copyCallback = [=] {
|
||||
QGuiApplication::clipboard()->setText(currentLink());
|
||||
show->showToast(tr::lng_group_invite_copied(tr::now));
|
||||
};
|
||||
auto countMessagesCallback = [=](const TextWithTags &comment) {
|
||||
return 1;
|
||||
};
|
||||
auto submitCallback = [=](
|
||||
std::vector<not_null<Data::Thread*>> &&result,
|
||||
Fn<bool()> checkPaid,
|
||||
TextWithTags &&comment,
|
||||
Api::SendOptions options,
|
||||
Data::ForwardOptions) {
|
||||
if (*sending || result.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = GetErrorForSending(
|
||||
result,
|
||||
{ .text = &comment });
|
||||
if (error.error) {
|
||||
if (const auto weak = *box) {
|
||||
weak->getDelegate()->show(
|
||||
MakeSendErrorBox(error, result.size() > 1));
|
||||
}
|
||||
return;
|
||||
} else if (!checkPaid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
*sending = true;
|
||||
const auto link = currentLink();
|
||||
if (!comment.text.isEmpty()) {
|
||||
comment.text = link + "\n" + comment.text;
|
||||
const auto add = link.size() + 1;
|
||||
for (auto &tag : comment.tags) {
|
||||
tag.offset += add;
|
||||
}
|
||||
} else {
|
||||
comment.text = link;
|
||||
}
|
||||
auto &api = peer->session().api();
|
||||
for (const auto thread : result) {
|
||||
auto message = Api::MessageToSend(
|
||||
Api::SendAction(thread, options));
|
||||
message.textWithTags = comment;
|
||||
message.action.clearDraft = false;
|
||||
api.sendMessage(std::move(message));
|
||||
}
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
show->showToast(tr::lng_share_done(tr::now));
|
||||
};
|
||||
auto filterCallback = [](not_null<Data::Thread*> thread) {
|
||||
if (const auto user = thread->peer()->asUser()) {
|
||||
if (user->canSendIgnoreMoneyRestrictions()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Data::CanSend(thread, ChatRestriction::SendOther);
|
||||
};
|
||||
|
||||
const auto st = ::Settings::DarkCreditsEntryBoxStyle();
|
||||
auto result = Box<ShareBox>(ShareBox::Descriptor{
|
||||
.session = &peer->session(),
|
||||
.copyCallback = std::move(copyCallback),
|
||||
.countMessagesCallback = std::move(countMessagesCallback),
|
||||
.submitCallback = std::move(submitCallback),
|
||||
.filterCallback = std::move(filterCallback),
|
||||
.bottomWidget = std::move(bottom),
|
||||
.copyLinkText = rpl::conditional(
|
||||
(speakerCheckbox
|
||||
? speakerCheckbox->checkedValue()
|
||||
: rpl::single(false)),
|
||||
tr::lng_group_call_copy_speaker_link(),
|
||||
tr::lng_group_call_copy_listener_link()),
|
||||
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
|
||||
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
|
||||
});
|
||||
*box = result.data();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call) {
|
||||
using namespace Settings;
|
||||
|
||||
const auto weakCall = base::make_weak(call);
|
||||
const auto weakBox = base::make_weak(box);
|
||||
|
||||
struct State {
|
||||
std::unique_ptr<Webrtc::DeviceResolver> deviceId;
|
||||
std::unique_ptr<Webrtc::AudioInputTester> micTester;
|
||||
Ui::LevelMeter *micTestLevel = nullptr;
|
||||
float micLevel = 0.;
|
||||
Ui::Animations::Simple micLevelAnimation;
|
||||
base::Timer levelUpdateTimer;
|
||||
bool generatingLink = false;
|
||||
};
|
||||
const auto peer = call->peer();
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
const auto real = call->conference()
|
||||
? call->lookupReal()
|
||||
: peer->groupCall();
|
||||
const auto rtmp = call->rtmp();
|
||||
const auto id = call->id();
|
||||
const auto goodReal = (real && real->id() == id);
|
||||
|
||||
const auto layout = box->verticalLayout();
|
||||
const auto &settings = Core::App().settings();
|
||||
|
||||
const auto joinMuted = !call->conference()
|
||||
&& goodReal
|
||||
&& real->joinMuted();
|
||||
const auto messagesEnabled = goodReal && real->messagesEnabled();
|
||||
const auto canChangeJoinMuted = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeJoinMuted();
|
||||
const auto canChangeMessagesEnabled = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeMessagesEnabled();
|
||||
const auto addCheck = canChangeJoinMuted && peer->canManageGroupCall();
|
||||
const auto addMessages = canChangeMessagesEnabled
|
||||
&& (call->conference() || peer->canManageGroupCall());
|
||||
|
||||
const auto addDivider = [&] {
|
||||
layout->add(object_ptr<Ui::BoxContentDivider>(
|
||||
layout,
|
||||
st::boxDividerHeight,
|
||||
st::groupCallDividerBar));
|
||||
};
|
||||
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
const auto muteJoined = addCheck
|
||||
? layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_new_muted(),
|
||||
st::groupCallSettingsButton))->toggleOn(rpl::single(joinMuted))
|
||||
: nullptr;
|
||||
const auto enableMessages = addMessages
|
||||
? layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_enable_messages(),
|
||||
st::groupCallSettingsButton))->toggleOn(
|
||||
rpl::single(messagesEnabled))
|
||||
: nullptr;
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
|
||||
auto playbackIdWithFallback = Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callPlaybackDeviceIdValue(),
|
||||
Core::App().settings().playbackDeviceIdValue());
|
||||
AddButtonWithLabel(
|
||||
layout,
|
||||
tr::lng_group_call_speakers(),
|
||||
PlaybackDeviceNameValue(rpl::duplicate(playbackIdWithFallback)),
|
||||
st::groupCallSettingsButton
|
||||
)->addClickHandler([=] {
|
||||
box->getDelegate()->show(ChoosePlaybackDeviceBox(
|
||||
rpl::duplicate(playbackIdWithFallback),
|
||||
crl::guard(box, [=](const QString &id) {
|
||||
Core::App().settings().setCallPlaybackDeviceId(id);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}),
|
||||
&st::groupCallCheckbox,
|
||||
&st::groupCallRadio));
|
||||
});
|
||||
|
||||
if (!rtmp) {
|
||||
auto captureIdWithFallback = Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue());
|
||||
AddButtonWithLabel(
|
||||
layout,
|
||||
tr::lng_group_call_microphone(),
|
||||
CaptureDeviceNameValue(rpl::duplicate(captureIdWithFallback)),
|
||||
st::groupCallSettingsButton
|
||||
)->addClickHandler([=] {
|
||||
box->getDelegate()->show(ChooseCaptureDeviceBox(
|
||||
rpl::duplicate(captureIdWithFallback),
|
||||
crl::guard(box, [=](const QString &id) {
|
||||
Core::App().settings().setCallCaptureDeviceId(id);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}),
|
||||
&st::groupCallCheckbox,
|
||||
&st::groupCallRadio));
|
||||
});
|
||||
|
||||
state->micTestLevel = box->addRow(
|
||||
object_ptr<Ui::LevelMeter>(
|
||||
box.get(),
|
||||
st::groupCallLevelMeter),
|
||||
st::settingsLevelMeterPadding);
|
||||
state->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));
|
||||
|
||||
state->levelUpdateTimer.setCallback([=] {
|
||||
const auto was = state->micLevel;
|
||||
state->micLevel = state->micTester->getAndResetLevel();
|
||||
state->micLevelAnimation.start([=] {
|
||||
state->micTestLevel->setValue(
|
||||
state->micLevelAnimation.value(state->micLevel));
|
||||
}, was, state->micLevel, kMicTestAnimationDuration);
|
||||
});
|
||||
|
||||
Ui::AddSkip(layout);
|
||||
//Ui::AddDivider(layout);
|
||||
//Ui::AddSkip(layout);
|
||||
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_noise_suppression(),
|
||||
st::groupCallSettingsButton
|
||||
))->toggleOn(rpl::single(
|
||||
settings.groupCallNoiseSuppression()
|
||||
))->toggledChanges(
|
||||
) | rpl::on_next([=](bool enabled) {
|
||||
Core::App().settings().setGroupCallNoiseSuppression(enabled);
|
||||
call->setNoiseSuppression(enabled);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}, layout->lifetime());
|
||||
|
||||
using GlobalShortcut = base::GlobalShortcut;
|
||||
struct PushToTalkState {
|
||||
rpl::variable<QString> recordText = tr::lng_group_call_ptt_shortcut();
|
||||
rpl::variable<QString> shortcutText;
|
||||
rpl::event_stream<bool> pushToTalkToggles;
|
||||
std::shared_ptr<base::GlobalShortcutManager> manager;
|
||||
GlobalShortcut shortcut;
|
||||
crl::time delay = 0;
|
||||
bool recording = false;
|
||||
};
|
||||
if (base::GlobalShortcutsAvailable()) {
|
||||
const auto state = box->lifetime().make_state<PushToTalkState>();
|
||||
if (!base::GlobalShortcutsAllowed()) {
|
||||
Core::App().settings().setGroupCallPushToTalk(false);
|
||||
}
|
||||
const auto tryFillFromManager = [=] {
|
||||
state->shortcut = state->manager
|
||||
? state->manager->shortcutFromSerialized(
|
||||
Core::App().settings().groupCallPushToTalkShortcut())
|
||||
: nullptr;
|
||||
state->shortcutText = state->shortcut
|
||||
? state->shortcut->toDisplayString()
|
||||
: QString();
|
||||
};
|
||||
state->manager = settings.groupCallPushToTalk()
|
||||
? call->ensureGlobalShortcutManager()
|
||||
: nullptr;
|
||||
tryFillFromManager();
|
||||
|
||||
state->delay = settings.groupCallPushToTalkDelay();
|
||||
const auto pushToTalk = layout->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_push_to_talk(),
|
||||
st::groupCallSettingsButton
|
||||
))->toggleOn(rpl::single(
|
||||
settings.groupCallPushToTalk()
|
||||
) | rpl::then(state->pushToTalkToggles.events()));
|
||||
const auto pushToTalkWrap = layout->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
layout,
|
||||
object_ptr<Ui::VerticalLayout>(layout)));
|
||||
const auto pushToTalkInner = pushToTalkWrap->entity();
|
||||
const auto recording = pushToTalkInner->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
pushToTalkInner,
|
||||
state->recordText.value(),
|
||||
st::groupCallSettingsButton));
|
||||
CreateRightLabel(
|
||||
recording,
|
||||
state->shortcutText.value(),
|
||||
st::groupCallSettingsButton,
|
||||
state->recordText.value());
|
||||
|
||||
const auto applyAndSave = [=] {
|
||||
call->applyGlobalShortcutChanges();
|
||||
Core::App().saveSettingsDelayed();
|
||||
};
|
||||
const auto showPrivacyRequest = [=] {
|
||||
#ifdef Q_OS_MAC
|
||||
if (!Platform::IsMac10_14OrGreater()) {
|
||||
return;
|
||||
}
|
||||
const auto requestInputMonitoring = Platform::IsMac10_15OrGreater();
|
||||
box->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
rpl::combine(
|
||||
tr::lng_group_call_mac_access(),
|
||||
(requestInputMonitoring
|
||||
? tr::lng_group_call_mac_input()
|
||||
: tr::lng_group_call_mac_accessibility())
|
||||
) | rpl::map([](QString a, QString b) {
|
||||
auto result = tr::rich(a);
|
||||
result.append("\n\n").append(tr::rich(b));
|
||||
return result;
|
||||
}),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
box->addButton(tr::lng_group_call_mac_settings(), [=] {
|
||||
if (requestInputMonitoring) {
|
||||
Platform::OpenInputMonitoringPrivacySettings();
|
||||
} else {
|
||||
Platform::OpenAccessibilityPrivacySettings();
|
||||
}
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
if (!requestInputMonitoring) {
|
||||
// Accessibility is enabled without app restart, so short-poll it.
|
||||
base::timer_each(
|
||||
kCheckAccessibilityInterval
|
||||
) | rpl::filter([] {
|
||||
return base::GlobalShortcutsAllowed();
|
||||
}) | rpl::on_next([=] {
|
||||
box->closeBox();
|
||||
}, box->lifetime());
|
||||
}
|
||||
}));
|
||||
#endif // Q_OS_MAC
|
||||
};
|
||||
const auto ensureManager = [=] {
|
||||
if (state->manager) {
|
||||
return true;
|
||||
} else if (base::GlobalShortcutsAllowed()) {
|
||||
state->manager = call->ensureGlobalShortcutManager();
|
||||
tryFillFromManager();
|
||||
return true;
|
||||
}
|
||||
showPrivacyRequest();
|
||||
return false;
|
||||
};
|
||||
const auto stopRecording = [=] {
|
||||
state->recording = false;
|
||||
state->recordText = tr::lng_group_call_ptt_shortcut();
|
||||
state->shortcutText = state->shortcut
|
||||
? state->shortcut->toDisplayString()
|
||||
: QString();
|
||||
recording->setColorOverride(std::nullopt);
|
||||
if (state->manager) {
|
||||
state->manager->stopRecording();
|
||||
}
|
||||
};
|
||||
const auto startRecording = [=] {
|
||||
if (!ensureManager()) {
|
||||
state->pushToTalkToggles.fire(false);
|
||||
pushToTalkWrap->hide(anim::type::instant);
|
||||
return;
|
||||
}
|
||||
state->recording = true;
|
||||
state->recordText = tr::lng_group_call_ptt_recording();
|
||||
recording->setColorOverride(
|
||||
st::groupCallSettingsAttentionButton.textFg->c);
|
||||
auto progress = crl::guard(box, [=](GlobalShortcut shortcut) {
|
||||
state->shortcutText = shortcut->toDisplayString();
|
||||
});
|
||||
auto done = crl::guard(box, [=](GlobalShortcut shortcut) {
|
||||
state->shortcut = shortcut;
|
||||
Core::App().settings().setGroupCallPushToTalkShortcut(shortcut
|
||||
? shortcut->serialize()
|
||||
: QByteArray());
|
||||
applyAndSave();
|
||||
stopRecording();
|
||||
});
|
||||
state->manager->startRecording(std::move(progress), std::move(done));
|
||||
};
|
||||
recording->addClickHandler([=] {
|
||||
if (state->recording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
const auto label = pushToTalkInner->add(
|
||||
object_ptr<Ui::LabelSimple>(
|
||||
pushToTalkInner,
|
||||
st::groupCallDelayLabel),
|
||||
st::groupCallDelayLabelMargin);
|
||||
const auto value = std::clamp(
|
||||
state->delay,
|
||||
crl::time(0),
|
||||
DelayByIndex(kDelaysCount - 1));
|
||||
const auto callback = [=](crl::time delay) {
|
||||
state->delay = delay;
|
||||
label->setText(tr::lng_group_call_ptt_delay(
|
||||
tr::now,
|
||||
lt_delay,
|
||||
FormatDelay(delay)));
|
||||
if (Core::App().settings().groupCallPushToTalkDelay() != delay) {
|
||||
Core::App().settings().setGroupCallPushToTalkDelay(delay);
|
||||
applyAndSave();
|
||||
}
|
||||
};
|
||||
callback(value);
|
||||
const auto slider = pushToTalkInner->add(
|
||||
object_ptr<Ui::MediaSlider>(
|
||||
pushToTalkInner,
|
||||
st::groupCallDelaySlider),
|
||||
st::groupCallDelayMargin);
|
||||
slider->resize(st::groupCallDelaySlider.seekSize);
|
||||
slider->setPseudoDiscrete(
|
||||
kDelaysCount,
|
||||
DelayByIndex,
|
||||
value,
|
||||
callback);
|
||||
|
||||
pushToTalkWrap->toggle(
|
||||
settings.groupCallPushToTalk(),
|
||||
anim::type::instant);
|
||||
pushToTalk->toggledChanges(
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
if (!toggled) {
|
||||
stopRecording();
|
||||
} else if (!ensureManager()) {
|
||||
state->pushToTalkToggles.fire(false);
|
||||
pushToTalkWrap->hide(anim::type::instant);
|
||||
return;
|
||||
}
|
||||
Core::App().settings().setGroupCallPushToTalk(toggled);
|
||||
applyAndSave();
|
||||
pushToTalkWrap->toggle(toggled, anim::type::normal);
|
||||
}, pushToTalk->lifetime());
|
||||
|
||||
auto boxKeyFilter = [=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::KeyPress && state->recording)
|
||||
? base::EventFilterResult::Cancel
|
||||
: base::EventFilterResult::Continue;
|
||||
};
|
||||
box->lifetime().make_state<base::unique_qptr<QObject>>(
|
||||
base::install_event_filter(box, std::move(boxKeyFilter)));
|
||||
}
|
||||
|
||||
Ui::AddSkip(layout);
|
||||
//Ui::AddDivider(layout);
|
||||
//Ui::AddSkip(layout);
|
||||
}
|
||||
auto shareLink = Fn<void()>();
|
||||
if (peer->isChannel()
|
||||
&& peer->asChannel()->hasUsername()
|
||||
&& goodReal) {
|
||||
const auto showBox = crl::guard(box, [=](
|
||||
object_ptr<Ui::BoxContent> next) {
|
||||
box->getDelegate()->show(std::move(next));
|
||||
});
|
||||
const auto showToast = crl::guard(box, [=](QString text) {
|
||||
box->showToast(text);
|
||||
});
|
||||
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
|
||||
peer,
|
||||
box->uiShow());
|
||||
shareLink = std::move(shareLinkCallback);
|
||||
box->lifetime().add(std::move(shareLinkLifetime));
|
||||
} else {
|
||||
const auto lookupLink = [=] {
|
||||
if (const auto group = peer->asMegagroup()) {
|
||||
return group->hasUsername()
|
||||
? group->session().createInternalLinkFull(
|
||||
group->username())
|
||||
: group->inviteLink();
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
return chat->inviteLink();
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
const auto canCreateLink = [&] {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
return chat->canHaveInviteLink();
|
||||
} else if (const auto group = peer->asMegagroup()) {
|
||||
return group->canHaveInviteLink();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto alreadyHasLink = !lookupLink().isEmpty();
|
||||
if (alreadyHasLink || canCreateLink()) {
|
||||
if (!alreadyHasLink) {
|
||||
// Request invite link.
|
||||
peer->session().api().requestFullPeer(peer);
|
||||
}
|
||||
const auto copyLink = [=] {
|
||||
const auto link = lookupLink();
|
||||
if (link.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
if (weakBox) {
|
||||
box->showToast(
|
||||
tr::lng_create_channel_link_copied(tr::now));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
shareLink = [=] {
|
||||
if (!copyLink() && !state->generatingLink) {
|
||||
state->generatingLink = true;
|
||||
peer->session().api().inviteLinks().create({
|
||||
peer,
|
||||
crl::guard(layout, [=](auto&&) { copyLink(); })
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if (shareLink) {
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_share(),
|
||||
st::groupCallSettingsButton
|
||||
))->addClickHandler(std::move(shareLink));
|
||||
}
|
||||
if (rtmp && !call->rtmpInfo().url.isEmpty()) {
|
||||
Ui::AddSkip(layout);
|
||||
addDivider();
|
||||
Ui::AddSkip(layout);
|
||||
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
mtpRequestId requestId;
|
||||
rpl::event_stream<RtmpInfo> data;
|
||||
};
|
||||
const auto top = box->addTopButton(st::groupCallMenuToggle);
|
||||
const auto state = top->lifetime().make_state<State>();
|
||||
const auto revokeSure = [=] {
|
||||
const auto session = &peer->session();
|
||||
state->requestId = session->api().request(
|
||||
MTPphone_GetGroupCallStreamRtmpUrl(
|
||||
MTP_flags(0),
|
||||
peer->input(),
|
||||
MTP_bool(true)
|
||||
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
|
||||
auto data = result.match([&](
|
||||
const MTPDphone_groupCallStreamRtmpUrl &data) {
|
||||
return RtmpInfo{
|
||||
.url = qs(data.vurl()),
|
||||
.key = qs(data.vkey()),
|
||||
};
|
||||
});
|
||||
if (const auto call = weakCall.get()) {
|
||||
call->setRtmpInfo(data);
|
||||
}
|
||||
if (!top) {
|
||||
return;
|
||||
}
|
||||
state->requestId = 0;
|
||||
state->data.fire(std::move(data));
|
||||
}).fail([=] {
|
||||
state->requestId = 0;
|
||||
}).send();
|
||||
};
|
||||
const auto revoke = [=] {
|
||||
if (state->requestId || !top) {
|
||||
return;
|
||||
}
|
||||
box->getDelegate()->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_revoke_sure(),
|
||||
.confirmed = [=](Fn<void()> &&close) {
|
||||
revokeSure();
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_group_invite_context_revoke(),
|
||||
.labelStyle = &st::groupCallBoxLabel,
|
||||
}));
|
||||
};
|
||||
top->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
box,
|
||||
st::groupCallPopupMenu);
|
||||
state->menu->addAction(
|
||||
tr::lng_group_call_rtmp_revoke(tr::now),
|
||||
revoke);
|
||||
state->menu->setForcedOrigin(
|
||||
Ui::PanelAnimation::Origin::TopRight);
|
||||
top->setForceRippled(true);
|
||||
const auto raw = state->menu.get();
|
||||
raw->setDestroyedCallback([=] {
|
||||
if ((state->menu == raw) && top) {
|
||||
top->setForceRippled(false);
|
||||
}
|
||||
});
|
||||
state->menu->popup(
|
||||
top->mapToGlobal(QPoint(top->width() / 2, top->height())));
|
||||
return true;
|
||||
});
|
||||
|
||||
StartRtmpProcess::FillRtmpRows(
|
||||
layout,
|
||||
false,
|
||||
box->uiShow(),
|
||||
state->data.events(),
|
||||
&st::groupCallBoxLabel,
|
||||
&st::groupCallSettingsRtmpShowButton,
|
||||
&st::groupCallSubsectionTitle,
|
||||
&st::groupCallAttentionBoxButton,
|
||||
&st::groupCallPopupMenu);
|
||||
state->data.fire(call->rtmpInfo());
|
||||
|
||||
addDivider();
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
if (rtmp) {
|
||||
const auto volumeItem = layout->add(
|
||||
object_ptr<MenuVolumeItem>(
|
||||
layout,
|
||||
st::groupCallVolumeSettings,
|
||||
st::groupCallVolumeSettingsSlider,
|
||||
call->otherParticipantStateValue(
|
||||
) | rpl::filter([=](const Group::ParticipantState &data) {
|
||||
return data.peer == peer;
|
||||
}),
|
||||
call->rtmpVolume(),
|
||||
Group::kMaxVolume,
|
||||
false,
|
||||
st::groupCallVolumeSettingsPadding));
|
||||
|
||||
const auto toggleMute = crl::guard(layout, [=](bool m, bool local) {
|
||||
if (call) {
|
||||
call->toggleMute({
|
||||
.peer = peer,
|
||||
.mute = m,
|
||||
.locallyOnly = local,
|
||||
});
|
||||
}
|
||||
});
|
||||
const auto changeVolume = crl::guard(layout, [=](int v, bool local) {
|
||||
if (call) {
|
||||
call->changeVolume({
|
||||
.peer = peer,
|
||||
.volume = std::clamp(v, 1, Group::kMaxVolume),
|
||||
.locallyOnly = local,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
volumeItem->toggleMuteLocallyRequests(
|
||||
) | rpl::on_next([=](bool muted) {
|
||||
toggleMute(muted, true);
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
volumeItem->changeVolumeLocallyRequests(
|
||||
) | rpl::on_next([=](int volume) {
|
||||
changeVolume(volume, true);
|
||||
}, volumeItem->lifetime());
|
||||
}
|
||||
|
||||
if (call->canManage()) {
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
(peer->isBroadcast()
|
||||
? tr::lng_group_call_end_channel()
|
||||
: tr::lng_group_call_end()),
|
||||
st::groupCallSettingsAttentionButton
|
||||
))->addClickHandler([=] {
|
||||
if (const auto call = weakCall.get()) {
|
||||
box->getDelegate()->show(Box(
|
||||
LeaveBox,
|
||||
call,
|
||||
true,
|
||||
BoxContext::GroupCallPanel));
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!rtmp) {
|
||||
box->setShowFinishedCallback([=] {
|
||||
// Means we finished showing the box.
|
||||
crl::on_main(box, [=] {
|
||||
state->deviceId = std::make_unique<Webrtc::DeviceResolver>(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue()));
|
||||
state->micTester = std::make_unique<Webrtc::AudioInputTester>(
|
||||
state->deviceId->value());
|
||||
state->levelUpdateTimer.callEach(kMicTestUpdateInterval);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
box->setTitle(tr::lng_group_call_settings_title());
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
if (canChangeJoinMuted
|
||||
&& muteJoined
|
||||
&& muteJoined->toggled() != joinMuted) {
|
||||
SaveCallJoinMuted(peer, id, muteJoined->toggled());
|
||||
}
|
||||
if (canChangeMessagesEnabled
|
||||
&& enableMessages
|
||||
&& enableMessages->toggled() != messagesEnabled) {
|
||||
const auto value = enableMessages->toggled();
|
||||
if (!call->conference()) {
|
||||
SaveCallMessagesEnabled(peer, id, value);
|
||||
} else if (const auto real = call->lookupReal()) {
|
||||
real->setMessagesEnabledLocally(value);
|
||||
}
|
||||
}
|
||||
}, box->lifetime());
|
||||
box->addButton(tr::lng_box_done(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
|
||||
std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
struct State {
|
||||
State(not_null<Main::Session*> session) : session(session) {
|
||||
}
|
||||
~State() {
|
||||
session->api().request(linkListenerRequestId).cancel();
|
||||
session->api().request(linkSpeakerRequestId).cancel();
|
||||
}
|
||||
|
||||
not_null<Main::Session*> session;
|
||||
std::optional<QString> linkSpeaker;
|
||||
QString linkListener;
|
||||
mtpRequestId linkListenerRequestId = 0;
|
||||
mtpRequestId linkSpeakerRequestId = 0;
|
||||
bool generatingLink = false;
|
||||
};
|
||||
const auto state = lifetime.make_state<State>(&peer->session());
|
||||
if (peer->isUser() || !peer->canManageGroupCall()) {
|
||||
state->linkSpeaker = QString();
|
||||
}
|
||||
|
||||
const auto shareReady = [=] {
|
||||
if (!state->linkSpeaker.has_value()
|
||||
|| state->linkListener.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
show->showBox(ShareInviteLinkBox(
|
||||
peer,
|
||||
*state->linkSpeaker,
|
||||
state->linkListener,
|
||||
show));
|
||||
return true;
|
||||
};
|
||||
auto callback = [=] {
|
||||
const auto real = peer->migrateToOrMe()->groupCall();
|
||||
if (shareReady() || state->generatingLink || !real) {
|
||||
return;
|
||||
}
|
||||
state->generatingLink = true;
|
||||
|
||||
state->linkListenerRequestId = peer->session().api().request(
|
||||
MTPphone_ExportGroupCallInvite(
|
||||
MTP_flags(0),
|
||||
real->input()
|
||||
)
|
||||
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
|
||||
state->linkListenerRequestId = 0;
|
||||
result.match([&](
|
||||
const MTPDphone_exportedGroupCallInvite &data) {
|
||||
state->linkListener = qs(data.vlink());
|
||||
shareReady();
|
||||
});
|
||||
}).send();
|
||||
|
||||
if (real->rtmp()) {
|
||||
state->linkSpeaker = QString();
|
||||
state->linkSpeakerRequestId = 0;
|
||||
shareReady();
|
||||
} else if (!state->linkSpeaker.has_value()) {
|
||||
using Flag = MTPphone_ExportGroupCallInvite::Flag;
|
||||
state->linkSpeakerRequestId = peer->session().api().request(
|
||||
MTPphone_ExportGroupCallInvite(
|
||||
MTP_flags(Flag::f_can_self_unmute),
|
||||
real->input())
|
||||
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
|
||||
state->linkSpeakerRequestId = 0;
|
||||
result.match([&](
|
||||
const MTPDphone_exportedGroupCallInvite &data) {
|
||||
state->linkSpeaker = qs(data.vlink());
|
||||
shareReady();
|
||||
});
|
||||
}).fail([=] {
|
||||
state->linkSpeakerRequestId = 0;
|
||||
state->linkSpeaker = QString();
|
||||
shareReady();
|
||||
}).send();
|
||||
}
|
||||
};
|
||||
return { std::move(callback), std::move(lifetime) };
|
||||
}
|
||||
|
||||
MicLevelTester::MicLevelTester(Fn<void()> show)
|
||||
: _show(std::move(show))
|
||||
, _timer([=] { check(); })
|
||||
, _deviceId(std::make_unique<Webrtc::DeviceResolver>(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue())))
|
||||
, _tester(std::make_unique<Webrtc::AudioInputTester>(_deviceId->value())) {
|
||||
_timer.callEach(kMicrophoneTooltipCheckInterval);
|
||||
}
|
||||
|
||||
bool MicLevelTester::showTooltip() const {
|
||||
return (_loudCount >= kMicrophoneTooltipAfterLoudCount);
|
||||
}
|
||||
|
||||
void MicLevelTester::check() {
|
||||
const auto level = _tester->getAndResetLevel();
|
||||
if (level >= kMicrophoneTooltipLevelThreshold) {
|
||||
_quietCount = 0;
|
||||
if (++_loudCount >= kMicrophoneTooltipAfterLoudCount) {
|
||||
_show();
|
||||
}
|
||||
} else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) {
|
||||
_quietCount = 0;
|
||||
_loudCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
49
Telegram/SourceFiles/calls/group/calls_group_settings.h
Normal file
49
Telegram/SourceFiles/calls/group/calls_group_settings.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace Webrtc {
|
||||
class AudioInputTester;
|
||||
class DeviceResolver;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
void SettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
[[nodiscard]] std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
|
||||
class MicLevelTester final {
|
||||
public:
|
||||
explicit MicLevelTester(Fn<void()> show);
|
||||
|
||||
[[nodiscard]] bool showTooltip() const;
|
||||
|
||||
private:
|
||||
void check();
|
||||
|
||||
Fn<void()> _show;
|
||||
base::Timer _timer;
|
||||
std::unique_ptr<Webrtc::DeviceResolver> _deviceId;
|
||||
std::unique_ptr<Webrtc::AudioInputTester> _tester;
|
||||
int _loudCount = 0;
|
||||
int _quietCount = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
147
Telegram/SourceFiles/calls/group/calls_group_stars_box.cpp
Normal file
147
Telegram/SourceFiles/calls/group/calls_group_stars_box.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
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_stars_box.h"
|
||||
|
||||
#include "boxes/send_credits_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "payments/ui/payments_reaction_box.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxStarsFallback = 10'000;
|
||||
constexpr auto kDefaultStars = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
int MaxVideoStreamStarsCount(not_null<Main::Session*> session) {
|
||||
const auto appConfig = &session->appConfig();
|
||||
return std::max(
|
||||
appConfig->get<int>(
|
||||
u"stars_groupcall_message_amount_max"_q,
|
||||
kMaxStarsFallback),
|
||||
2);
|
||||
}
|
||||
|
||||
void VideoStreamStarsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
VideoStreamStarsBoxArgs &&args) {
|
||||
args.show->session().credits().load();
|
||||
|
||||
const auto admin = args.admin;
|
||||
const auto sending = args.sending;
|
||||
auto submitText = [=](rpl::producer<int> amount) {
|
||||
auto nice = std::move(amount) | rpl::map([=](int count) {
|
||||
return Ui::CreditsEmojiSmall().append(
|
||||
Lang::FormatCountDecimal(count));
|
||||
});
|
||||
return admin
|
||||
? tr::lng_box_ok(tr::marked)
|
||||
: (sending
|
||||
? tr::lng_paid_reaction_button
|
||||
: tr::lng_paid_comment_button)(
|
||||
lt_stars,
|
||||
std::move(nice),
|
||||
tr::rich);
|
||||
};
|
||||
const auto &show = args.show;
|
||||
const auto session = &show->session();
|
||||
const auto max = std::max(args.min, MaxVideoStreamStarsCount(session));
|
||||
const auto chosen = std::clamp(
|
||||
args.current ? args.current : kDefaultStars,
|
||||
args.min,
|
||||
max);
|
||||
auto top = std::vector<Ui::PaidReactionTop>();
|
||||
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
|
||||
const auto peer = entry.peer;
|
||||
const auto name = peer
|
||||
? peer->shortName()
|
||||
: tr::lng_paid_react_anonymous(tr::now);
|
||||
const auto open = [=] {
|
||||
if (const auto controller = show->resolveWindow()) {
|
||||
controller->showPeerInfo(peer);
|
||||
}
|
||||
};
|
||||
top.push_back({
|
||||
.name = name,
|
||||
.photo = (peer
|
||||
? Ui::MakeUserpicThumbnail(peer)
|
||||
: Ui::MakeHiddenAuthorThumbnail()),
|
||||
.barePeerId = peer ? uint64(peer->id.value) : 0,
|
||||
.count = int(entry.count),
|
||||
.click = peer ? open : Fn<void()>(),
|
||||
.my = (entry.my == 1),
|
||||
});
|
||||
};
|
||||
|
||||
top.reserve(args.top.size() + 1);
|
||||
for (const auto &entry : args.top) {
|
||||
add(entry);
|
||||
}
|
||||
|
||||
auto myAdded = base::flat_set<uint64>();
|
||||
const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);
|
||||
if (i != end(top)) {
|
||||
myAdded.emplace(i->barePeerId);
|
||||
}
|
||||
const auto myCount = uint32((i != end(top)) ? i->count : 0);
|
||||
const auto myAdd = [&](not_null<PeerData*> peer) {
|
||||
const auto barePeerId = uint64(peer->id.value);
|
||||
if (!myAdded.emplace(barePeerId).second) {
|
||||
return;
|
||||
}
|
||||
add(Data::MessageReactionsTopPaid{
|
||||
.peer = peer,
|
||||
.count = myCount,
|
||||
.my = true,
|
||||
});
|
||||
};
|
||||
myAdd(session->user());
|
||||
ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
|
||||
const auto weak = base::make_weak(box);
|
||||
Ui::PaidReactionsBox(box, {
|
||||
.min = args.min,
|
||||
.chosen = chosen,
|
||||
.max = max,
|
||||
.top = std::move(top),
|
||||
.session = &show->session(),
|
||||
.name = args.name,
|
||||
.submit = std::move(submitText),
|
||||
.colorings = show->session().appConfig().groupCallColorings(),
|
||||
.balanceValue = session->credits().balanceValue(),
|
||||
.send = [=, save = args.save](int count, uint64 barePeerId) {
|
||||
if (!admin) {
|
||||
save(count);
|
||||
}
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
},
|
||||
.videoStreamChoosing = !sending,
|
||||
.videoStreamSending = sending,
|
||||
.videoStreamAdmin = admin,
|
||||
.dark = true,
|
||||
});
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
|
||||
VideoStreamStarsBoxArgs &&args) {
|
||||
return Box(VideoStreamStarsBox, std::move(args));
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
54
Telegram/SourceFiles/calls/group/calls_group_stars_box.h
Normal file
54
Telegram/SourceFiles/calls/group/calls_group_stars_box.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct MessageReactionsTopPaid;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
struct StarsColoring;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
[[nodiscard]] int MaxVideoStreamStarsCount(not_null<Main::Session*> session);
|
||||
|
||||
struct VideoStreamStarsBoxArgs {
|
||||
std::shared_ptr<ChatHelpers::Show> show;
|
||||
std::vector<Data::MessageReactionsTopPaid> top;
|
||||
int min = 0;
|
||||
int current = 0;
|
||||
bool sending = false;
|
||||
bool admin = false;
|
||||
Fn<void(int)> save;
|
||||
QString name;
|
||||
};
|
||||
|
||||
void VideoStreamStarsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
VideoStreamStarsBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
|
||||
VideoStreamStarsBoxArgs &&args);
|
||||
|
||||
} // namespace Calls::Group
|
||||
183
Telegram/SourceFiles/calls/group/calls_group_toasts.cpp
Normal file
183
Telegram/SourceFiles/calls/group/calls_group_toasts.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
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_toasts.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorDuration = 2 * crl::time(1000);
|
||||
|
||||
using State = GroupCall::State;
|
||||
|
||||
} // namespace
|
||||
|
||||
Toasts::Toasts(not_null<Panel*> panel)
|
||||
: _panel(panel)
|
||||
, _call(panel->call()) {
|
||||
setup();
|
||||
}
|
||||
|
||||
void Toasts::setup() {
|
||||
setupJoinAsChanged();
|
||||
setupTitleChanged();
|
||||
setupRequestedToSpeak();
|
||||
setupAllowedToSpeak();
|
||||
setupPinnedVideo();
|
||||
setupError();
|
||||
}
|
||||
|
||||
void Toasts::setupJoinAsChanged() {
|
||||
_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::on_next([=] {
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_join_as_changed_channel
|
||||
: tr::lng_group_call_join_as_changed)(
|
||||
tr::now,
|
||||
lt_name,
|
||||
tr::bold(_call->joinAs()->name()),
|
||||
tr::marked));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupTitleChanged() {
|
||||
_call->titleChanged(
|
||||
) | rpl::filter([=] {
|
||||
return (_call->lookupReal() != nullptr);
|
||||
}) | rpl::map([=] {
|
||||
const auto peer = _call->peer();
|
||||
return peer->groupCall()->title().isEmpty()
|
||||
? peer->name()
|
||||
: peer->groupCall()->title();
|
||||
}) | rpl::on_next([=](const QString &title) {
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_title_changed_channel
|
||||
: tr::lng_group_call_title_changed)(
|
||||
tr::now,
|
||||
lt_title,
|
||||
tr::bold(title),
|
||||
tr::marked));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupAllowedToSpeak() {
|
||||
_call->allowedToSpeakNotifications(
|
||||
) | rpl::on_next([=] {
|
||||
if (_panel->isActive()) {
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_can_speak_here(tr::now));
|
||||
} else {
|
||||
const auto real = _call->lookupReal();
|
||||
const auto name = (real && !real->title().isEmpty())
|
||||
? real->title()
|
||||
: _call->peer()->name();
|
||||
Ui::Toast::Show({
|
||||
.text = tr::lng_group_call_can_speak(
|
||||
tr::now,
|
||||
lt_chat,
|
||||
tr::bold(name),
|
||||
tr::marked),
|
||||
});
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupPinnedVideo() {
|
||||
_call->videoEndpointPinnedValue(
|
||||
) | rpl::map([=](bool pinned) {
|
||||
return pinned
|
||||
? _call->videoEndpointLargeValue()
|
||||
: rpl::single(_call->videoEndpointLarge());
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=] {
|
||||
return (_call->shownVideoTracks().size() > 1);
|
||||
}) | rpl::on_next([=](const VideoEndpoint &endpoint) {
|
||||
const auto pinned = _call->videoEndpointPinned();
|
||||
const auto peer = endpoint.peer;
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
const auto text = [&] {
|
||||
const auto me = (peer == _call->joinAs());
|
||||
const auto camera = (endpoint.type == VideoEndpointType::Camera);
|
||||
if (me) {
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera_me
|
||||
: tr::lng_group_call_unpinned_camera_me)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen_me
|
||||
: tr::lng_group_call_unpinned_screen_me);
|
||||
return key(tr::now);
|
||||
}
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera
|
||||
: tr::lng_group_call_unpinned_camera)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen
|
||||
: tr::lng_group_call_unpinned_screen);
|
||||
return key(tr::now, lt_user, peer->shortName());
|
||||
}();
|
||||
_panel->uiShow()->showToast(text);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupRequestedToSpeak() {
|
||||
_call->mutedValue(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::on_next([=](MuteState was, MuteState now) {
|
||||
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_tooltip_raised_hand(tr::now));
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupError() {
|
||||
_call->errors(
|
||||
) | rpl::on_next([=](Error error) {
|
||||
const auto key = [&] {
|
||||
switch (error) {
|
||||
case Error::NoCamera: return tr::lng_call_error_no_camera;
|
||||
case Error::CameraFailed:
|
||||
return tr::lng_group_call_failed_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::Toasts::setupErrorToasts.");
|
||||
}();
|
||||
_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
38
Telegram/SourceFiles/calls/group/calls_group_toasts.h
Normal file
38
Telegram/SourceFiles/calls/group/calls_group_toasts.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Panel;
|
||||
|
||||
class Toasts final {
|
||||
public:
|
||||
explicit Toasts(not_null<Panel*> panel);
|
||||
|
||||
private:
|
||||
void setup();
|
||||
void setupJoinAsChanged();
|
||||
void setupTitleChanged();
|
||||
void setupRequestedToSpeak();
|
||||
void setupAllowedToSpeak();
|
||||
void setupPinnedVideo();
|
||||
void setupError();
|
||||
|
||||
const not_null<Panel*> _panel;
|
||||
const not_null<GroupCall*> _call;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
1039
Telegram/SourceFiles/calls/group/calls_group_viewport.cpp
Normal file
1039
Telegram/SourceFiles/calls/group/calls_group_viewport.cpp
Normal file
File diff suppressed because it is too large
Load Diff
234
Telegram/SourceFiles/calls/group/calls_group_viewport.h
Normal file
234
Telegram/SourceFiles/calls/group/calls_group_viewport.h
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Painter;
|
||||
class QOpenGLFunctions;
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class RpWidgetWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::GL {
|
||||
enum class Backend;
|
||||
struct Capabilities;
|
||||
struct ChosenRenderer;
|
||||
class Renderer;
|
||||
} // namespace Ui::GL
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct VideoEndpoint;
|
||||
struct VideoQualityRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Webrtc {
|
||||
class VideoTrack;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class MembersRow;
|
||||
enum class PanelMode;
|
||||
enum class VideoQuality;
|
||||
|
||||
struct VideoTileTrack {
|
||||
Webrtc::VideoTrack *track = nullptr;
|
||||
MembersRow *row = nullptr;
|
||||
rpl::variable<QSize> trackSize;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return track != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool operator==(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return (a.track == b.track) && (a.row == b.row);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool operator!=(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
class Viewport final {
|
||||
public:
|
||||
Viewport(
|
||||
not_null<QWidget*> parent,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend,
|
||||
Ui::RpWidgetWrap *borrowedRp = nullptr,
|
||||
bool borrowedOpenGL = false);
|
||||
~Viewport();
|
||||
|
||||
[[nodiscard]] not_null<QWidget*> widget() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
|
||||
|
||||
void setMode(PanelMode mode, not_null<QWidget*> parent);
|
||||
void setControlsShown(float64 shown);
|
||||
void setCursorShown(bool shown);
|
||||
void setGeometry(bool fullscreen, QRect geometry);
|
||||
void resizeToWidth(int newWidth);
|
||||
void setScrollTop(int scrollTop);
|
||||
|
||||
void add(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
bool self);
|
||||
void remove(const VideoEndpoint &endpoint);
|
||||
void showLarge(const VideoEndpoint &endpoint);
|
||||
|
||||
[[nodiscard]] bool requireARGB32() const;
|
||||
[[nodiscard]] int fullHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> fullHeightValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> pinToggled() const;
|
||||
[[nodiscard]] rpl::producer<VideoEndpoint> clicks() const;
|
||||
[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;
|
||||
|
||||
void ensureBorrowedRenderer(QOpenGLFunctions &f);
|
||||
void ensureBorrowedCleared(QOpenGLFunctions *f);
|
||||
void borrowedPaint(QOpenGLFunctions &f);
|
||||
|
||||
void ensureBorrowedRenderer();
|
||||
void ensureBorrowedCleared();
|
||||
void borrowedPaint(Painter &p, const QRegion &clip);
|
||||
|
||||
[[nodiscard]] QPoint borrowedOrigin() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
static constexpr auto kShadowMaxAlpha = 80;
|
||||
|
||||
private:
|
||||
struct Textures;
|
||||
class VideoTile;
|
||||
class RendererSW;
|
||||
class RendererGL;
|
||||
using TileId = quintptr;
|
||||
|
||||
struct Geometry {
|
||||
VideoTile *tile = nullptr;
|
||||
QSize size;
|
||||
QRect rows;
|
||||
QRect columns;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
std::vector<Geometry> list;
|
||||
QSize outer;
|
||||
bool useColumns = false;
|
||||
};
|
||||
|
||||
struct TileAnimation {
|
||||
QSize from;
|
||||
QSize to;
|
||||
float64 ratio = -1.;
|
||||
};
|
||||
|
||||
struct Selection {
|
||||
enum class Element {
|
||||
None,
|
||||
Tile,
|
||||
PinButton,
|
||||
BackButton,
|
||||
};
|
||||
VideoTile *tile = nullptr;
|
||||
Element element = Element::None;
|
||||
|
||||
inline bool operator==(Selection other) const {
|
||||
return (tile == other.tile) && (element == other.element);
|
||||
}
|
||||
};
|
||||
|
||||
void setup();
|
||||
[[nodiscard]] bool wide() const;
|
||||
[[nodiscard]] bool videoStream() const;
|
||||
|
||||
void updateCursor();
|
||||
void updateTilesGeometry();
|
||||
void updateTilesGeometry(int outerWidth);
|
||||
void updateTilesGeometryWide(int outerWidth, int outerHeight);
|
||||
void updateTilesGeometryNarrow(int outerWidth);
|
||||
void updateTilesGeometryColumn(int outerWidth);
|
||||
void setTileGeometry(not_null<VideoTile*> tile, QRect geometry);
|
||||
void refreshHasTwoOrMore();
|
||||
void updateTopControlsVisibility();
|
||||
|
||||
void prepareLargeChangeAnimation();
|
||||
void startLargeChangeAnimation();
|
||||
void updateTilesAnimated();
|
||||
[[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const;
|
||||
[[nodiscard]] Layout applyLarge(Layout layout) const;
|
||||
|
||||
void setSelected(Selection value);
|
||||
void setPressed(Selection value);
|
||||
|
||||
void handleMousePress(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseRelease(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseMove(QPoint position);
|
||||
void updateSelected(QPoint position);
|
||||
void updateSelected();
|
||||
|
||||
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
|
||||
Ui::GL::Backend backend);
|
||||
[[nodiscard]] std::unique_ptr<Ui::GL::Renderer> makeRenderer();
|
||||
void updateMyWidgetPart();
|
||||
|
||||
PanelMode _mode = PanelMode();
|
||||
bool _opengl = false;
|
||||
const std::unique_ptr<Ui::RpWidgetWrap> _content;
|
||||
std::vector<std::unique_ptr<VideoTile>> _tiles;
|
||||
std::vector<not_null<VideoTile*>> _tilesForOrder;
|
||||
rpl::variable<int> _fullHeight = 0;
|
||||
bool _hasTwoOrMore = false;
|
||||
bool _fullscreen = false;
|
||||
bool _cursorHidden = false;
|
||||
int _scrollTop = 0;
|
||||
QImage _shadow;
|
||||
rpl::event_stream<VideoEndpoint> _clicks;
|
||||
rpl::event_stream<bool> _pinToggles;
|
||||
rpl::event_stream<VideoQualityRequest> _qualityRequests;
|
||||
float64 _controlsShownRatio = 1.;
|
||||
VideoTile *_large = nullptr;
|
||||
Fn<void()> _updateLargeScheduled;
|
||||
Ui::Animations::Simple _largeChangeAnimation;
|
||||
Layout _startTilesLayout;
|
||||
Layout _finishTilesLayout;
|
||||
Selection _selected;
|
||||
Selection _pressed;
|
||||
rpl::variable<bool> _mouseInside = false;
|
||||
|
||||
Ui::RpWidgetWrap * const _borrowed = nullptr;
|
||||
QRect _borrowedGeometry;
|
||||
std::unique_ptr<Ui::GL::Renderer> _borrowedRenderer;
|
||||
QMetaObject::Connection _borrowedConnection;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage GenerateShadow(
|
||||
int height,
|
||||
int topAlpha,
|
||||
int bottomAlpha,
|
||||
QColor color = QColor(0, 0, 0));
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> MuteButtonTooltip(
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
} // namespace Calls::Group
|
||||
1482
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
Normal file
1482
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
Normal file
File diff suppressed because it is too large
Load Diff
165
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h
Normal file
165
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/gl/gl_primitives.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_image.h"
|
||||
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLShaderProgram>
|
||||
|
||||
namespace Webrtc {
|
||||
struct FrameWithInfo;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::RendererGL final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererGL(not_null<Viewport*> owner);
|
||||
|
||||
void init(QOpenGLFunctions &f) override;
|
||||
|
||||
void deinit(QOpenGLFunctions *f) override;
|
||||
|
||||
void paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
std::optional<QColor> clearColor() override;
|
||||
|
||||
private:
|
||||
struct TileData {
|
||||
quintptr id = 0;
|
||||
not_null<PeerData*> peer;
|
||||
Ui::GL::Textures<5> textures;
|
||||
Ui::GL::Framebuffers<2> framebuffers;
|
||||
Ui::Animations::Simple outlined;
|
||||
Ui::Animations::Simple paused;
|
||||
QImage userpicFrame;
|
||||
QRect nameRect;
|
||||
int nameVersion = 0;
|
||||
mutable int trackIndex = -1;
|
||||
mutable QSize rgbaSize;
|
||||
mutable QSize textureSize;
|
||||
mutable QSize textureChromaSize;
|
||||
mutable QSize textureBlurSize;
|
||||
bool stale = false;
|
||||
bool pause = false;
|
||||
bool outline = false;
|
||||
};
|
||||
struct Program {
|
||||
std::optional<QOpenGLShaderProgram> argb32;
|
||||
std::optional<QOpenGLShaderProgram> yuv420;
|
||||
};
|
||||
|
||||
void setDefaultViewport(QOpenGLFunctions &f);
|
||||
void paintTile(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject,
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &nameData);
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(
|
||||
const Ui::GL::Rect &raster) const;
|
||||
|
||||
void ensureARGB32Program();
|
||||
void ensureButtonsImage();
|
||||
void prepareObjects(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void bindFrame(
|
||||
QOpenGLFunctions &f,
|
||||
const Webrtc::FrameWithInfo &data,
|
||||
TileData &tileData,
|
||||
Program &program);
|
||||
void drawDownscalePass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData);
|
||||
void drawFirstBlurPass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void validateDatas();
|
||||
void validateNoiseTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject);
|
||||
void validateOutlineAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validatePausedAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &tileData);
|
||||
|
||||
void uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const;
|
||||
|
||||
[[nodiscard]] bool isExpanded(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
QSize tileSize) const;
|
||||
[[nodiscard]] float64 countExpandRatio(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
const TileAnimation &animation) const;
|
||||
|
||||
const not_null<Viewport*> _owner;
|
||||
|
||||
GLfloat _factor = 1.;
|
||||
int _ifactor = 1;
|
||||
QSize _viewport;
|
||||
bool _rgbaFrame = false;
|
||||
bool _userpicFrame;
|
||||
std::optional<QOpenGLBuffer> _frameBuffer;
|
||||
Program _downscaleProgram;
|
||||
std::optional<QOpenGLShaderProgram> _blurProgram;
|
||||
Program _frameProgram;
|
||||
std::optional<QOpenGLShaderProgram> _imageProgram;
|
||||
Ui::GL::Textures<1> _noiseTexture;
|
||||
Ui::GL::Framebuffers<1> _noiseFramebuffer;
|
||||
QOpenGLShader *_downscaleVertexShader = nullptr;
|
||||
QOpenGLShader *_frameVertexShader = nullptr;
|
||||
|
||||
Ui::GL::Image _buttons;
|
||||
QRect _pinOn;
|
||||
QRect _pinOff;
|
||||
QRect _back;
|
||||
QRect _muteOn;
|
||||
QRect _muteOff;
|
||||
QRect _paused;
|
||||
|
||||
Ui::GL::Image _names;
|
||||
QRect _pausedTextRect;
|
||||
std::vector<TileData> _tileData;
|
||||
std::vector<int> _tileDataIndices;
|
||||
|
||||
Ui::CrossLineAnimation _pinIcon;
|
||||
Ui::CrossLineAnimation _muteIcon;
|
||||
|
||||
Ui::RoundRect _pinBackground;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
347
Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
Normal file
347
Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
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_viewport_raster.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_viewport_tile.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBlurRadius = 15;
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::RendererSW::RendererSW(not_null<Viewport*> owner)
|
||||
: _owner(owner)
|
||||
, _pinIcon(st::groupCallVideoTile.pin)
|
||||
, _pinBackground(
|
||||
(st::groupCallVideoTile.pinPadding.top()
|
||||
+ st::groupCallVideoTile.pin.icon.height()
|
||||
+ st::groupCallVideoTile.pinPadding.bottom()) / 2,
|
||||
st::radialBg) {
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) {
|
||||
auto bg = clip;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto bounding = clip.boundingRect();
|
||||
for (auto &[tile, tileData] : _tileData) {
|
||||
tileData.stale = true;
|
||||
}
|
||||
for (const auto &tile : _owner->_tiles) {
|
||||
if (!tile->visible()) {
|
||||
continue;
|
||||
}
|
||||
paintTile(p, tile.get(), bounding, bg);
|
||||
}
|
||||
if (_owner->borrowedOrigin().isNull()) {
|
||||
const auto fullscreen = _owner->_fullscreen;
|
||||
const auto color = fullscreen
|
||||
? QColor(0, 0, 0)
|
||||
: st::groupCallBg->c;
|
||||
for (const auto &rect : bg) {
|
||||
p.fillRect(rect, color);
|
||||
}
|
||||
}
|
||||
for (auto i = _tileData.begin(); i != _tileData.end();) {
|
||||
if (i->second.stale) {
|
||||
i = _tileData.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data) {
|
||||
if (!_userpicFrame) {
|
||||
data.userpicFrame = QImage();
|
||||
return;
|
||||
} else if (!data.userpicFrame.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto size = tile->trackOrUserpicSize();
|
||||
data.userpicFrame = Images::BlurLargeImage(
|
||||
PeerData::GenerateUserpicImage(
|
||||
tile->peer(),
|
||||
tile->row()->ensureUserpicView(),
|
||||
size.width(),
|
||||
0),
|
||||
kBlurRadius);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTile(
|
||||
Painter &p,
|
||||
not_null<VideoTile*> tile,
|
||||
const QRect &clip,
|
||||
QRegion &bg) {
|
||||
const auto track = tile->track();
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
tile->track()->markFrameShown();
|
||||
});
|
||||
const auto data = track->frameWithInfo(true);
|
||||
auto &tileData = _tileData[tile];
|
||||
tileData.stale = false;
|
||||
_userpicFrame = (data.format == Webrtc::FrameFormat::None);
|
||||
_pausedFrame = (track->state() == Webrtc::VideoState::Paused);
|
||||
validateUserpicFrame(tile, tileData);
|
||||
if (_userpicFrame || !_pausedFrame) {
|
||||
tileData.blurredFrame = QImage();
|
||||
} else if (tileData.blurredFrame.isNull()) {
|
||||
tileData.blurredFrame = Images::BlurLargeImage(
|
||||
data.original.scaled(
|
||||
VideoTile::PausedVideoSize(),
|
||||
Qt::KeepAspectRatio).mirrored(tile->mirror(), false),
|
||||
kBlurRadius);
|
||||
}
|
||||
const auto &image = _userpicFrame
|
||||
? tileData.userpicFrame
|
||||
: _pausedFrame
|
||||
? tileData.blurredFrame
|
||||
: data.original.mirrored(tile->mirror(), false);
|
||||
const auto frameRotation = _userpicFrame ? 0 : data.rotation;
|
||||
Assert(!image.isNull());
|
||||
|
||||
const auto background = _owner->_fullscreen
|
||||
? QColor(0, 0, 0)
|
||||
: st::groupCallMembersBg->c;
|
||||
const auto fill = [&](QRect rect) {
|
||||
const auto intersected = rect.intersected(clip);
|
||||
if (!intersected.isEmpty()) {
|
||||
p.fillRect(intersected, background);
|
||||
bg -= intersected;
|
||||
}
|
||||
};
|
||||
|
||||
using namespace Media::View;
|
||||
const auto geometry = tile->geometry().translated(
|
||||
_owner->borrowedOrigin());
|
||||
const auto x = geometry.x();
|
||||
const auto y = geometry.y();
|
||||
const auto width = geometry.width();
|
||||
const auto height = geometry.height();
|
||||
const auto scaled = FlipSizeByRotation(
|
||||
image.size(),
|
||||
frameRotation
|
||||
).scaled(QSize(width, height), Qt::KeepAspectRatio);
|
||||
const auto left = (width - scaled.width()) / 2;
|
||||
const auto top = (height - scaled.height()) / 2;
|
||||
const auto target = QRect(QPoint(x + left, y + top), scaled);
|
||||
if (UsePainterRotation(frameRotation)) {
|
||||
if (frameRotation) {
|
||||
p.save();
|
||||
p.rotate(frameRotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(target, frameRotation), image);
|
||||
if (frameRotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (frameRotation) {
|
||||
p.drawImage(target, RotateFrameImage(image, frameRotation));
|
||||
} else {
|
||||
p.drawImage(target, image);
|
||||
}
|
||||
bg -= target;
|
||||
|
||||
if (left > 0) {
|
||||
fill({ x, y, left, height });
|
||||
}
|
||||
if (const auto right = left + scaled.width(); right < width) {
|
||||
fill({ x + right, y, width - right, height });
|
||||
}
|
||||
if (top > 0) {
|
||||
fill({ x, y, width, top });
|
||||
}
|
||||
if (const auto bottom = top + scaled.height(); bottom < height) {
|
||||
fill({ x, y + bottom, width, height - bottom });
|
||||
}
|
||||
|
||||
paintTileControls(p, x, y, width, height, tile);
|
||||
paintTileOutline(p, x, y, width, height, tile);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileOutline(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
if (!tile->row()->speaking()) {
|
||||
return;
|
||||
}
|
||||
const auto outline = st::groupCallOutline;
|
||||
const auto &color = st::groupCallMemberActiveIcon;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.fillRect(x, y, outline, height - outline, color);
|
||||
p.fillRect(x + outline, y, width - outline, outline, color);
|
||||
p.fillRect(
|
||||
x + width - outline,
|
||||
y + outline,
|
||||
outline,
|
||||
height - outline,
|
||||
color);
|
||||
p.fillRect(x, y + height - outline, width - outline, outline, color);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileControls(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
p.setClipRect(x, y, width, height);
|
||||
const auto guard = gsl::finally([&] { p.setClipping(false); });
|
||||
|
||||
const auto wide = _owner->wide();
|
||||
if (wide) {
|
||||
// Pin.
|
||||
const auto pinInner = tile->pinInner();
|
||||
VideoTile::PaintPinButton(
|
||||
p,
|
||||
tile->pinned(),
|
||||
x + pinInner.x(),
|
||||
y + pinInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground,
|
||||
&_pinIcon);
|
||||
|
||||
// Back.
|
||||
const auto backInner = tile->backInner();
|
||||
VideoTile::PaintBackButton(
|
||||
p,
|
||||
x + backInner.x(),
|
||||
y + backInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground);
|
||||
}
|
||||
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto nameTop = y + (height
|
||||
- st.namePosition.y()
|
||||
- st::semiboldFont->height);
|
||||
|
||||
if (_pausedFrame) {
|
||||
p.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha));
|
||||
|
||||
const auto middle = (st::groupCallVideoPlaceholderHeight
|
||||
- st::groupCallPaused.height()) / 2;
|
||||
const auto pausedSpace = (nameTop - y)
|
||||
- st::groupCallPaused.height()
|
||||
- st::semiboldFont->height;
|
||||
const auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop;
|
||||
const auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop
|
||||
- st::groupCallVideoPlaceholderIconTop;
|
||||
const auto pauseIconTop = !_owner->wide()
|
||||
? (y + (height - st::groupCallPaused.height()) / 2)
|
||||
: (pausedSpace < 3 * st::semiboldFont->height)
|
||||
? (pausedSpace / 3)
|
||||
: std::min(
|
||||
y + (height / 2) - pauseIconSkip,
|
||||
(nameTop
|
||||
- st::semiboldFont->height * 3
|
||||
- st::groupCallPaused.height()));
|
||||
const auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height)
|
||||
? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)
|
||||
: std::min(
|
||||
pauseIconTop + pauseTextSkip,
|
||||
nameTop - st::semiboldFont->height * 2);
|
||||
|
||||
st::groupCallPaused.paint(
|
||||
p,
|
||||
x + (width - st::groupCallPaused.width()) / 2,
|
||||
pauseIconTop,
|
||||
width);
|
||||
if (_owner->wide()) {
|
||||
p.drawText(
|
||||
QRect(x, pauseTextTop, width, y + height - pauseTextTop),
|
||||
tr::lng_group_call_video_paused(tr::now),
|
||||
style::al_top);
|
||||
}
|
||||
}
|
||||
|
||||
const auto shown = _owner->_controlsShownRatio;
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fullShift = st.namePosition.y() + st::normalFont->height;
|
||||
const auto shift = anim::interpolate(fullShift, 0, shown);
|
||||
|
||||
// Shadow.
|
||||
if (_shadow.isNull()) {
|
||||
_shadow = Images::GenerateShadow(
|
||||
st.shadowHeight,
|
||||
0,
|
||||
kShadowMaxAlpha);
|
||||
}
|
||||
const auto shadowRect = QRect(
|
||||
x,
|
||||
y + (height - anim::interpolate(0, st.shadowHeight, shown)),
|
||||
width,
|
||||
st.shadowHeight);
|
||||
const auto shadowFill = shadowRect.intersected({ x, y, width, height });
|
||||
if (shadowFill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
if (!_pausedFrame) {
|
||||
p.drawImage(
|
||||
shadowFill,
|
||||
_shadow,
|
||||
QRect(
|
||||
0,
|
||||
(shadowFill.y() - shadowRect.y()) * factor,
|
||||
_shadow.width(),
|
||||
shadowFill.height() * factor));
|
||||
}
|
||||
const auto row = tile->row();
|
||||
row->lazyInitialize(st::groupCallMembersListItem);
|
||||
|
||||
// Mute.
|
||||
const auto &icon = st::groupCallVideoCrossLine.icon;
|
||||
const auto iconLeft = x + width - st.iconPosition.x() - icon.width();
|
||||
const auto iconTop = y + (height
|
||||
- st.iconPosition.y()
|
||||
- icon.height()
|
||||
+ shift);
|
||||
row->paintMuteIcon(
|
||||
p,
|
||||
{ iconLeft, iconTop, icon.width(), icon.height() },
|
||||
MembersRowStyle::Video);
|
||||
|
||||
// Name.
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
const auto hasWidth = width
|
||||
- st.iconPosition.x() - icon.width()
|
||||
- st.namePosition.x();
|
||||
const auto nameLeft = x + st.namePosition.x();
|
||||
row->name().drawLeftElided(
|
||||
p,
|
||||
nameLeft,
|
||||
nameTop + shift,
|
||||
hasWidth,
|
||||
width);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::RendererSW final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererSW(not_null<Viewport*> owner);
|
||||
|
||||
void paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) override;
|
||||
|
||||
private:
|
||||
struct TileData {
|
||||
QImage userpicFrame;
|
||||
QImage blurredFrame;
|
||||
bool stale = false;
|
||||
};
|
||||
void paintTile(
|
||||
Painter &p,
|
||||
not_null<VideoTile*> tile,
|
||||
const QRect &clip,
|
||||
QRegion &bg);
|
||||
void paintTileOutline(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile);
|
||||
void paintTileControls(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile);
|
||||
void validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
|
||||
const not_null<Viewport*> _owner;
|
||||
|
||||
QImage _shadow;
|
||||
bool _userpicFrame = false;
|
||||
bool _pausedFrame = false;
|
||||
base::flat_map<not_null<VideoTile*>, TileData> _tileData;
|
||||
Ui::CrossLineAnimation _pinIcon;
|
||||
Ui::RoundRect _pinBackground;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
278
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp
Normal file
278
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
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_viewport_tile.h"
|
||||
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtGui/QOpenGLFunctions>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPausedVideoSize = 90;
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::VideoTile::VideoTile(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
Fn<void()> update,
|
||||
bool self)
|
||||
: _endpoint(endpoint)
|
||||
, _update(std::move(update))
|
||||
, _track(std::move(track))
|
||||
, _peer(_track.row->peer())
|
||||
, _trackSize(std::move(trackSize))
|
||||
, _rtmp(endpoint.rtmp())
|
||||
, _self(self) {
|
||||
Expects(_track.track != nullptr);
|
||||
|
||||
using namespace rpl::mappers;
|
||||
_track.track->stateValue(
|
||||
) | rpl::filter(
|
||||
_1 == Webrtc::VideoState::Paused
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
_wasPaused = true;
|
||||
}, _lifetime);
|
||||
|
||||
setup(std::move(pinned));
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::mirror() const {
|
||||
return _self && (_endpoint.type == VideoEndpointType::Camera);
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinOuter() const {
|
||||
return _pinOuter;
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinInner() const {
|
||||
return _pinInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backOuter() const {
|
||||
return _backOuter;
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backInner() const {
|
||||
return _backInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
int Viewport::VideoTile::topControlsSlide() const {
|
||||
return anim::interpolate(
|
||||
st::groupCallVideoTile.pinPosition.y() + _pinInner.height(),
|
||||
0,
|
||||
_topControlsShownAnimation.value(_topControlsShown ? 1. : 0.));
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::PausedVideoSize() {
|
||||
return QSize(kPausedVideoSize, kPausedVideoSize);
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::trackOrUserpicSize() const {
|
||||
if (const auto size = trackSize(); !size.isEmpty()) {
|
||||
return size;
|
||||
}
|
||||
return _wasPaused ? PausedVideoSize() : QSize();
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::screencast() const {
|
||||
return (_endpoint.type == VideoEndpointType::Screen);
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::setGeometry(
|
||||
QRect geometry,
|
||||
TileAnimation animation) {
|
||||
_hidden = false;
|
||||
_geometry = geometry;
|
||||
_animation = animation;
|
||||
updateTopControlsPosition();
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::hide() {
|
||||
_hidden = true;
|
||||
_quality = std::nullopt;
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::toggleTopControlsShown(bool shown) {
|
||||
if (_topControlsShown == shown) {
|
||||
return;
|
||||
}
|
||||
_topControlsShown = shown;
|
||||
_topControlsShownAnimation.start(
|
||||
_update,
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration);
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) {
|
||||
if (_hidden) {
|
||||
_quality = std::nullopt;
|
||||
return false;
|
||||
} else if (_quality && *_quality == quality) {
|
||||
return false;
|
||||
}
|
||||
_quality = quality;
|
||||
return true;
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::PinInnerSize(bool pinned) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto &icon = st::groupCallVideoTile.pin.icon;
|
||||
const auto innerWidth = icon.width()
|
||||
+ st.pinTextPosition.x()
|
||||
+ st::semiboldFont->width(pinned
|
||||
? tr::lng_pinned_unpin(tr::now)
|
||||
: tr::lng_pinned_pin(tr::now));
|
||||
const auto innerHeight = icon.height();
|
||||
const auto buttonWidth = st.pinPadding.left()
|
||||
+ innerWidth
|
||||
+ st.pinPadding.right();
|
||||
const auto buttonHeight = st.pinPadding.top()
|
||||
+ innerHeight
|
||||
+ st.pinPadding.bottom();
|
||||
return { buttonWidth, buttonHeight };
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::PaintPinButton(
|
||||
Painter &p,
|
||||
bool pinned,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background,
|
||||
not_null<Ui::CrossLineAnimation*> icon) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto rect = QRect(QPoint(x, y), PinInnerSize(pinned));
|
||||
background->paint(p, rect);
|
||||
icon->paint(
|
||||
p,
|
||||
rect.marginsRemoved(st.pinPadding).topLeft(),
|
||||
pinned ? 1. : 0.);
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
(x
|
||||
+ st.pinPadding.left()
|
||||
+ st::groupCallVideoTile.pin.icon.width()
|
||||
+ st.pinTextPosition.x()),
|
||||
(y
|
||||
+ st.pinPadding.top()
|
||||
+ st.pinTextPosition.y()),
|
||||
outerWidth,
|
||||
(pinned
|
||||
? tr::lng_pinned_unpin(tr::now)
|
||||
: tr::lng_pinned_pin(tr::now)));
|
||||
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::BackInnerSize() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto &icon = st::groupCallVideoTile.back;
|
||||
const auto innerWidth = icon.width()
|
||||
+ st.pinTextPosition.x()
|
||||
+ st::semiboldFont->width(tr::lng_create_group_back(tr::now));
|
||||
const auto innerHeight = icon.height();
|
||||
const auto buttonWidth = st.pinPadding.left()
|
||||
+ innerWidth
|
||||
+ st.pinPadding.right();
|
||||
const auto buttonHeight = st.pinPadding.top()
|
||||
+ innerHeight
|
||||
+ st.pinPadding.bottom();
|
||||
return { buttonWidth, buttonHeight };
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto rect = QRect(QPoint(x, y), BackInnerSize());
|
||||
background->paint(p, rect);
|
||||
st.back.paint(
|
||||
p,
|
||||
rect.marginsRemoved(st.pinPadding).topLeft(),
|
||||
outerWidth);
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
(x
|
||||
+ st.pinPadding.left()
|
||||
+ st::groupCallVideoTile.pin.icon.width()
|
||||
+ st.pinTextPosition.x()),
|
||||
(y
|
||||
+ st.pinPadding.top()
|
||||
+ st.pinTextPosition.y()),
|
||||
outerWidth,
|
||||
tr::lng_create_group_back(tr::now));
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updateTopControlsSize() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
|
||||
const auto pinSize = PinInnerSize(_pinned);
|
||||
const auto pinWidth = st.pinPosition.x() * 2 + pinSize.width();
|
||||
const auto pinHeight = st.pinPosition.y() * 2 + pinSize.height();
|
||||
_pinInner = QRect(QPoint(), pinSize);
|
||||
_pinOuter = QRect(0, 0, pinWidth, pinHeight);
|
||||
|
||||
const auto backSize = BackInnerSize();
|
||||
const auto backWidth = st.pinPosition.x() * 2 + backSize.width();
|
||||
const auto backHeight = st.pinPosition.y() * 2 + backSize.height();
|
||||
_backInner = QRect(QPoint(), backSize);
|
||||
_backOuter = QRect(0, 0, backWidth, backHeight);
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updateTopControlsPosition() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
|
||||
_pinInner = QRect(
|
||||
_geometry.width() - st.pinPosition.x() - _pinInner.width(),
|
||||
st.pinPosition.y(),
|
||||
_pinInner.width(),
|
||||
_pinInner.height());
|
||||
_pinOuter = QRect(
|
||||
_geometry.width() - _pinOuter.width(),
|
||||
0,
|
||||
_pinOuter.width(),
|
||||
_pinOuter.height());
|
||||
_backInner = QRect(st.pinPosition, _backInner.size());
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::setup(rpl::producer<bool> pinned) {
|
||||
std::move(
|
||||
pinned
|
||||
) | rpl::filter([=](bool pinned) {
|
||||
return (_pinned != pinned);
|
||||
}) | rpl::on_next([=](bool pinned) {
|
||||
_pinned = pinned;
|
||||
updateTopControlsSize();
|
||||
if (!_hidden) {
|
||||
updateTopControlsPosition();
|
||||
_update();
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_track.track->renderNextFrame(
|
||||
) | rpl::on_next(_update, _lifetime);
|
||||
|
||||
updateTopControlsSize();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
142
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h
Normal file
142
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Painter;
|
||||
class QOpenGLFunctions;
|
||||
|
||||
namespace Ui {
|
||||
class CrossLineAnimation;
|
||||
class RoundRect;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::VideoTile final {
|
||||
public:
|
||||
VideoTile(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
Fn<void()> update,
|
||||
bool self);
|
||||
|
||||
[[nodiscard]] not_null<Webrtc::VideoTrack*> track() const {
|
||||
return _track.track;
|
||||
}
|
||||
[[nodiscard]] not_null<MembersRow*> row() const {
|
||||
return _track.row;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] bool rtmp() const {
|
||||
return _rtmp;
|
||||
}
|
||||
[[nodiscard]] QRect geometry() const {
|
||||
return _geometry;
|
||||
}
|
||||
[[nodiscard]] TileAnimation animation() const {
|
||||
return _animation;
|
||||
}
|
||||
[[nodiscard]] bool pinned() const {
|
||||
return _pinned;
|
||||
}
|
||||
[[nodiscard]] bool hidden() const {
|
||||
return _hidden;
|
||||
}
|
||||
[[nodiscard]] bool visible() const {
|
||||
return !_hidden && !_geometry.isEmpty();
|
||||
}
|
||||
[[nodiscard]] bool self() const {
|
||||
return _self;
|
||||
}
|
||||
[[nodiscard]] bool mirror() const;
|
||||
[[nodiscard]] QRect pinOuter() const;
|
||||
[[nodiscard]] QRect pinInner() const;
|
||||
[[nodiscard]] QRect backOuter() const;
|
||||
[[nodiscard]] QRect backInner() const;
|
||||
[[nodiscard]] const VideoEndpoint &endpoint() const {
|
||||
return _endpoint;
|
||||
}
|
||||
[[nodiscard]] QSize trackSize() const {
|
||||
return _trackSize.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<QSize> trackSizeValue() const {
|
||||
return _trackSize.value();
|
||||
}
|
||||
[[nodiscard]] QSize trackOrUserpicSize() const;
|
||||
[[nodiscard]] static QSize PausedVideoSize();
|
||||
|
||||
[[nodiscard]] bool screencast() const;
|
||||
void setGeometry(
|
||||
QRect geometry,
|
||||
TileAnimation animation = TileAnimation());
|
||||
void hide();
|
||||
void toggleTopControlsShown(bool shown);
|
||||
bool updateRequestedQuality(VideoQuality quality);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
[[nodiscard]] static QSize PinInnerSize(bool pinned);
|
||||
static void PaintPinButton(
|
||||
Painter &p,
|
||||
bool pinned,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background,
|
||||
not_null<Ui::CrossLineAnimation*> icon);
|
||||
|
||||
[[nodiscard]] static QSize BackInnerSize();
|
||||
static void PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background);
|
||||
|
||||
private:
|
||||
void setup(rpl::producer<bool> pinned);
|
||||
[[nodiscard]] int topControlsSlide() const;
|
||||
void updateTopControlsSize();
|
||||
void updateTopControlsPosition();
|
||||
|
||||
const VideoEndpoint _endpoint;
|
||||
const Fn<void()> _update;
|
||||
const VideoTileTrack _track;
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
QRect _geometry;
|
||||
TileAnimation _animation;
|
||||
rpl::variable<QSize> _trackSize;
|
||||
QRect _pinOuter;
|
||||
QRect _pinInner;
|
||||
QRect _backOuter;
|
||||
QRect _backInner;
|
||||
Ui::Animations::Simple _topControlsShownAnimation;
|
||||
bool _wasPaused = false;
|
||||
bool _topControlsShown = false;
|
||||
bool _pinned = false;
|
||||
bool _hidden = true;
|
||||
bool _rtmp = false;
|
||||
bool _self = false;
|
||||
std::optional<VideoQuality> _quality;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
301
Telegram/SourceFiles/calls/group/calls_volume_item.cpp
Normal file
301
Telegram/SourceFiles/calls/group/calls_volume_item.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
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_volume_item.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "ui/color_int_conversion.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include "ui/paint/arcs.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxVolumePercent = 200;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
10.0f / kMaxVolumePercent,
|
||||
50.0f / kMaxVolumePercent,
|
||||
150.0f / kMaxVolumePercent };
|
||||
|
||||
constexpr auto kVolumeStickedValues
|
||||
= std::array<std::pair<float64, float64>, 7>{{
|
||||
{ 25. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 50. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 75. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 100. / kMaxVolumePercent, 10. / kMaxVolumePercent },
|
||||
{ 125. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 150. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
MenuVolumeItem::MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MediaSlider &stSlider,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted,
|
||||
const QMargins &padding)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _maxVolume(maxVolume)
|
||||
, _cloudMuted(muted)
|
||||
, _localMuted(muted)
|
||||
, _slider(base::make_unique_q<Ui::MediaSlider>(this, stSlider))
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _stCross(st::groupCallMuteCrossLine)
|
||||
, _padding(padding)
|
||||
, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))
|
||||
, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
|
||||
st::groupCallSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
_localMuted ? 0. : (startVolume / float(maxVolume)),
|
||||
Ui::Paint::ArcsAnimation::Direction::Right)) {
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_slider.get());
|
||||
|
||||
_slider->setAlwaysDisplayMarker(true);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
const auto geometry = QRect(QPoint(), size);
|
||||
_itemRect = geometry - _padding;
|
||||
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
|
||||
_arcPosition = _speakerRect.center()
|
||||
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
|
||||
_slider->setGeometry(
|
||||
st::groupCallMenuVolumeMargin.left(),
|
||||
_speakerRect.y(),
|
||||
(geometry.width()
|
||||
- st::groupCallMenuVolumeMargin.left()
|
||||
- st::groupCallMenuVolumeMargin.right()),
|
||||
_speakerRect.height());
|
||||
}, lifetime());
|
||||
|
||||
setCloudVolume(startVolume);
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](const QRect &clip) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto volume = _localMuted
|
||||
? 0
|
||||
: base::SafeRound(_slider->value() * kMaxVolumePercent);
|
||||
const auto muteProgress
|
||||
= _crossLineAnimation.value((!volume) ? 1. : 0.);
|
||||
|
||||
const auto selected = isSelected();
|
||||
p.fillRect(clip, selected ? st.itemBgOver : st.itemBg);
|
||||
|
||||
const auto mutePen = anim::color(
|
||||
unmuteColor(),
|
||||
muteColor(),
|
||||
muteProgress);
|
||||
|
||||
_crossLineMute->paint(
|
||||
p,
|
||||
_speakerRect.topLeft(),
|
||||
muteProgress,
|
||||
(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
|
||||
|
||||
{
|
||||
p.translate(_arcPosition);
|
||||
_arcs->paint(p);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_slider->setChangeProgressCallback([=](float64 value) {
|
||||
const auto newMuted = (value == 0);
|
||||
if (_localMuted != newMuted) {
|
||||
_localMuted = newMuted;
|
||||
_toggleMuteLocallyRequests.fire_copy(newMuted);
|
||||
|
||||
_crossLineAnimation.start(
|
||||
[=] { update(_speakerRect); },
|
||||
_localMuted ? 0. : 1.,
|
||||
_localMuted ? 1. : 0.,
|
||||
st::callPanelDuration);
|
||||
}
|
||||
if (value > 0) {
|
||||
_changeVolumeLocallyRequests.fire(value * _maxVolume);
|
||||
}
|
||||
_arcs->setValue(value);
|
||||
updateSliderColor(value);
|
||||
});
|
||||
|
||||
const auto returnVolume = [=] {
|
||||
_changeVolumeLocallyRequests.fire_copy(_cloudVolume);
|
||||
};
|
||||
|
||||
_slider->setChangeFinishedCallback([=](float64 value) {
|
||||
const auto newVolume = base::SafeRound(value * _maxVolume);
|
||||
const auto muted = (value == 0);
|
||||
|
||||
if (!_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
_localMuted = true;
|
||||
_toggleMuteRequests.fire(true);
|
||||
}
|
||||
if (_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
}
|
||||
if (_cloudMuted && !muted) {
|
||||
_waitingForUpdateVolume = true;
|
||||
_localMuted = false;
|
||||
_toggleMuteRequests.fire(false);
|
||||
}
|
||||
if (!_cloudMuted && !muted) {
|
||||
_changeVolumeRequests.fire_copy(newVolume);
|
||||
}
|
||||
updateSliderColor(value);
|
||||
});
|
||||
|
||||
std::move(
|
||||
participantState
|
||||
) | rpl::on_next([=](const Group::ParticipantState &state) {
|
||||
const auto newMuted = state.mutedByMe;
|
||||
const auto newVolume = state.volume.value_or(0);
|
||||
|
||||
_cloudMuted = _localMuted = newMuted;
|
||||
|
||||
if (!newVolume) {
|
||||
return;
|
||||
}
|
||||
if (_waitingForUpdateVolume) {
|
||||
const auto localVolume
|
||||
= base::SafeRound(_slider->value() * _maxVolume);
|
||||
if ((localVolume != newVolume)
|
||||
&& (_cloudVolume == newVolume)) {
|
||||
_changeVolumeRequests.fire(int(localVolume));
|
||||
}
|
||||
} else {
|
||||
setCloudVolume(newVolume);
|
||||
}
|
||||
_waitingForUpdateVolume = false;
|
||||
}, lifetime());
|
||||
|
||||
_slider->setAdjustCallback([=](float64 value) {
|
||||
for (const auto &snap : kVolumeStickedValues) {
|
||||
if (value > (snap.first - snap.second)
|
||||
&& value < (snap.first + snap.second)) {
|
||||
return snap.first;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
initArcsAnimation();
|
||||
}
|
||||
|
||||
void MenuVolumeItem::initArcsAnimation() {
|
||||
const auto lastTime = lifetime().make_state<int>(0);
|
||||
_arcsAnimation.init([=](crl::time now) {
|
||||
_arcs->update(now);
|
||||
update(_speakerRect);
|
||||
});
|
||||
|
||||
_arcs->startUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
if (!_arcsAnimation.animating()) {
|
||||
*lastTime = crl::now();
|
||||
_arcsAnimation.start();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_arcs->stopUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_arcsAnimation.stop();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::unmuteColor() const {
|
||||
return (isSelected()
|
||||
? _st.itemFgOver
|
||||
: isEnabled()
|
||||
? _st.itemFg
|
||||
: _st.itemFgDisabled)->c;
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::muteColor() const {
|
||||
return (isSelected()
|
||||
? st::attentionButtonFgOver
|
||||
: st::attentionButtonFg)->c;
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setCloudVolume(int volume) {
|
||||
if (_cloudVolume == volume) {
|
||||
return;
|
||||
}
|
||||
_cloudVolume = volume;
|
||||
if (!_slider->isChanging()) {
|
||||
setSliderVolume(_cloudMuted ? 0. : volume);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setSliderVolume(int volume) {
|
||||
const auto value = float64(volume) / _maxVolume;
|
||||
_slider->setValue(value);
|
||||
updateSliderColor(value);
|
||||
}
|
||||
|
||||
void MenuVolumeItem::updateSliderColor(float64 value) {
|
||||
value = std::clamp(value, 0., 1.);
|
||||
const auto colors = std::array<QColor, 4>{ {
|
||||
Ui::ColorFromSerialized(0xF66464),
|
||||
Ui::ColorFromSerialized(0xD0B738),
|
||||
Ui::ColorFromSerialized(0x24CD80),
|
||||
Ui::ColorFromSerialized(0x3BBCEC),
|
||||
} };
|
||||
_slider->setColorOverrides({
|
||||
.activeFg = (value < 0.25)
|
||||
? anim::color(colors[0], colors[1], value / 0.25)
|
||||
: (value < 0.5)
|
||||
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
|
||||
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
|
||||
});
|
||||
}
|
||||
|
||||
not_null<QAction*> MenuVolumeItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool MenuVolumeItem::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
int MenuVolumeItem::contentHeight() const {
|
||||
return rect::m::sum::v(_padding) + _stCross.icon.height();
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {
|
||||
return _toggleMuteRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteLocallyRequests() const {
|
||||
return _toggleMuteLocallyRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeRequests() const {
|
||||
return _changeVolumeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeLocallyRequests() const {
|
||||
return _changeVolumeLocallyRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
90
Telegram/SourceFiles/calls/group/calls_volume_item.h
Normal file
90
Telegram/SourceFiles/calls/group/calls_volume_item.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
namespace Ui {
|
||||
class CrossLineAnimation;
|
||||
class MediaSlider;
|
||||
namespace Paint {
|
||||
class ArcsAnimation;
|
||||
} // namespace Paint
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
class MenuVolumeItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MediaSlider &stSlider,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted,
|
||||
const QMargins &padding);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteLocallyRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeLocallyRequests() const;
|
||||
|
||||
protected:
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void initArcsAnimation();
|
||||
|
||||
void setCloudVolume(int volume);
|
||||
void setSliderVolume(int volume);
|
||||
void updateSliderColor(float64 value);
|
||||
|
||||
QColor unmuteColor() const;
|
||||
QColor muteColor() const;
|
||||
|
||||
const int _maxVolume;
|
||||
int _cloudVolume = 0;
|
||||
bool _waitingForUpdateVolume = false;
|
||||
bool _cloudMuted = false;
|
||||
bool _localMuted = false;
|
||||
|
||||
QRect _itemRect;
|
||||
QRect _speakerRect;
|
||||
QPoint _arcPosition;
|
||||
|
||||
const base::unique_qptr<Ui::MediaSlider> _slider;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const style::CrossLineAnimation &_stCross;
|
||||
const QMargins &_padding;
|
||||
|
||||
const std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;
|
||||
Ui::Animations::Simple _crossLineAnimation;
|
||||
const std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;
|
||||
Ui::Animations::Basic _arcsAnimation;
|
||||
|
||||
rpl::event_stream<bool> _toggleMuteRequests;
|
||||
rpl::event_stream<bool> _toggleMuteLocallyRequests;
|
||||
rpl::event_stream<int> _changeVolumeRequests;
|
||||
rpl::event_stream<int> _changeVolumeLocallyRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
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/ui/calls_group_recording_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QSvgRenderer>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRoundRadius = 9;
|
||||
constexpr auto kMaxGroupCallLength = 40;
|
||||
constexpr auto kSwitchDuration = 200;
|
||||
|
||||
class GraphicButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
GraphicButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &filename,
|
||||
int selectWidth = 0);
|
||||
|
||||
void setToggled(bool value);
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e);
|
||||
|
||||
private:
|
||||
const style::margins _margins;
|
||||
QSvgRenderer _renderer;
|
||||
Ui::RoundRect _roundRect;
|
||||
Ui::RoundRect _roundRectSelect;
|
||||
Ui::Animations::Simple _animation;
|
||||
bool _toggled = false;
|
||||
};
|
||||
|
||||
class RecordingInfo final : public Ui::RpWidget {
|
||||
public:
|
||||
RecordingInfo(not_null<Ui::RpWidget*> parent);
|
||||
|
||||
void prepareAudio();
|
||||
void prepareVideo();
|
||||
|
||||
RecordingType type() const;
|
||||
|
||||
private:
|
||||
void setLabel(const QString &text);
|
||||
|
||||
const object_ptr<Ui::VerticalLayout> _container;
|
||||
RecordingType _type = RecordingType::AudioOnly;
|
||||
};
|
||||
|
||||
class Switcher final : public Ui::RpWidget {
|
||||
public:
|
||||
Switcher(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<bool> &&toggled);
|
||||
|
||||
RecordingType type() const;
|
||||
|
||||
private:
|
||||
const object_ptr<Ui::BoxContentDivider> _background;
|
||||
const object_ptr<RecordingInfo> _audio;
|
||||
const object_ptr<RecordingInfo> _video;
|
||||
bool _toggled = false;
|
||||
|
||||
Ui::Animations::Simple _animation;
|
||||
};
|
||||
|
||||
GraphicButton::GraphicButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &filename,
|
||||
int selectWidth)
|
||||
: AbstractButton(parent)
|
||||
, _margins(selectWidth, selectWidth, selectWidth, selectWidth)
|
||||
, _renderer(u":/gui/recording/%1.svg"_q.arg(filename))
|
||||
, _roundRect(kRoundRadius, st::groupCallMembersBg)
|
||||
, _roundRectSelect(kRoundRadius, st::groupCallActiveFg) {
|
||||
const auto size = style::ConvertScale(_renderer.defaultSize());
|
||||
resize((QRect(QPoint(), size) + _margins).size());
|
||||
}
|
||||
|
||||
void GraphicButton::setToggled(bool value) {
|
||||
if (_toggled == value) {
|
||||
return;
|
||||
}
|
||||
_toggled = value;
|
||||
_animation.start(
|
||||
[=] { update(); },
|
||||
_toggled ? 0. : 1.,
|
||||
_toggled ? 1. : 0.,
|
||||
st::universalDuration);
|
||||
}
|
||||
|
||||
void GraphicButton::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
const auto progress = _animation.value(_toggled ? 1. : 0.);
|
||||
p.setOpacity(progress);
|
||||
_roundRectSelect.paint(p, rect());
|
||||
p.setOpacity(1.);
|
||||
const auto r = rect() - _margins;
|
||||
_roundRect.paint(p, r);
|
||||
_renderer.render(&p, r);
|
||||
}
|
||||
|
||||
RecordingInfo::RecordingInfo(not_null<Ui::RpWidget*> parent)
|
||||
: RpWidget(parent)
|
||||
, _container(this) {
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
_container->resizeToWidth(size.width());
|
||||
}, _container->lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::prepareAudio() {
|
||||
_type = RecordingType::AudioOnly;
|
||||
setLabel(tr::lng_group_call_recording_start_audio_subtitle(tr::now));
|
||||
|
||||
const auto wrap = _container->add(
|
||||
object_ptr<Ui::RpWidget>(_container),
|
||||
style::margins(0, st::groupCallRecordingAudioSkip, 0, 0));
|
||||
const auto audioIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_audio");
|
||||
wrap->resize(width(), audioIcon->height());
|
||||
audioIcon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
audioIcon->moveToLeft((size.width() - audioIcon->width()) / 2, 0);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::prepareVideo() {
|
||||
setLabel(tr::lng_group_call_recording_start_video_subtitle(tr::now));
|
||||
|
||||
const auto wrap = _container->add(
|
||||
object_ptr<Ui::RpWidget>(_container),
|
||||
style::margins());
|
||||
|
||||
const auto landscapeIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_video_landscape",
|
||||
st::groupCallRecordingSelectWidth);
|
||||
const auto portraitIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_video_portrait",
|
||||
st::groupCallRecordingSelectWidth);
|
||||
wrap->resize(width(), portraitIcon->height());
|
||||
|
||||
landscapeIcon->setToggled(true);
|
||||
_type = RecordingType::VideoLandscape;
|
||||
|
||||
const auto icons = std::vector<GraphicButton*>{
|
||||
landscapeIcon,
|
||||
portraitIcon,
|
||||
};
|
||||
const auto types = std::map<GraphicButton*, RecordingType>{
|
||||
{ landscapeIcon, RecordingType::VideoLandscape },
|
||||
{ portraitIcon, RecordingType::VideoPortrait },
|
||||
};
|
||||
for (const auto icon : icons) {
|
||||
icon->clicks(
|
||||
) | rpl::on_next([=] {
|
||||
for (const auto &i : icons) {
|
||||
i->setToggled(icon == i);
|
||||
}
|
||||
_type = types.at(icon);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
wrap->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
const auto wHalf = size.width() / icons.size();
|
||||
for (auto i = 0; i < icons.size(); i++) {
|
||||
const auto &icon = icons[i];
|
||||
icon->moveToLeft(
|
||||
wHalf * i + (wHalf - icon->width()) / 2,
|
||||
(size.height() - icon->height()) / 2);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::setLabel(const QString &text) {
|
||||
_container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_container,
|
||||
text,
|
||||
st::groupCallRecordingSubLabel),
|
||||
st::groupCallRecordingSubLabelMargins,
|
||||
style::al_top);
|
||||
}
|
||||
|
||||
RecordingType RecordingInfo::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
Switcher::Switcher(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<bool> &&toggled)
|
||||
: RpWidget(parent)
|
||||
, _background(
|
||||
this,
|
||||
st::groupCallRecordingInfoHeight,
|
||||
st::groupCallDividerBar)
|
||||
, _audio(this)
|
||||
, _video(this) {
|
||||
_audio->prepareAudio();
|
||||
_video->prepareVideo();
|
||||
|
||||
resize(0, st::groupCallRecordingInfoHeight);
|
||||
|
||||
const auto updatePositions = [=](float64 progress) {
|
||||
_audio->moveToLeft(-width() * progress, 0);
|
||||
_video->moveToLeft(_audio->x() + _audio->width(), 0);
|
||||
};
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
_audio->resize(size.width(), size.height());
|
||||
_video->resize(size.width(), size.height());
|
||||
|
||||
updatePositions(_toggled ? 1. : 0.);
|
||||
|
||||
_background->lower();
|
||||
_background->setGeometry(QRect(QPoint(), size));
|
||||
}, lifetime());
|
||||
|
||||
std::move(
|
||||
toggled
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
_toggled = toggled;
|
||||
_animation.start(
|
||||
updatePositions,
|
||||
toggled ? 0. : 1.,
|
||||
toggled ? 1. : 0.,
|
||||
kSwitchDuration);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
RecordingType Switcher::type() const {
|
||||
return _toggled ? _video->type() : _audio->type();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditGroupCallTitleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &placeholder,
|
||||
const QString &title,
|
||||
bool livestream,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(livestream
|
||||
? tr::lng_group_call_edit_title_channel()
|
||||
: tr::lng_group_call_edit_title());
|
||||
const auto input = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::groupCallField,
|
||||
rpl::single(placeholder),
|
||||
title));
|
||||
input->setMaxLength(kMaxGroupCallLength);
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
const auto submit = [=] {
|
||||
const auto result = input->getLastText().trimmed();
|
||||
box->closeBox();
|
||||
done(result);
|
||||
};
|
||||
input->submits() | rpl::on_next(submit, input->lifetime());
|
||||
box->addButton(tr::lng_settings_save(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void StartGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(RecordingType)> done) {
|
||||
box->setTitle(tr::lng_group_call_recording_start());
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_group_call_recording_start_sure(),
|
||||
st::groupCallBoxLabel));
|
||||
|
||||
const auto checkbox = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::lng_group_call_recording_start_checkbox(),
|
||||
false,
|
||||
st::groupCallCheckbox),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxRowPadding.bottom()));
|
||||
|
||||
const auto switcher = box->addRow(
|
||||
object_ptr<Switcher>(box, checkbox->checkedChanges()),
|
||||
st::groupCallRecordingInfoMargins);
|
||||
|
||||
box->addButton(tr::lng_continue(), [=] {
|
||||
const auto type = switcher->type();
|
||||
box->closeBox();
|
||||
done(type);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void AddTitleGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &title,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(tr::lng_group_call_recording_start_title());
|
||||
|
||||
const auto input = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::groupCallField,
|
||||
tr::lng_group_call_recording_start_field(),
|
||||
title));
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
const auto submit = [=] {
|
||||
const auto result = input->getLastText().trimmed();
|
||||
box->closeBox();
|
||||
done(result);
|
||||
};
|
||||
input->submits() | rpl::on_next(submit, input->lifetime());
|
||||
box->addButton(tr::lng_group_call_recording_start_button(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
|
||||
void StopGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(QString)> done) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_group_call_recording_stop_sure(),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] {
|
||||
box->closeBox();
|
||||
done(QString());
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class RecordingType {
|
||||
AudioOnly,
|
||||
VideoLandscape,
|
||||
VideoPortrait,
|
||||
};
|
||||
|
||||
void EditGroupCallTitleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &placeholder,
|
||||
const QString &title,
|
||||
bool livestream,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
void StartGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(RecordingType)> done);
|
||||
|
||||
void AddTitleGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &title,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
void StopGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
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/ui/calls_group_scheduled_labels.h"
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
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 = QLocale().toString(
|
||||
parsedDate.time(),
|
||||
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();
|
||||
}
|
||||
|
||||
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::on_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::on_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;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> StartsWhenText(
|
||||
rpl::producer<TimeId> date);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> CreateGradientLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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/ui/calls_group_stars_coloring.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "payments/ui/payments_reaction_box.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
StarsColoring StarsColoringForCount(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
int stars) {
|
||||
for (auto i = begin(colorings), e = end(colorings); i != e; ++i) {
|
||||
if (i->fromStars > stars) {
|
||||
Assert(i != begin(colorings));
|
||||
return *(std::prev(i));
|
||||
}
|
||||
}
|
||||
return colorings.back();
|
||||
}
|
||||
|
||||
int StarsRequiredForMessage(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
const TextWithTags &text) {
|
||||
Expects(!colorings.empty());
|
||||
|
||||
auto emojis = 0;
|
||||
auto outLength = 0;
|
||||
auto view = QStringView(text.text);
|
||||
const auto length = int(view.size());
|
||||
while (!view.isEmpty()) {
|
||||
if (Emoji::Find(view, &outLength)) {
|
||||
view = view.mid(outLength);
|
||||
++emojis;
|
||||
} else {
|
||||
view = view.mid(1);
|
||||
}
|
||||
}
|
||||
for (const auto &entry : colorings) {
|
||||
if (emojis <= entry.emojiLimit && length <= entry.charactersMax) {
|
||||
return entry.fromStars;
|
||||
}
|
||||
}
|
||||
return colorings.back().fromStars + 1;
|
||||
}
|
||||
|
||||
object_ptr<RpWidget> VideoStreamStarsLevel(
|
||||
not_null<RpWidget*> box,
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
rpl::producer<int> starsValue) {
|
||||
struct State {
|
||||
rpl::variable<int> stars;
|
||||
rpl::variable<StarsColoring> coloring;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->stars = std::move(starsValue);
|
||||
state->coloring = state->stars.value(
|
||||
) | rpl::map([=](int stars) {
|
||||
return StarsColoringForCount(colorings, stars);
|
||||
});
|
||||
|
||||
auto pinTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
const auto seconds = value.secondsPin;
|
||||
return (seconds >= 3600)
|
||||
? tr::lng_hours_tiny(tr::now, lt_count, seconds / 3600)
|
||||
: (seconds >= 60)
|
||||
? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)
|
||||
: tr::lng_seconds_tiny(tr::now, lt_count, seconds);
|
||||
});
|
||||
auto limitTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return QString::number(value.charactersMax);
|
||||
});
|
||||
auto limitSubtext = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return tr::lng_paid_comment_limit_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
value.charactersMax);
|
||||
});
|
||||
auto emojiTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return QString::number(value.emojiLimit);
|
||||
});
|
||||
auto emojiSubtext = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return tr::lng_paid_comment_emoji_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
value.emojiLimit);
|
||||
});
|
||||
return MakeStarSelectInfoBlocks(box, {
|
||||
{
|
||||
.title = std::move(pinTitle) | rpl::map(tr::marked),
|
||||
.subtext = tr::lng_paid_comment_pin_about(),
|
||||
},
|
||||
{
|
||||
.title = std::move(limitTitle) | rpl::map(tr::marked),
|
||||
.subtext = std::move(limitSubtext),
|
||||
},
|
||||
{
|
||||
.title = std::move(emojiTitle) | rpl::map(tr::marked),
|
||||
.subtext = std::move(emojiSubtext),
|
||||
},
|
||||
}, {}, true);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct StarsColoring {
|
||||
int bgLight = 0;
|
||||
int bgDark = 0;
|
||||
int fromStars = 0;
|
||||
TimeId secondsPin = 0;
|
||||
int charactersMax = 0;
|
||||
int emojiLimit = 0;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const StarsColoring &,
|
||||
const StarsColoring &) = default;
|
||||
friend inline bool operator==(
|
||||
const StarsColoring &,
|
||||
const StarsColoring &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] StarsColoring StarsColoringForCount(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
int stars);
|
||||
|
||||
[[nodiscard]] int StarsRequiredForMessage(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
const TextWithTags &text);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> VideoStreamStarsLevel(
|
||||
not_null<Ui::RpWidget*> box,
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
rpl::producer<int> starsValue);
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
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/ui/desktop_capture_choose_source.h"
|
||||
|
||||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
|
||||
#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
namespace Calls::Group::Ui::DesktopCapture {
|
||||
namespace {
|
||||
|
||||
constexpr auto kColumns = 3;
|
||||
constexpr auto kRows = 2;
|
||||
|
||||
struct Preview {
|
||||
explicit Preview(tgcalls::DesktopCaptureSource source);
|
||||
|
||||
tgcalls::DesktopCaptureSourceHelper helper;
|
||||
Webrtc::VideoTrack track;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
class SourceButton final : public RippleButton {
|
||||
public:
|
||||
using RippleButton::RippleButton;
|
||||
|
||||
private:
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
};
|
||||
|
||||
QImage SourceButton::prepareRippleMask() const {
|
||||
return RippleAnimation::RoundRectMask(size(), st::roundRadiusLarge);
|
||||
}
|
||||
|
||||
class Source final {
|
||||
public:
|
||||
Source(
|
||||
not_null<QWidget*> parent,
|
||||
tgcalls::DesktopCaptureSource source,
|
||||
const QString &title);
|
||||
|
||||
void setGeometry(QRect geometry);
|
||||
void clearHelper();
|
||||
|
||||
[[nodiscard]] rpl::producer<> activations() const;
|
||||
void setActive(bool active);
|
||||
[[nodiscard]] QString deviceIdKey() const;
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void paint();
|
||||
void setupPreview();
|
||||
|
||||
SourceButton _widget;
|
||||
FlatLabel _label;
|
||||
Ui::RoundRect _selectedRect;
|
||||
Ui::RoundRect _activeRect;
|
||||
tgcalls::DesktopCaptureSource _source;
|
||||
std::unique_ptr<Preview> _preview;
|
||||
rpl::event_stream<> _activations;
|
||||
QImage _frame;
|
||||
bool _active = false;
|
||||
|
||||
};
|
||||
|
||||
class ChooseSourceProcess final {
|
||||
public:
|
||||
static void Start(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
explicit ChooseSourceProcess(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
void activate();
|
||||
|
||||
private:
|
||||
void setupPanel();
|
||||
void setupSources();
|
||||
void setupGeometryWithParent(not_null<QWidget*> parent);
|
||||
void fillSources();
|
||||
void setupSourcesGeometry();
|
||||
void updateButtonsVisibility();
|
||||
void destroy();
|
||||
|
||||
static base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>> &Map();
|
||||
|
||||
const not_null<ChooseSourceDelegate*> _delegate;
|
||||
const std::unique_ptr<RpWindow> _window;
|
||||
const std::unique_ptr<ScrollArea> _scroll;
|
||||
const not_null<RpWidget*> _inner;
|
||||
const not_null<RpWidget*> _bottom;
|
||||
const not_null<RoundButton*> _submit;
|
||||
const not_null<RoundButton*> _finish;
|
||||
const not_null<Checkbox*> _withAudio;
|
||||
|
||||
QSize _fixedSize;
|
||||
std::vector<std::unique_ptr<Source>> _sources;
|
||||
Source *_selected = nullptr;
|
||||
QString _selectedId;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = st::desktopCaptureSourceSize * factor;
|
||||
return {
|
||||
.aspectSize = { size.width(), size.height() },
|
||||
.fps = 1,
|
||||
.captureMouse = false,
|
||||
};
|
||||
}
|
||||
|
||||
Preview::Preview(tgcalls::DesktopCaptureSource source)
|
||||
: helper(source, SourceData())
|
||||
, track(Webrtc::VideoState::Active) {
|
||||
helper.setOutput(track.sink());
|
||||
helper.start();
|
||||
}
|
||||
|
||||
Source::Source(
|
||||
not_null<QWidget*> parent,
|
||||
tgcalls::DesktopCaptureSource source,
|
||||
const QString &title)
|
||||
: _widget(parent, st::groupCallRipple)
|
||||
, _label(&_widget, title, st::desktopCaptureLabel)
|
||||
, _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver)
|
||||
, _activeRect(ImageRoundRadius::Large, st::groupCallMuted1)
|
||||
, _source(source) {
|
||||
_widget.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
paint();
|
||||
}, _widget.lifetime());
|
||||
|
||||
_label.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_widget.sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
_label.resizeToNaturalWidth(
|
||||
size.width() - padding.left() - padding.right());
|
||||
_label.move(
|
||||
(size.width() - _label.width()) / 2,
|
||||
size.height() - _label.height() - st::desktopCaptureLabelBottom);
|
||||
}, _label.lifetime());
|
||||
|
||||
_widget.setClickedCallback([=] {
|
||||
setActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<> Source::activations() const {
|
||||
return _activations.events();
|
||||
}
|
||||
|
||||
QString Source::deviceIdKey() const {
|
||||
return QString::fromStdString(_source.deviceIdKey());
|
||||
}
|
||||
|
||||
void Source::setActive(bool active) {
|
||||
if (_active != active) {
|
||||
_active = active;
|
||||
_widget.update();
|
||||
if (active) {
|
||||
_activations.fire({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Source::setGeometry(QRect geometry) {
|
||||
_widget.setGeometry(geometry);
|
||||
}
|
||||
|
||||
void Source::clearHelper() {
|
||||
_preview = nullptr;
|
||||
}
|
||||
|
||||
void Source::paint() {
|
||||
auto p = QPainter(&_widget);
|
||||
|
||||
if (_frame.isNull() && !_preview) {
|
||||
setupPreview();
|
||||
}
|
||||
if (_active) {
|
||||
_activeRect.paint(p, _widget.rect());
|
||||
} else if (_widget.isOver() || _widget.isDown()) {
|
||||
_selectedRect.paint(p, _widget.rect());
|
||||
}
|
||||
_widget.paintRipple(
|
||||
p,
|
||||
{ 0, 0 },
|
||||
_active ? &st::shadowFg->c : nullptr);
|
||||
|
||||
const auto size = _preview ? _preview->track.frameSize() : QSize();
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
const auto rect = _widget.rect();
|
||||
const auto inner = rect.marginsRemoved(padding);
|
||||
if (!size.isEmpty()) {
|
||||
const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio);
|
||||
const auto request = Webrtc::FrameRequest{
|
||||
.resize = scaled * factor,
|
||||
.outer = scaled * factor,
|
||||
};
|
||||
_frame = _preview->track.frame(request);
|
||||
_preview->track.markFrameShown();
|
||||
}
|
||||
if (!_frame.isNull()) {
|
||||
clearHelper();
|
||||
const auto size = _frame.size() / factor;
|
||||
const auto x = inner.x() + (inner.width() - size.width()) / 2;
|
||||
const auto y = inner.y() + (inner.height() - size.height()) / 2;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawImage(QRect(x, y, size.width(), size.height()), _frame);
|
||||
}
|
||||
}
|
||||
|
||||
void Source::setupPreview() {
|
||||
_preview = std::make_unique<Preview>(_source);
|
||||
_preview->track.renderNextFrame(
|
||||
) | rpl::on_next([=] {
|
||||
if (_preview->track.frameSize().isEmpty()) {
|
||||
_preview->track.markFrameShown();
|
||||
}
|
||||
_widget.update();
|
||||
}, _preview->lifetime);
|
||||
}
|
||||
|
||||
rpl::lifetime &Source::lifetime() {
|
||||
return _widget.lifetime();
|
||||
}
|
||||
|
||||
ChooseSourceProcess::ChooseSourceProcess(
|
||||
not_null<ChooseSourceDelegate*> delegate)
|
||||
: _delegate(delegate)
|
||||
, _window(std::make_unique<RpWindow>())
|
||||
, _scroll(std::make_unique<ScrollArea>(_window->body()))
|
||||
, _inner(_scroll->setOwnedWidget(object_ptr<RpWidget>(_scroll.get())))
|
||||
, _bottom(CreateChild<RpWidget>(_window->body().get()))
|
||||
, _submit(
|
||||
CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_start(),
|
||||
st::desktopCaptureSubmit))
|
||||
, _finish(
|
||||
CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_stop(),
|
||||
st::desktopCaptureFinish))
|
||||
, _withAudio(
|
||||
CreateChild<Checkbox>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_audio(tr::now),
|
||||
false,
|
||||
st::desktopCaptureWithAudio)) {
|
||||
setupPanel();
|
||||
setupSources();
|
||||
activate();
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::Start(not_null<ChooseSourceDelegate*> delegate) {
|
||||
auto &map = Map();
|
||||
auto i = map.find(delegate);
|
||||
if (i == end(map)) {
|
||||
i = map.emplace(delegate, nullptr).first;
|
||||
delegate->chooseSourceInstanceLifetime().add([=] {
|
||||
Map().erase(delegate);
|
||||
});
|
||||
}
|
||||
if (!i->second) {
|
||||
i->second = std::make_unique<ChooseSourceProcess>(delegate);
|
||||
} else {
|
||||
i->second->activate();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::activate() {
|
||||
if (_window->windowState() & Qt::WindowMinimized) {
|
||||
_window->showNormal();
|
||||
} else {
|
||||
_window->show();
|
||||
}
|
||||
_window->raise();
|
||||
_window->activateWindow();
|
||||
}
|
||||
|
||||
[[nodiscard]] base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>> &ChooseSourceProcess::Map() {
|
||||
static auto result = base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>>();
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupPanel() {
|
||||
#ifndef Q_OS_LINUX
|
||||
//_window->setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
#endif // Q_OS_LINUX
|
||||
//_window->setAttribute(Qt::WA_NoSystemBackground);
|
||||
|
||||
_window->setWindowIcon(QIcon(
|
||||
QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
|
||||
_window->setTitleStyle(st::desktopCaptureSourceTitle);
|
||||
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
const auto bottomSkip = margins.right() + padding.right();
|
||||
const auto bottomHeight = 2 * bottomSkip
|
||||
+ st::desktopCaptureCancel.height;
|
||||
const auto width = margins.left()
|
||||
+ kColumns * st::desktopCaptureSourceSize.width()
|
||||
+ (kColumns - 1) * skips.width()
|
||||
+ margins.right();
|
||||
const auto height = margins.top()
|
||||
+ kRows * st::desktopCaptureSourceSize.height()
|
||||
+ (kRows - 1) * skips.height()
|
||||
+ (st::desktopCaptureSourceSize.height() / 2)
|
||||
+ bottomHeight;
|
||||
_fixedSize = QSize(width, height);
|
||||
_window->setStaysOnTop(true);
|
||||
|
||||
_window->body()->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg);
|
||||
}, _window->lifetime());
|
||||
|
||||
_bottom->setGeometry(0, height - bottomHeight, width, bottomHeight);
|
||||
|
||||
_submit->setClickedCallback([=] {
|
||||
if (_selectedId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto weak = base::make_weak(_window.get());
|
||||
_delegate->chooseSourceAccepted(
|
||||
_selectedId,
|
||||
!_withAudio->isHidden() && _withAudio->checked());
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->close();
|
||||
}
|
||||
});
|
||||
_finish->setClickedCallback([=] {
|
||||
const auto weak = base::make_weak(_window.get());
|
||||
_delegate->chooseSourceStop();
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->close();
|
||||
}
|
||||
});
|
||||
const auto cancel = CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_cancel(),
|
||||
st::desktopCaptureCancel);
|
||||
cancel->setClickedCallback([=] {
|
||||
_window->close();
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
_submit->widthValue(),
|
||||
_submit->shownValue(),
|
||||
_finish->widthValue(),
|
||||
_finish->shownValue(),
|
||||
cancel->widthValue()
|
||||
) | rpl::on_next([=](
|
||||
int submitWidth,
|
||||
bool submitShown,
|
||||
int finishWidth,
|
||||
bool finishShown,
|
||||
int cancelWidth) {
|
||||
_finish->moveToRight(bottomSkip, bottomSkip);
|
||||
_submit->moveToRight(bottomSkip, bottomSkip);
|
||||
cancel->moveToRight(
|
||||
bottomSkip * 2 + (submitShown ? submitWidth : finishWidth),
|
||||
bottomSkip);
|
||||
}, _bottom->lifetime());
|
||||
|
||||
_withAudio->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2;
|
||||
_withAudio->moveToLeft(bottomSkip, top);
|
||||
}, _withAudio->lifetime());
|
||||
|
||||
_withAudio->setChecked(_delegate->chooseSourceActiveWithAudio());
|
||||
_withAudio->checkedChanges(
|
||||
) | rpl::on_next([=] {
|
||||
updateButtonsVisibility();
|
||||
}, _withAudio->lifetime());
|
||||
|
||||
const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty();
|
||||
_finish->setVisible(sharing);
|
||||
_submit->setVisible(!sharing);
|
||||
|
||||
_window->body()->sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
_scroll->setGeometry(
|
||||
0,
|
||||
0,
|
||||
size.width(),
|
||||
size.height() - _bottom->height());
|
||||
}, _scroll->lifetime());
|
||||
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto innerHeight = margins.top()
|
||||
+ rows * st::desktopCaptureSourceSize.height()
|
||||
+ (rows - 1) * skips.height()
|
||||
+ margins.bottom();
|
||||
_inner->resize(width, innerHeight);
|
||||
}, _inner->lifetime());
|
||||
|
||||
if (const auto parent = _delegate->chooseSourceParent()) {
|
||||
setupGeometryWithParent(parent);
|
||||
}
|
||||
|
||||
_window->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return e->type() == QEvent::Close;
|
||||
}) | rpl::on_next([=] {
|
||||
destroy();
|
||||
}, _window->lifetime());
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupSources() {
|
||||
fillSources();
|
||||
setupSourcesGeometry();
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::fillSources() {
|
||||
using Type = tgcalls::DesktopCaptureType;
|
||||
auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen);
|
||||
auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window);
|
||||
|
||||
_withAudio->setVisible(_delegate->chooseSourceWithAudioSupported());
|
||||
|
||||
auto screenIndex = 0;
|
||||
auto windowIndex = 0;
|
||||
auto firstScreenSelected = false;
|
||||
const auto active = _delegate->chooseSourceActiveDeviceId();
|
||||
const auto append = [&](const tgcalls::DesktopCaptureSource &source) {
|
||||
const auto firstScreen = !source.isWindow() && !screenIndex;
|
||||
const auto title = !source.isWindow()
|
||||
? tr::lng_group_call_screen_title(
|
||||
tr::now,
|
||||
lt_index,
|
||||
QString::number(++screenIndex))
|
||||
: !source.title().empty()
|
||||
? QString::fromStdString(source.title())
|
||||
: "Window " + QString::number(++windowIndex);
|
||||
const auto id = source.deviceIdKey();
|
||||
_sources.push_back(std::make_unique<Source>(_inner, source, title));
|
||||
|
||||
const auto raw = _sources.back().get();
|
||||
if (!active.isEmpty() && active.toStdString() == id) {
|
||||
_selected = raw;
|
||||
raw->setActive(true);
|
||||
} else if (active.isEmpty() && firstScreen) {
|
||||
_selected = raw;
|
||||
raw->setActive(true);
|
||||
firstScreenSelected = true;
|
||||
}
|
||||
_sources.back()->activations(
|
||||
) | rpl::filter([=] {
|
||||
return (_selected != raw);
|
||||
}) | rpl::on_next([=]{
|
||||
if (_selected) {
|
||||
_selected->setActive(false);
|
||||
}
|
||||
_selected = raw;
|
||||
updateButtonsVisibility();
|
||||
}, raw->lifetime());
|
||||
};
|
||||
for (const auto &source : screensManager.sources()) {
|
||||
append(source);
|
||||
}
|
||||
for (const auto &source : windowsManager.sources()) {
|
||||
append(source);
|
||||
}
|
||||
if (firstScreenSelected) {
|
||||
updateButtonsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::updateButtonsVisibility() {
|
||||
const auto selectedId = _selected
|
||||
? _selected->deviceIdKey()
|
||||
: QString();
|
||||
if (selectedId == _delegate->chooseSourceActiveDeviceId()
|
||||
&& (!_delegate->chooseSourceWithAudioSupported()
|
||||
|| (_withAudio->checked()
|
||||
== _delegate->chooseSourceActiveWithAudio()))) {
|
||||
_selectedId = QString();
|
||||
_finish->setVisible(true);
|
||||
_submit->setVisible(false);
|
||||
} else {
|
||||
_selectedId = selectedId;
|
||||
_finish->setVisible(false);
|
||||
_submit->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupSourcesGeometry() {
|
||||
if (_sources.empty()) {
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
_inner->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto single = (width
|
||||
- margins.left()
|
||||
- margins.right()
|
||||
- (kColumns - 1) * skips.width()) / kColumns;
|
||||
const auto height = st::desktopCaptureSourceSize.height();
|
||||
auto top = margins.top();
|
||||
auto index = 0;
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
auto left = margins.left();
|
||||
for (auto column = 0; column != kColumns; ++column) {
|
||||
_sources[index]->setGeometry({ left, top, single, height });
|
||||
if (++index == _sources.size()) {
|
||||
break;
|
||||
}
|
||||
left += single + skips.width();
|
||||
}
|
||||
if (index >= _sources.size()) {
|
||||
break;
|
||||
}
|
||||
top += height + skips.height();
|
||||
}
|
||||
}, _inner->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue()
|
||||
) | rpl::on_next([=](int scrollTop, int scrollHeight) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto height = st::desktopCaptureSourceSize.height();
|
||||
auto top = margins.top();
|
||||
auto index = 0;
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
const auto hidden = (top + height <= scrollTop)
|
||||
|| (top >= scrollTop + scrollHeight);
|
||||
if (hidden) {
|
||||
for (auto column = 0; column != kColumns; ++column) {
|
||||
_sources[index]->clearHelper();
|
||||
if (++index == _sources.size()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
index += kColumns;
|
||||
}
|
||||
if (index >= _sources.size()) {
|
||||
break;
|
||||
}
|
||||
top += height + skips.height();
|
||||
}
|
||||
}, _inner->lifetime());
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupGeometryWithParent(
|
||||
not_null<QWidget*> parent) {
|
||||
const auto parentScreen = parent->screen();
|
||||
const auto myScreen = _window->screen();
|
||||
if (parentScreen && myScreen != parentScreen) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
_window->setScreen(parentScreen);
|
||||
#else // Qt >= 6.0.0
|
||||
_window->createWinId();
|
||||
_window->windowHandle()->setScreen(parentScreen);
|
||||
#endif // Qt < 6.0.0
|
||||
}
|
||||
_window->setFixedSize(_fixedSize);
|
||||
_window->move(
|
||||
parent->x() + (parent->width() - _window->width()) / 2,
|
||||
parent->y() + (parent->height() - _window->height()) / 2);
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::destroy() {
|
||||
auto &map = Map();
|
||||
if (const auto i = map.find(_delegate); i != end(map)) {
|
||||
if (i->second.get() == this) {
|
||||
base::take(i->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseSource(not_null<ChooseSourceDelegate*> delegate) {
|
||||
ChooseSourceProcess::Start(delegate);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui::DesktopCapture
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group::Ui::DesktopCapture {
|
||||
|
||||
class ChooseSourceDelegate {
|
||||
public:
|
||||
virtual QWidget *chooseSourceParent() = 0;
|
||||
virtual QString chooseSourceActiveDeviceId() = 0;
|
||||
virtual bool chooseSourceActiveWithAudio() = 0;
|
||||
virtual bool chooseSourceWithAudioSupported() = 0;
|
||||
virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0;
|
||||
virtual void chooseSourceAccepted(
|
||||
const QString &deviceId,
|
||||
bool withAudio) = 0;
|
||||
virtual void chooseSourceStop() = 0;
|
||||
};
|
||||
|
||||
void ChooseSource(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
} // namespace Calls::Group::Ui::DesktopCapture
|
||||
Reference in New Issue
Block a user