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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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> &params);
[[nodiscard]] const std::string &GetScreenEndpoint(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] bool IsCameraPaused(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] bool IsScreenPaused(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] uint32 GetAdditionalAudioSsrc(
const std::shared_ptr<ParticipantVideoParams> &params);
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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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