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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
1754
Telegram/SourceFiles/calls/calls.style
Normal file
1754
Telegram/SourceFiles/calls/calls.style
Normal file
File diff suppressed because it is too large
Load Diff
899
Telegram/SourceFiles/calls/calls_box_controller.cpp
Normal file
899
Telegram/SourceFiles/calls/calls_box_controller.cpp
Normal file
@@ -0,0 +1,899 @@
|
||||
/*
|
||||
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/calls_box_controller.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "core/application.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_invite_controller.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "mainwidget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_peer_values.h" // Data::ChannelHasActiveCall.
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "boxes/delete_messages_box.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "api/api_updates.h"
|
||||
#include "apiwrap.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "settings/settings_calls.h"
|
||||
#include "styles/style_info.h" // infoTopBarMenu
|
||||
#include "styles/style_layers.h" // st::boxLabel.
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kFirstPageCount = 20;
|
||||
constexpr auto kPerPageCount = 100;
|
||||
|
||||
class GroupCallRow final : public PeerListRow {
|
||||
public:
|
||||
GroupCallRow(not_null<PeerData*> peer);
|
||||
|
||||
void rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void rightActionStopLastRipple() override;
|
||||
|
||||
int paintNameIconGetWidth(
|
||||
Painter &p,
|
||||
Fn<void()> repaint,
|
||||
crl::time now,
|
||||
int nameLeft,
|
||||
int nameTop,
|
||||
int nameWidth,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override {
|
||||
return 0;
|
||||
}
|
||||
QSize rightActionSize() const override {
|
||||
return peer()->isChannel() ? QSize(_st.width, _st.height) : QSize();
|
||||
}
|
||||
QMargins rightActionMargins() const override {
|
||||
return QMargins(
|
||||
0,
|
||||
0,
|
||||
st::defaultPeerListItem.photoPosition.x(),
|
||||
0);
|
||||
}
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
private:
|
||||
const style::IconButton &_st;
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
|
||||
};
|
||||
|
||||
GroupCallRow::GroupCallRow(not_null<PeerData*> peer)
|
||||
: PeerListRow(peer)
|
||||
, _st(st::callGroupCall) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
const auto status = (!channel->isMegagroup()
|
||||
? (channel->isPublic()
|
||||
? tr::lng_create_public_channel_title
|
||||
: tr::lng_create_private_channel_title)
|
||||
: (channel->isPublic()
|
||||
? tr::lng_create_public_group_title
|
||||
: tr::lng_create_private_group_title))(tr::now);
|
||||
setCustomStatus(status.toLower());
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallRow::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = rightActionSize();
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
x + _st.rippleAreaPosition.x(),
|
||||
y + _st.rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
_st.icon.paintInCenter(
|
||||
p,
|
||||
style::rtlrect(x, y, size.width(), size.height(), outerWidth));
|
||||
}
|
||||
|
||||
void GroupCallRow::rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(
|
||||
QSize(_st.rippleAreaSize, _st.rippleAreaSize));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
_st.ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point - _st.rippleAreaPosition);
|
||||
}
|
||||
|
||||
void GroupCallRow::rightActionStopLastRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace GroupCalls {
|
||||
|
||||
ListController::ListController(not_null<::Window::SessionController*> window)
|
||||
: _window(window) {
|
||||
setStyleOverrides(&st::peerListSingleRow);
|
||||
}
|
||||
|
||||
Main::Session &ListController::session() const {
|
||||
return _window->session();
|
||||
}
|
||||
|
||||
void ListController::prepare() {
|
||||
const auto removeRow = [=](not_null<PeerData*> peer) {
|
||||
const auto it = _groupCalls.find(peer->id);
|
||||
if (it != end(_groupCalls)) {
|
||||
const auto &row = it->second;
|
||||
delegate()->peerListRemoveRow(row);
|
||||
_groupCalls.erase(it);
|
||||
}
|
||||
};
|
||||
const auto createRow = [=](not_null<PeerData*> peer) {
|
||||
const auto it = _groupCalls.find(peer->id);
|
||||
if (it == end(_groupCalls)) {
|
||||
auto row = std::make_unique<GroupCallRow>(peer);
|
||||
_groupCalls.emplace(peer->id, row.get());
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
};
|
||||
|
||||
const auto processPeer = [=](PeerData *peer) {
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
const auto channel = peer->asChannel();
|
||||
if (channel && Data::ChannelHasActiveCall(channel)) {
|
||||
createRow(peer);
|
||||
} else {
|
||||
removeRow(peer);
|
||||
}
|
||||
};
|
||||
const auto finishProcess = [=] {
|
||||
delegate()->peerListRefreshRows();
|
||||
_fullCount = delegate()->peerListFullRowsCount();
|
||||
};
|
||||
|
||||
session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::GroupCall
|
||||
) | rpl::on_next([=](const Data::PeerUpdate &update) {
|
||||
processPeer(update.peer);
|
||||
finishProcess();
|
||||
}, lifetime());
|
||||
|
||||
{
|
||||
auto count = 0;
|
||||
const auto list = session().data().chatsList(nullptr);
|
||||
for (const auto &key : list->pinned()->order()) {
|
||||
processPeer(key.peer());
|
||||
}
|
||||
for (const auto &key : list->indexed()->all()) {
|
||||
if (count > kFirstPageCount) {
|
||||
break;
|
||||
}
|
||||
processPeer(key->key().peer());
|
||||
count++;
|
||||
}
|
||||
finishProcess();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<bool> ListController::shownValue() const {
|
||||
return _fullCount.value(
|
||||
) | rpl::map(rpl::mappers::_1 > 0) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
void ListController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto window = _window;
|
||||
crl::on_main(window, [=, peer = row->peer()] {
|
||||
window->showPeerHistory(
|
||||
peer,
|
||||
::Window::SectionShow::Way::ClearStack);
|
||||
});
|
||||
}
|
||||
|
||||
void ListController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
_window->startOrJoinGroupCall(row->peer());
|
||||
}
|
||||
|
||||
} // namespace GroupCalls
|
||||
|
||||
class BoxController::Row : public PeerListRow {
|
||||
public:
|
||||
Row(not_null<HistoryItem*> item);
|
||||
|
||||
enum class Type {
|
||||
Out,
|
||||
In,
|
||||
Missed,
|
||||
};
|
||||
|
||||
enum class CallType {
|
||||
Voice,
|
||||
Video,
|
||||
};
|
||||
|
||||
bool canAddItem(not_null<const HistoryItem*> item) const {
|
||||
return (ComputeType(item) == _type)
|
||||
&& (!hasItems() || _items.front()->history() == item->history())
|
||||
&& (ItemDateTime(item).date() == _date);
|
||||
}
|
||||
void addItem(not_null<HistoryItem*> item) {
|
||||
Expects(canAddItem(item));
|
||||
|
||||
_items.push_back(item);
|
||||
ranges::sort(_items, [](not_null<HistoryItem*> a, auto b) {
|
||||
return (a->id > b->id);
|
||||
});
|
||||
refreshStatus();
|
||||
}
|
||||
void itemRemoved(not_null<const HistoryItem*> item) {
|
||||
if (hasItems() && item->id >= minItemId() && item->id <= maxItemId()) {
|
||||
_items.erase(std::remove(_items.begin(), _items.end(), item), _items.end());
|
||||
refreshStatus();
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool hasItems() const {
|
||||
return !_items.empty();
|
||||
}
|
||||
|
||||
[[nodiscard]] MsgId minItemId() const {
|
||||
Expects(hasItems());
|
||||
|
||||
return _items.back()->id;
|
||||
}
|
||||
|
||||
[[nodiscard]] MsgId maxItemId() const {
|
||||
Expects(hasItems());
|
||||
|
||||
return _items.front()->id;
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::vector<not_null<HistoryItem*>> &items() const {
|
||||
return _items;
|
||||
}
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
void rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void rightActionStopLastRipple() override;
|
||||
|
||||
int paintNameIconGetWidth(
|
||||
Painter &p,
|
||||
Fn<void()> repaint,
|
||||
crl::time now,
|
||||
int nameLeft,
|
||||
int nameTop,
|
||||
int nameWidth,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override {
|
||||
return 0;
|
||||
}
|
||||
QSize rightActionSize() const override {
|
||||
return peer()->isUser() ? QSize(_st->width, _st->height) : QSize();
|
||||
}
|
||||
QMargins rightActionMargins() const override {
|
||||
return QMargins(
|
||||
0,
|
||||
0,
|
||||
st::defaultPeerListItem.photoPosition.x(),
|
||||
0);
|
||||
}
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
private:
|
||||
void refreshStatus() override;
|
||||
static Type ComputeType(not_null<const HistoryItem*> item);
|
||||
static CallType ComputeCallType(not_null<const HistoryItem*> item);
|
||||
|
||||
std::vector<not_null<HistoryItem*>> _items;
|
||||
QDate _date;
|
||||
Type _type;
|
||||
not_null<const style::IconButton*> _st;
|
||||
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
|
||||
};
|
||||
|
||||
BoxController::Row::Row(not_null<HistoryItem*> item)
|
||||
: PeerListRow(item->history()->peer, item->id.bare)
|
||||
, _items(1, item)
|
||||
, _date(ItemDateTime(item).date())
|
||||
, _type(ComputeType(item))
|
||||
, _st(ComputeCallType(item) == CallType::Voice
|
||||
? &st::callReDial
|
||||
: &st::callCameraReDial) {
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void BoxController::Row::paintStatusText(Painter &p, const style::PeerListItem &st, int x, int y, int availableWidth, int outerWidth, bool selected) {
|
||||
auto icon = ([this] {
|
||||
switch (_type) {
|
||||
case Type::In: return &st::callArrowIn;
|
||||
case Type::Out: return &st::callArrowOut;
|
||||
case Type::Missed: return &st::callArrowMissed;
|
||||
}
|
||||
Unexpected("_type in Calls::BoxController::Row::paintStatusText().");
|
||||
})();
|
||||
icon->paint(p, x + st::callArrowPosition.x(), y + st::callArrowPosition.y(), outerWidth);
|
||||
auto shift = st::callArrowPosition.x() + icon->width() + st::callArrowSkip;
|
||||
x += shift;
|
||||
availableWidth -= shift;
|
||||
|
||||
PeerListRow::paintStatusText(p, st, x, y, availableWidth, outerWidth, selected);
|
||||
}
|
||||
|
||||
void BoxController::Row::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = rightActionSize();
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
x + _st->rippleAreaPosition.x(),
|
||||
y + _st->rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
_st->icon.paintInCenter(
|
||||
p,
|
||||
style::rtlrect(x, y, size.width(), size.height(), outerWidth));
|
||||
}
|
||||
|
||||
void BoxController::Row::refreshStatus() {
|
||||
if (!hasItems()) {
|
||||
return;
|
||||
}
|
||||
auto text = [this] {
|
||||
auto time = QLocale().toString(ItemDateTime(_items.front()).time(), QLocale::ShortFormat);
|
||||
auto today = QDateTime::currentDateTime().date();
|
||||
if (_date == today) {
|
||||
return tr::lng_call_box_status_today(tr::now, lt_time, time);
|
||||
} else if (_date.addDays(1) == today) {
|
||||
return tr::lng_call_box_status_yesterday(tr::now, lt_time, time);
|
||||
}
|
||||
return tr::lng_call_box_status_date(tr::now, lt_date, langDayOfMonthFull(_date), lt_time, time);
|
||||
};
|
||||
setCustomStatus((_items.size() > 1)
|
||||
? tr::lng_call_box_status_group(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
QString::number(_items.size()),
|
||||
lt_status,
|
||||
text())
|
||||
: text());
|
||||
}
|
||||
|
||||
BoxController::Row::Type BoxController::Row::ComputeType(
|
||||
not_null<const HistoryItem*> item) {
|
||||
if (item->out()) {
|
||||
return Type::Out;
|
||||
} else if (auto media = item->media()) {
|
||||
if (const auto call = media->call()) {
|
||||
using State = Data::CallState;
|
||||
const auto state = call->state;
|
||||
if (state == State::Busy || state == State::Missed) {
|
||||
return Type::Missed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Type::In;
|
||||
}
|
||||
|
||||
BoxController::Row::CallType BoxController::Row::ComputeCallType(
|
||||
not_null<const HistoryItem*> item) {
|
||||
if (auto media = item->media()) {
|
||||
if (const auto call = media->call()) {
|
||||
if (call->video) {
|
||||
return CallType::Video;
|
||||
}
|
||||
}
|
||||
}
|
||||
return CallType::Voice;
|
||||
}
|
||||
|
||||
void BoxController::Row::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(
|
||||
QSize(_st->rippleAreaSize, _st->rippleAreaSize));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
_st->ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point - _st->rippleAreaPosition);
|
||||
}
|
||||
|
||||
void BoxController::Row::rightActionStopLastRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
BoxController::BoxController(not_null<::Window::SessionController*> window)
|
||||
: _window(window)
|
||||
, _api(&_window->session().mtp()) {
|
||||
}
|
||||
|
||||
Main::Session &BoxController::session() const {
|
||||
return _window->session();
|
||||
}
|
||||
|
||||
void BoxController::prepare() {
|
||||
session().data().itemRemoved(
|
||||
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
|
||||
if (const auto row = rowForItem(item)) {
|
||||
row->itemRemoved(item);
|
||||
if (!row->hasItems()) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
if (!delegate()->peerListFullRowsCount()) {
|
||||
refreshAbout();
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
session().changes().messageUpdates(
|
||||
Data::MessageUpdate::Flag::NewAdded
|
||||
) | rpl::filter([=](const Data::MessageUpdate &update) {
|
||||
const auto media = update.item->media();
|
||||
return (media != nullptr) && (media->call() != nullptr);
|
||||
}) | rpl::on_next([=](const Data::MessageUpdate &update) {
|
||||
insertRow(update.item, InsertWay::Prepend);
|
||||
}, lifetime());
|
||||
|
||||
delegate()->peerListSetTitle(tr::lng_call_box_title());
|
||||
setDescriptionText(tr::lng_contacts_loading(tr::now));
|
||||
delegate()->peerListRefreshRows();
|
||||
|
||||
loadMoreRows();
|
||||
}
|
||||
|
||||
void BoxController::loadMoreRows() {
|
||||
if (_loadRequestId || _allLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
_loadRequestId = _api.request(MTPmessages_Search(
|
||||
MTP_flags(0),
|
||||
MTP_inputPeerEmpty(),
|
||||
MTP_string(), // q
|
||||
MTP_inputPeerEmpty(),
|
||||
MTPInputPeer(), // saved_peer_id
|
||||
MTPVector<MTPReaction>(), // saved_reaction
|
||||
MTPint(), // top_msg_id
|
||||
MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)),
|
||||
MTP_int(0), // min_date
|
||||
MTP_int(0), // max_date
|
||||
MTP_int(_offsetId),
|
||||
MTP_int(0), // add_offset
|
||||
MTP_int(_offsetId ? kFirstPageCount : kPerPageCount),
|
||||
MTP_int(0), // max_id
|
||||
MTP_int(0), // min_id
|
||||
MTP_long(0) // hash
|
||||
)).done([this](const MTPmessages_Messages &result) {
|
||||
_loadRequestId = 0;
|
||||
|
||||
auto handleResult = [&](auto &data) {
|
||||
session().data().processUsers(data.vusers());
|
||||
session().data().processChats(data.vchats());
|
||||
receivedCalls(data.vmessages().v);
|
||||
};
|
||||
|
||||
switch (result.type()) {
|
||||
case mtpc_messages_messages: handleResult(result.c_messages_messages()); _allLoaded = true; break;
|
||||
case mtpc_messages_messagesSlice: handleResult(result.c_messages_messagesSlice()); break;
|
||||
case mtpc_messages_channelMessages: {
|
||||
LOG(("API Error: received messages.channelMessages! (Calls::BoxController::preloadRows)"));
|
||||
handleResult(result.c_messages_channelMessages());
|
||||
} break;
|
||||
case mtpc_messages_messagesNotModified: {
|
||||
LOG(("API Error: received messages.messagesNotModified! (Calls::BoxController::preloadRows)"));
|
||||
} break;
|
||||
default: Unexpected("Type of messages.Messages (Calls::BoxController::preloadRows)");
|
||||
}
|
||||
}).fail([this] {
|
||||
_loadRequestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> BoxController::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
const auto &items = static_cast<Row*>(row.get())->items();
|
||||
const auto session = &this->session();
|
||||
const auto ids = session->data().itemsToIds(items);
|
||||
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
result->addAction(tr::lng_context_delete_selected(tr::now), [=] {
|
||||
_window->show(
|
||||
Box<DeleteMessagesBox>(session, base::duplicate(ids)));
|
||||
}, &st::menuIconDelete);
|
||||
result->addAction(tr::lng_context_to_msg(tr::now), [=, window = _window] {
|
||||
if (const auto item = session->data().message(ids.front())) {
|
||||
window->showMessage(item);
|
||||
}
|
||||
}, &st::menuIconShowInChat);
|
||||
return result;
|
||||
}
|
||||
|
||||
void BoxController::refreshAbout() {
|
||||
setDescriptionText(delegate()->peerListFullRowsCount() ? QString() : tr::lng_call_box_about(tr::now));
|
||||
}
|
||||
|
||||
void BoxController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto itemsRow = static_cast<Row*>(row.get());
|
||||
const auto itemId = itemsRow->maxItemId();
|
||||
const auto window = _window;
|
||||
crl::on_main(window, [=, peer = row->peer()] {
|
||||
window->showPeerHistory(
|
||||
peer,
|
||||
::Window::SectionShow::Way::ClearStack,
|
||||
itemId);
|
||||
});
|
||||
}
|
||||
|
||||
void BoxController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
auto user = row->peer()->asUser();
|
||||
Assert(user != nullptr);
|
||||
|
||||
Core::App().calls().startOutgoingCall(user, false);
|
||||
}
|
||||
|
||||
void BoxController::receivedCalls(const QVector<MTPMessage> &result) {
|
||||
if (result.empty()) {
|
||||
_allLoaded = true;
|
||||
}
|
||||
|
||||
for (const auto &message : result) {
|
||||
const auto msgId = IdFromMessage(message);
|
||||
const auto peerId = PeerFromMessage(message);
|
||||
if (session().data().peerLoaded(peerId)) {
|
||||
const auto item = session().data().addNewMessage(
|
||||
message,
|
||||
MessageFlags(),
|
||||
NewMessageType::Existing);
|
||||
insertRow(item, InsertWay::Append);
|
||||
} else {
|
||||
LOG(("API Error: a search results with not loaded peer %1"
|
||||
).arg(peerId.value));
|
||||
}
|
||||
_offsetId = msgId;
|
||||
}
|
||||
|
||||
refreshAbout();
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
bool BoxController::insertRow(
|
||||
not_null<HistoryItem*> item,
|
||||
InsertWay way) {
|
||||
if (auto row = rowForItem(item)) {
|
||||
if (row->canAddItem(item)) {
|
||||
row->addItem(item);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
(way == InsertWay::Append)
|
||||
? delegate()->peerListAppendRow(createRow(item))
|
||||
: delegate()->peerListPrependRow(createRow(item));
|
||||
delegate()->peerListSortRows([](
|
||||
const PeerListRow &a,
|
||||
const PeerListRow &b) {
|
||||
return static_cast<const Row&>(a).maxItemId()
|
||||
> static_cast<const Row&>(b).maxItemId();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
BoxController::Row *BoxController::rowForItem(not_null<const HistoryItem*> item) {
|
||||
auto v = delegate();
|
||||
if (auto fullRowsCount = v->peerListFullRowsCount()) {
|
||||
auto itemId = item->id;
|
||||
auto lastRow = static_cast<Row*>(v->peerListRowAt(fullRowsCount - 1).get());
|
||||
if (itemId < lastRow->minItemId()) {
|
||||
return lastRow;
|
||||
}
|
||||
auto firstRow = static_cast<Row*>(v->peerListRowAt(0).get());
|
||||
if (itemId > firstRow->maxItemId()) {
|
||||
return firstRow;
|
||||
}
|
||||
|
||||
// Binary search. Invariant:
|
||||
// 1. rowAt(left)->maxItemId() >= itemId.
|
||||
// 2. (left + 1 == fullRowsCount) OR rowAt(left + 1)->maxItemId() < itemId.
|
||||
auto left = 0;
|
||||
auto right = fullRowsCount;
|
||||
while (left + 1 < right) {
|
||||
auto middle = (right + left) / 2;
|
||||
auto middleRow = static_cast<Row*>(v->peerListRowAt(middle).get());
|
||||
if (middleRow->maxItemId() >= itemId) {
|
||||
left = middle;
|
||||
} else {
|
||||
right = middle;
|
||||
}
|
||||
}
|
||||
auto result = static_cast<Row*>(v->peerListRowAt(left).get());
|
||||
// Check for rowAt(left)->minItemId > itemId > rowAt(left + 1)->maxItemId.
|
||||
// In that case we sometimes need to return rowAt(left + 1), not rowAt(left).
|
||||
if (result->minItemId() > itemId && left + 1 < fullRowsCount) {
|
||||
auto possibleResult = static_cast<Row*>(v->peerListRowAt(left + 1).get());
|
||||
Assert(possibleResult->maxItemId() < itemId);
|
||||
if (possibleResult->canAddItem(item)) {
|
||||
return possibleResult;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> BoxController::createRow(
|
||||
not_null<HistoryItem*> item) const {
|
||||
return std::make_unique<Row>(item);
|
||||
}
|
||||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<::Window::SessionController*> window) {
|
||||
const auto weak = base::make_weak(box);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_call_box_clear_sure(),
|
||||
st::boxLabel),
|
||||
st::boxPadding);
|
||||
const auto revokeCheckbox = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::lng_delete_for_everyone_check(tr::now),
|
||||
false,
|
||||
st::defaultBoxCheckbox),
|
||||
style::margins(
|
||||
st::boxPadding.left(),
|
||||
st::boxPadding.bottom(),
|
||||
st::boxPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
|
||||
const auto api = &window->session().api();
|
||||
const auto sendRequest = [=](bool revoke, auto self) -> void {
|
||||
using Flag = MTPmessages_DeletePhoneCallHistory::Flag;
|
||||
api->request(MTPmessages_DeletePhoneCallHistory(
|
||||
MTP_flags(revoke ? Flag::f_revoke : Flag(0))
|
||||
)).done([=](const MTPmessages_AffectedFoundMessages &result) {
|
||||
result.match([&](
|
||||
const MTPDmessages_affectedFoundMessages &data) {
|
||||
api->applyUpdates(MTP_updates(
|
||||
MTP_vector<MTPUpdate>(
|
||||
1,
|
||||
MTP_updateDeleteMessages(
|
||||
data.vmessages(),
|
||||
data.vpts(),
|
||||
data.vpts_count())),
|
||||
MTP_vector<MTPUser>(),
|
||||
MTP_vector<MTPChat>(),
|
||||
MTP_int(base::unixtime::now()),
|
||||
MTP_int(0)));
|
||||
const auto offset = data.voffset().v;
|
||||
if (offset > 0) {
|
||||
self(revoke, self);
|
||||
} else {
|
||||
api->session().data().destroyAllCallItems();
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).send();
|
||||
};
|
||||
|
||||
box->addButton(tr::lng_call_box_clear_button(), [=] {
|
||||
sendRequest(revokeCheckbox->checked(), sendRequest);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateCallButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<::Window::SessionController*> controller,
|
||||
Fn<void()> done) {
|
||||
const auto result = container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
tr::lng_confcall_create_call(),
|
||||
st::inviteViaLinkButton), QMargins());
|
||||
Ui::AddSkip(container);
|
||||
Ui::AddDividerText(
|
||||
container,
|
||||
tr::lng_confcall_create_call_description(
|
||||
lt_count,
|
||||
rpl::single(controller->session().appConfig().confcallSizeLimit()
|
||||
* 1.),
|
||||
tr::marked));
|
||||
|
||||
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
|
||||
result,
|
||||
st::inviteViaLinkIcon,
|
||||
QPoint());
|
||||
result->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
icon->moveToLeft(
|
||||
st::inviteViaLinkIconPosition.x(),
|
||||
(height - st::inviteViaLinkIcon.height()) / 2);
|
||||
}, icon->lifetime());
|
||||
|
||||
result->setClickedCallback([=] {
|
||||
controller->show(Group::PrepareCreateCallBox(controller, done));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShowCallsBox(not_null<::Window::SessionController*> window) {
|
||||
struct State {
|
||||
State(not_null<::Window::SessionController*> window)
|
||||
: callsController(window)
|
||||
, groupCallsController(window) {
|
||||
}
|
||||
Calls::BoxController callsController;
|
||||
PeerListContentDelegateSimple callsDelegate;
|
||||
|
||||
Calls::GroupCalls::ListController groupCallsController;
|
||||
PeerListContentDelegateSimple groupCallsDelegate;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
|
||||
window->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
const auto state = box->lifetime().make_state<State>(window);
|
||||
|
||||
const auto groupCalls = box->addRow(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
box,
|
||||
object_ptr<Ui::VerticalLayout>(box)),
|
||||
style::margins());
|
||||
groupCalls->hide(anim::type::instant);
|
||||
groupCalls->toggleOn(state->groupCallsController.shownValue());
|
||||
|
||||
Ui::AddSubsectionTitle(
|
||||
groupCalls->entity(),
|
||||
tr::lng_call_box_groupcalls_subtitle());
|
||||
state->groupCallsDelegate.setContent(groupCalls->entity()->add(
|
||||
object_ptr<PeerListContent>(box, &state->groupCallsController)));
|
||||
state->groupCallsController.setDelegate(&state->groupCallsDelegate);
|
||||
Ui::AddSkip(groupCalls->entity());
|
||||
Ui::AddDivider(groupCalls->entity());
|
||||
Ui::AddSkip(groupCalls->entity());
|
||||
|
||||
const auto button = AddCreateCallButton(
|
||||
box->verticalLayout(),
|
||||
window,
|
||||
crl::guard(box, [=] { box->closeBox(); }));
|
||||
button->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Enter);
|
||||
}) | rpl::on_next([=] {
|
||||
state->callsDelegate.peerListMouseLeftGeometry();
|
||||
}, button->lifetime());
|
||||
|
||||
const auto content = box->addRow(
|
||||
object_ptr<PeerListContent>(box, &state->callsController),
|
||||
style::margins());
|
||||
state->callsDelegate.setContent(content);
|
||||
state->callsController.setDelegate(&state->callsDelegate);
|
||||
|
||||
box->setWidth(state->callsController.contentWidth());
|
||||
state->callsController.boxHeightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
box->setMinHeight(height);
|
||||
}, box->lifetime());
|
||||
box->setTitle(tr::lng_call_box_title());
|
||||
box->addButton(tr::lng_close(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
const auto menuButton = box->addTopButton(st::infoTopBarMenu);
|
||||
menuButton->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
menuButton,
|
||||
st::popupMenuWithIcons);
|
||||
const auto showSettings = [=] {
|
||||
window->showSettings(
|
||||
Settings::Calls::Id(),
|
||||
::Window::SectionShow(anim::type::instant));
|
||||
};
|
||||
const auto clearAll = crl::guard(box, [=] {
|
||||
box->uiShow()->showBox(Box(Calls::ClearCallsBox, window));
|
||||
});
|
||||
state->menu->addAction(
|
||||
tr::lng_settings_section_call_settings(tr::now),
|
||||
showSettings,
|
||||
&st::menuIconSettings);
|
||||
if (state->callsDelegate.peerListFullRowsCount() > 0) {
|
||||
Ui::Menu::CreateAddActionCallback(state->menu)({
|
||||
.text = tr::lng_call_box_clear_all(tr::now),
|
||||
.handler = clearAll,
|
||||
.icon = &st::menuIconDeleteAttention,
|
||||
.isAttention = true,
|
||||
});
|
||||
}
|
||||
state->menu->popup(QCursor::pos());
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
86
Telegram/SourceFiles/calls/calls_box_controller.h
Normal file
86
Telegram/SourceFiles/calls/calls_box_controller.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
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 "ui/layers/generic_box.h"
|
||||
#include "mtproto/sender.h"
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Calls {
|
||||
namespace GroupCalls {
|
||||
|
||||
class ListController : public PeerListController {
|
||||
public:
|
||||
explicit ListController(not_null<::Window::SessionController*> window);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> shownValue() const;
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void rowRightActionClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
const not_null<::Window::SessionController*> _window;
|
||||
base::flat_map<PeerId, not_null<PeerListRow*>> _groupCalls;
|
||||
rpl::variable<int> _fullCount;
|
||||
|
||||
};
|
||||
|
||||
} // namespace GroupCalls
|
||||
|
||||
class BoxController : public PeerListController {
|
||||
public:
|
||||
explicit BoxController(not_null<::Window::SessionController*> window);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void rowRightActionClicked(not_null<PeerListRow*> row) override;
|
||||
void loadMoreRows() override;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
void receivedCalls(const QVector<MTPMessage> &result);
|
||||
void refreshAbout();
|
||||
|
||||
class GroupCallRow;
|
||||
class Row;
|
||||
Row *rowForItem(not_null<const HistoryItem*> item);
|
||||
|
||||
enum class InsertWay {
|
||||
Append,
|
||||
Prepend,
|
||||
};
|
||||
bool insertRow(not_null<HistoryItem*> item, InsertWay way);
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<HistoryItem*> item) const;
|
||||
|
||||
const not_null<::Window::SessionController*> _window;
|
||||
MTP::Sender _api;
|
||||
|
||||
MsgId _offsetId = 0;
|
||||
int _loadRequestId = 0; // Not a real mtpRequestId.
|
||||
bool _allLoaded = false;
|
||||
|
||||
};
|
||||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<::Window::SessionController*> window);
|
||||
|
||||
void ShowCallsBox(not_null<::Window::SessionController*> window);
|
||||
|
||||
} // namespace Calls
|
||||
1639
Telegram/SourceFiles/calls/calls_call.cpp
Normal file
1639
Telegram/SourceFiles/calls/calls_call.cpp
Normal file
File diff suppressed because it is too large
Load Diff
381
Telegram/SourceFiles/calls/calls_call.h
Normal file
381
Telegram/SourceFiles/calls/calls_call.h
Normal file
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
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_resolver.h"
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
class Track;
|
||||
} // namespace Audio
|
||||
} // namespace Media
|
||||
|
||||
namespace tgcalls {
|
||||
class Instance;
|
||||
class VideoCaptureInterface;
|
||||
enum class State;
|
||||
enum class VideoState;
|
||||
enum class AudioState;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Webrtc {
|
||||
enum class VideoState;
|
||||
class VideoTrack;
|
||||
struct DeviceResolvedId;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls {
|
||||
|
||||
struct StartConferenceInfo;
|
||||
|
||||
struct DhConfig {
|
||||
int32 version = 0;
|
||||
int32 g = 0;
|
||||
bytes::vector p;
|
||||
};
|
||||
|
||||
enum class ErrorType {
|
||||
NoCamera,
|
||||
NoMicrophone,
|
||||
NotStartedCall,
|
||||
NotVideoCall,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct Error {
|
||||
ErrorType type = ErrorType::Unknown;
|
||||
QString details;
|
||||
};
|
||||
|
||||
enum class CallType {
|
||||
Incoming,
|
||||
Outgoing,
|
||||
};
|
||||
|
||||
class Call final
|
||||
: public base::has_weak_ptr
|
||||
, private Webrtc::CaptureMuteTracker {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
virtual DhConfig getDhConfig() const = 0;
|
||||
virtual void callFinished(not_null<Call*> call) = 0;
|
||||
virtual void callFailed(not_null<Call*> call) = 0;
|
||||
virtual void callRedial(not_null<Call*> call) = 0;
|
||||
|
||||
enum class CallSound {
|
||||
Connecting,
|
||||
Busy,
|
||||
Ended,
|
||||
};
|
||||
virtual void callPlaySound(CallSound sound) = 0;
|
||||
virtual void callRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess,
|
||||
bool video) = 0;
|
||||
|
||||
virtual auto callGetVideoCapture(
|
||||
const QString &deviceId,
|
||||
bool isScreenCapture)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
};
|
||||
|
||||
static constexpr auto kSoundSampleMs = 100;
|
||||
|
||||
using Type = CallType;
|
||||
Call(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<UserData*> user,
|
||||
Type type,
|
||||
bool video);
|
||||
Call(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<UserData*> user,
|
||||
CallId conferenceId,
|
||||
MsgId conferenceInviteMsgId,
|
||||
std::vector<not_null<PeerData*>> conferenceParticipants,
|
||||
bool video);
|
||||
|
||||
[[nodiscard]] Type type() const {
|
||||
return _type;
|
||||
}
|
||||
[[nodiscard]] not_null<UserData*> user() const {
|
||||
return _user;
|
||||
}
|
||||
[[nodiscard]] CallId id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] bool conferenceInvite() const {
|
||||
return _conferenceId != 0;
|
||||
}
|
||||
[[nodiscard]] CallId conferenceId() const {
|
||||
return _conferenceId;
|
||||
}
|
||||
[[nodiscard]] MsgId conferenceInviteMsgId() const {
|
||||
return _conferenceInviteMsgId;
|
||||
}
|
||||
[[nodiscard]] auto conferenceParticipants() const
|
||||
-> const std::vector<not_null<PeerData*>> & {
|
||||
return _conferenceParticipants;
|
||||
}
|
||||
[[nodiscard]] bool isIncomingWaiting() const;
|
||||
|
||||
void start(bytes::const_span random);
|
||||
bool handleUpdate(const MTPPhoneCall &call);
|
||||
bool handleSignalingData(const MTPDupdatePhoneCallSignalingData &data);
|
||||
|
||||
enum State {
|
||||
Starting,
|
||||
WaitingInit,
|
||||
WaitingInitAck,
|
||||
Established,
|
||||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
MigrationHangingUp,
|
||||
Ended,
|
||||
EndedByOtherDevice,
|
||||
ExchangingKeys,
|
||||
Waiting,
|
||||
Requesting,
|
||||
WaitingIncoming,
|
||||
Ringing,
|
||||
Busy,
|
||||
WaitingUserConfirmation,
|
||||
};
|
||||
[[nodiscard]] State state() const {
|
||||
return _state.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Error> errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> confereceSupportedValue() const {
|
||||
return _conferenceSupported.value();
|
||||
}
|
||||
|
||||
enum class RemoteAudioState {
|
||||
Muted,
|
||||
Active,
|
||||
};
|
||||
[[nodiscard]] RemoteAudioState remoteAudioState() const {
|
||||
return _remoteAudioState.current();
|
||||
}
|
||||
[[nodiscard]] auto remoteAudioStateValue() const
|
||||
-> rpl::producer<RemoteAudioState> {
|
||||
return _remoteAudioState.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] Webrtc::VideoState remoteVideoState() const {
|
||||
return _remoteVideoState.current();
|
||||
}
|
||||
[[nodiscard]] auto remoteVideoStateValue() const
|
||||
-> rpl::producer<Webrtc::VideoState> {
|
||||
return _remoteVideoState.value();
|
||||
}
|
||||
|
||||
enum class RemoteBatteryState {
|
||||
Low,
|
||||
Normal,
|
||||
};
|
||||
[[nodiscard]] RemoteBatteryState remoteBatteryState() const {
|
||||
return _remoteBatteryState.current();
|
||||
}
|
||||
[[nodiscard]] auto remoteBatteryStateValue() const
|
||||
-> rpl::producer<RemoteBatteryState> {
|
||||
return _remoteBatteryState.value();
|
||||
}
|
||||
|
||||
static constexpr auto kSignalBarStarting = -1;
|
||||
static constexpr auto kSignalBarFinished = -2;
|
||||
static constexpr auto kSignalBarCount = 4;
|
||||
[[nodiscard]] rpl::producer<int> signalBarCountValue() const {
|
||||
return _signalBarCount.value();
|
||||
}
|
||||
|
||||
void setMuted(bool mute);
|
||||
[[nodiscard]] bool muted() const {
|
||||
return _muted.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> mutedValue() const {
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Webrtc::VideoTrack*> videoIncoming() const;
|
||||
[[nodiscard]] not_null<Webrtc::VideoTrack*> videoOutgoing() const;
|
||||
|
||||
crl::time getDurationMs() const;
|
||||
float64 getWaitingSoundPeakValue() const;
|
||||
|
||||
void applyUserConfirmation();
|
||||
void answer();
|
||||
void hangup(
|
||||
Data::GroupCall *migrateCall = nullptr,
|
||||
const QString &migrateSlug = QString());
|
||||
void redial();
|
||||
|
||||
bool isKeyShaForFingerprintReady() const;
|
||||
bytes::vector getKeyShaForFingerprint() const;
|
||||
|
||||
QString getDebugLog() const;
|
||||
|
||||
//void setAudioVolume(bool input, float level);
|
||||
void setAudioDuckingEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] QString videoDeviceId() const {
|
||||
return _videoCaptureDeviceId;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isSharingVideo() const;
|
||||
[[nodiscard]] bool isSharingCamera() const;
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] QString cameraSharingDeviceId() const;
|
||||
[[nodiscard]] QString screenSharingDeviceId() const;
|
||||
void toggleCameraSharing(bool enabled);
|
||||
void toggleScreenSharing(std::optional<QString> uniqueId);
|
||||
[[nodiscard]] auto peekVideoCapture() const
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
|
||||
|
||||
[[nodiscard]] auto playbackDeviceIdValue() const
|
||||
-> rpl::producer<Webrtc::DeviceResolvedId>;
|
||||
[[nodiscard]] auto captureDeviceIdValue() const
|
||||
-> rpl::producer<Webrtc::DeviceResolvedId>;
|
||||
[[nodiscard]] auto cameraDeviceIdValue() const
|
||||
-> rpl::producer<Webrtc::DeviceResolvedId>;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
~Call();
|
||||
|
||||
private:
|
||||
enum class FinishType {
|
||||
None,
|
||||
Ended,
|
||||
Failed,
|
||||
};
|
||||
|
||||
void handleRequestError(const QString &error);
|
||||
void handleControllerError(const QString &error);
|
||||
void finish(
|
||||
FinishType type,
|
||||
const MTPPhoneCallDiscardReason &reason
|
||||
= MTP_phoneCallDiscardReasonDisconnect(),
|
||||
Data::GroupCall *migrateCall = nullptr);
|
||||
void finishByMigration(const QString &slug);
|
||||
void startOutgoing();
|
||||
void startIncoming();
|
||||
void startWaitingTrack();
|
||||
void sendSignalingData(const QByteArray &data);
|
||||
|
||||
void generateModExpFirst(bytes::const_span randomSeed);
|
||||
void handleControllerStateChange(tgcalls::State state);
|
||||
void handleControllerBarCountChange(int count);
|
||||
void createAndStartController(const MTPDphoneCall &call);
|
||||
|
||||
template <typename T>
|
||||
bool checkCallCommonFields(const T &call);
|
||||
bool checkCallFields(const MTPDphoneCall &call);
|
||||
bool checkCallFields(const MTPDphoneCallAccepted &call);
|
||||
|
||||
void actuallyAnswer();
|
||||
void acceptConferenceInvite();
|
||||
void confirmAcceptedCall(const MTPDphoneCallAccepted &call);
|
||||
void startConfirmedCall(const MTPDphoneCall &call);
|
||||
void setState(State state);
|
||||
void setStateQueued(State state);
|
||||
void setFailedQueued(const QString &error);
|
||||
void setSignalBarCount(int count);
|
||||
void destroyController();
|
||||
|
||||
void captureMuteChanged(bool mute) override;
|
||||
rpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;
|
||||
|
||||
void setupMediaDevices();
|
||||
void setupOutgoingVideo();
|
||||
void updateRemoteMediaState(
|
||||
tgcalls::AudioState audio,
|
||||
tgcalls::VideoState video);
|
||||
|
||||
[[nodiscard]] StartConferenceInfo migrateConferenceInfo(
|
||||
StartConferenceInfo extend);
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
const not_null<UserData*> _user;
|
||||
MTP::Sender _api;
|
||||
Type _type = Type::Outgoing;
|
||||
rpl::variable<State> _state = State::Starting;
|
||||
rpl::variable<bool> _conferenceSupported = false;
|
||||
rpl::variable<RemoteAudioState> _remoteAudioState
|
||||
= RemoteAudioState::Active;
|
||||
rpl::variable<Webrtc::VideoState> _remoteVideoState;
|
||||
rpl::variable<RemoteBatteryState> _remoteBatteryState
|
||||
= RemoteBatteryState::Normal;
|
||||
rpl::event_stream<Error> _errors;
|
||||
FinishType _finishAfterRequestingCall = FinishType::None;
|
||||
bool _answerAfterDhConfigReceived = false;
|
||||
rpl::variable<int> _signalBarCount = kSignalBarStarting;
|
||||
crl::time _startTime = 0;
|
||||
base::DelayedCallTimer _finishByTimeoutTimer;
|
||||
base::Timer _discardByTimeoutTimer;
|
||||
|
||||
Fn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;
|
||||
Webrtc::DeviceResolver _playbackDeviceId;
|
||||
Webrtc::DeviceResolver _captureDeviceId;
|
||||
Webrtc::DeviceResolver _cameraDeviceId;
|
||||
|
||||
rpl::variable<bool> _muted = false;
|
||||
|
||||
DhConfig _dhConfig;
|
||||
bytes::vector _ga;
|
||||
bytes::vector _gb;
|
||||
bytes::vector _gaHash;
|
||||
bytes::vector _randomPower;
|
||||
MTP::AuthKey::Data _authKey;
|
||||
|
||||
CallId _id = 0;
|
||||
uint64 _accessHash = 0;
|
||||
uint64 _keyFingerprint = 0;
|
||||
|
||||
CallId _conferenceId = 0;
|
||||
MsgId _conferenceInviteMsgId = 0;
|
||||
std::vector<not_null<PeerData*>> _conferenceParticipants;
|
||||
|
||||
std::unique_ptr<tgcalls::Instance> _instance;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
|
||||
QString _videoCaptureDeviceId;
|
||||
bool _videoCaptureIsScreencast = false;
|
||||
const std::unique_ptr<Webrtc::VideoTrack> _videoIncoming;
|
||||
const std::unique_ptr<Webrtc::VideoTrack> _videoOutgoing;
|
||||
|
||||
std::unique_ptr<Media::Audio::Track> _waitingTrack;
|
||||
|
||||
rpl::lifetime _instanceLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
void UpdateConfig(const std::string &data);
|
||||
|
||||
} // namespace Calls
|
||||
53
Telegram/SourceFiles/calls/calls_controller.cpp
Normal file
53
Telegram/SourceFiles/calls/calls_controller.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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/calls_controller.h"
|
||||
|
||||
#include "calls/calls_controller_tgvoip.h"
|
||||
#include "calls/calls_controller_webrtc.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Controller> MakeController(
|
||||
const std::string &version,
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey,
|
||||
Fn<void(QByteArray)> sendSignalingData,
|
||||
Fn<void(QImage)> displayNextFrame) {
|
||||
if (version == WebrtcController::Version()) {
|
||||
return std::make_unique<WebrtcController>(
|
||||
config,
|
||||
persistentState,
|
||||
endpoints,
|
||||
proxy,
|
||||
initialNetworkType,
|
||||
encryptionKey,
|
||||
std::move(sendSignalingData),
|
||||
std::move(displayNextFrame));
|
||||
}
|
||||
return std::make_unique<TgVoipController>(
|
||||
config,
|
||||
persistentState,
|
||||
endpoints,
|
||||
proxy,
|
||||
initialNetworkType,
|
||||
encryptionKey);
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectControllerVersions() {
|
||||
return { WebrtcController::Version(), TgVoipController::Version() };
|
||||
}
|
||||
|
||||
int ControllerMaxLayer() {
|
||||
return TgVoip::getConnectionMaxLayer();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
59
Telegram/SourceFiles/calls/calls_controller.h
Normal file
59
Telegram/SourceFiles/calls/calls_controller.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
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 <TgVoip.h>
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Controller {
|
||||
public:
|
||||
virtual ~Controller() = default;
|
||||
|
||||
[[nodiscard]] virtual std::string version() = 0;
|
||||
|
||||
virtual void setNetworkType(TgVoipNetworkType networkType) = 0;
|
||||
virtual void setMuteMicrophone(bool muteMicrophone) = 0;
|
||||
virtual void setAudioOutputGainControlEnabled(bool enabled) = 0;
|
||||
virtual void setEchoCancellationStrength(int strength) = 0;
|
||||
virtual void setAudioInputDevice(std::string id) = 0;
|
||||
virtual void setAudioOutputDevice(std::string id) = 0;
|
||||
virtual void setInputVolume(float level) = 0;
|
||||
virtual void setOutputVolume(float level) = 0;
|
||||
virtual void setAudioOutputDuckingEnabled(bool enabled) = 0;
|
||||
virtual bool receiveSignalingData(const QByteArray &data) = 0;
|
||||
|
||||
virtual std::string getLastError() = 0;
|
||||
virtual std::string getDebugInfo() = 0;
|
||||
virtual int64_t getPreferredRelayId() = 0;
|
||||
virtual TgVoipTrafficStats getTrafficStats() = 0;
|
||||
virtual TgVoipPersistentState getPersistentState() = 0;
|
||||
|
||||
virtual void setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) = 0;
|
||||
virtual void setOnSignalBarsUpdated(
|
||||
Fn<void(int)> onSignalBarsUpdated) = 0;
|
||||
|
||||
virtual TgVoipFinalState stop() = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Controller> MakeController(
|
||||
const std::string &version,
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey,
|
||||
Fn<void(QByteArray)> sendSignalingData,
|
||||
Fn<void(QImage)> displayNextFrame);
|
||||
|
||||
[[nodiscard]] std::vector<std::string> CollectControllerVersions();
|
||||
[[nodiscard]] int ControllerMaxLayer();
|
||||
|
||||
} // namespace Calls
|
||||
99
Telegram/SourceFiles/calls/calls_controller_tgvoip.h
Normal file
99
Telegram/SourceFiles/calls/calls_controller_tgvoip.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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/calls_controller.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class TgVoipController final : public Controller {
|
||||
public:
|
||||
TgVoipController(
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey)
|
||||
: _impl(TgVoip::makeInstance(
|
||||
config,
|
||||
persistentState,
|
||||
endpoints,
|
||||
proxy,
|
||||
initialNetworkType,
|
||||
encryptionKey)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::string Version() {
|
||||
return TgVoip::getVersion();
|
||||
}
|
||||
|
||||
std::string version() override {
|
||||
return Version();
|
||||
}
|
||||
void setNetworkType(TgVoipNetworkType networkType) override {
|
||||
_impl->setNetworkType(networkType);
|
||||
}
|
||||
void setMuteMicrophone(bool muteMicrophone) override {
|
||||
_impl->setMuteMicrophone(muteMicrophone);
|
||||
}
|
||||
void setAudioOutputGainControlEnabled(bool enabled) override {
|
||||
_impl->setAudioOutputGainControlEnabled(enabled);
|
||||
}
|
||||
void setEchoCancellationStrength(int strength) override {
|
||||
_impl->setEchoCancellationStrength(strength);
|
||||
}
|
||||
void setAudioInputDevice(std::string id) override {
|
||||
_impl->setAudioInputDevice(id);
|
||||
}
|
||||
void setAudioOutputDevice(std::string id) override {
|
||||
_impl->setAudioOutputDevice(id);
|
||||
}
|
||||
void setInputVolume(float level) override {
|
||||
_impl->setInputVolume(level);
|
||||
}
|
||||
void setOutputVolume(float level) override {
|
||||
_impl->setOutputVolume(level);
|
||||
}
|
||||
void setAudioOutputDuckingEnabled(bool enabled) override {
|
||||
_impl->setAudioOutputDuckingEnabled(enabled);
|
||||
}
|
||||
bool receiveSignalingData(const QByteArray &data) override {
|
||||
return false;
|
||||
}
|
||||
std::string getLastError() override {
|
||||
return _impl->getLastError();
|
||||
}
|
||||
std::string getDebugInfo() override {
|
||||
return _impl->getDebugInfo();
|
||||
}
|
||||
int64_t getPreferredRelayId() override {
|
||||
return _impl->getPreferredRelayId();
|
||||
}
|
||||
TgVoipTrafficStats getTrafficStats() override {
|
||||
return _impl->getTrafficStats();
|
||||
}
|
||||
TgVoipPersistentState getPersistentState() override {
|
||||
return _impl->getPersistentState();
|
||||
}
|
||||
void setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) override {
|
||||
_impl->setOnStateUpdated(std::move(onStateUpdated));
|
||||
}
|
||||
void setOnSignalBarsUpdated(Fn<void(int)> onSignalBarsUpdated) override {
|
||||
_impl->setOnSignalBarsUpdated(std::move(onSignalBarsUpdated));
|
||||
}
|
||||
TgVoipFinalState stop() override {
|
||||
return _impl->stop();
|
||||
}
|
||||
|
||||
private:
|
||||
const std::unique_ptr<TgVoip> _impl;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
175
Telegram/SourceFiles/calls/calls_controller_webrtc.cpp
Normal file
175
Telegram/SourceFiles/calls/calls_controller_webrtc.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
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/calls_controller_webrtc.h"
|
||||
|
||||
#include "webrtc/webrtc_call_context.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
using namespace Webrtc;
|
||||
|
||||
[[nodiscard]] CallConnectionDescription ConvertEndpoint(const TgVoipEndpoint &data) {
|
||||
return CallConnectionDescription{
|
||||
.ip = QString::fromStdString(data.host.ipv4),
|
||||
.ipv6 = QString::fromStdString(data.host.ipv6),
|
||||
.peerTag = QByteArray(
|
||||
reinterpret_cast<const char*>(data.peerTag),
|
||||
base::array_size(data.peerTag)),
|
||||
.connectionId = data.endpointId,
|
||||
.port = data.port,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] CallContext::Config MakeContextConfig(
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey,
|
||||
Fn<void(QByteArray)> sendSignalingData,
|
||||
Fn<void(QImage)> displayNextFrame) {
|
||||
Expects(!endpoints.empty());
|
||||
|
||||
auto result = CallContext::Config{
|
||||
.proxy = (proxy
|
||||
? ProxyServer{
|
||||
.host = QString::fromStdString(proxy->host),
|
||||
.username = QString::fromStdString(proxy->login),
|
||||
.password = QString::fromStdString(proxy->password),
|
||||
.port = proxy->port }
|
||||
: ProxyServer()),
|
||||
.dataSaving = (config.dataSaving != TgVoipDataSaving::Never),
|
||||
.key = QByteArray(
|
||||
reinterpret_cast<const char*>(encryptionKey.value.data()),
|
||||
encryptionKey.value.size()),
|
||||
.outgoing = encryptionKey.isOutgoing,
|
||||
.primary = ConvertEndpoint(endpoints.front()),
|
||||
.alternatives = endpoints | ranges::views::drop(
|
||||
1
|
||||
) | ranges::views::transform(ConvertEndpoint) | ranges::to_vector,
|
||||
.maxLayer = config.maxApiLayer,
|
||||
.allowP2P = config.enableP2P,
|
||||
.sendSignalingData = std::move(sendSignalingData),
|
||||
.displayNextFrame = std::move(displayNextFrame),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WebrtcController::WebrtcController(
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey,
|
||||
Fn<void(QByteArray)> sendSignalingData,
|
||||
Fn<void(QImage)> displayNextFrame)
|
||||
: _impl(std::make_unique<CallContext>(MakeContextConfig(
|
||||
config,
|
||||
persistentState,
|
||||
endpoints,
|
||||
proxy,
|
||||
initialNetworkType,
|
||||
encryptionKey,
|
||||
std::move(sendSignalingData),
|
||||
std::move(displayNextFrame)))) {
|
||||
}
|
||||
|
||||
WebrtcController::~WebrtcController() = default;
|
||||
|
||||
std::string WebrtcController::Version() {
|
||||
return CallContext::Version().toStdString();
|
||||
}
|
||||
|
||||
std::string WebrtcController::version() {
|
||||
return Version();
|
||||
}
|
||||
|
||||
void WebrtcController::setNetworkType(TgVoipNetworkType networkType) {
|
||||
}
|
||||
|
||||
void WebrtcController::setMuteMicrophone(bool muteMicrophone) {
|
||||
_impl->setIsMuted(muteMicrophone);
|
||||
}
|
||||
|
||||
void WebrtcController::setAudioOutputGainControlEnabled(bool enabled) {
|
||||
}
|
||||
|
||||
void WebrtcController::setEchoCancellationStrength(int strength) {
|
||||
}
|
||||
|
||||
void WebrtcController::setAudioInputDevice(std::string id) {
|
||||
}
|
||||
|
||||
void WebrtcController::setAudioOutputDevice(std::string id) {
|
||||
}
|
||||
|
||||
void WebrtcController::setInputVolume(float level) {
|
||||
}
|
||||
|
||||
void WebrtcController::setOutputVolume(float level) {
|
||||
}
|
||||
|
||||
void WebrtcController::setAudioOutputDuckingEnabled(bool enabled) {
|
||||
}
|
||||
|
||||
bool WebrtcController::receiveSignalingData(const QByteArray &data) {
|
||||
return _impl->receiveSignalingData(data);
|
||||
}
|
||||
|
||||
std::string WebrtcController::getLastError() {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string WebrtcController::getDebugInfo() {
|
||||
return _impl->getDebugInfo().toStdString();
|
||||
}
|
||||
|
||||
int64_t WebrtcController::getPreferredRelayId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
TgVoipTrafficStats WebrtcController::getTrafficStats() {
|
||||
return {};
|
||||
}
|
||||
|
||||
TgVoipPersistentState WebrtcController::getPersistentState() {
|
||||
return TgVoipPersistentState{};
|
||||
}
|
||||
|
||||
void WebrtcController::setOnStateUpdated(
|
||||
Fn<void(TgVoipState)> onStateUpdated) {
|
||||
_stateUpdatedLifetime.destroy();
|
||||
_impl->state().changes(
|
||||
) | rpl::on_next([=](CallState state) {
|
||||
onStateUpdated([&] {
|
||||
switch (state) {
|
||||
case CallState::Initializing: return TgVoipState::WaitInit;
|
||||
case CallState::Reconnecting: return TgVoipState::Reconnecting;
|
||||
case CallState::Connected: return TgVoipState::Established;
|
||||
case CallState::Failed: return TgVoipState::Failed;
|
||||
}
|
||||
Unexpected("State value in Webrtc::CallContext::state.");
|
||||
}());
|
||||
}, _stateUpdatedLifetime);
|
||||
}
|
||||
|
||||
void WebrtcController::setOnSignalBarsUpdated(
|
||||
Fn<void(int)> onSignalBarsUpdated) {
|
||||
}
|
||||
|
||||
TgVoipFinalState WebrtcController::stop() {
|
||||
_impl->stop();
|
||||
return TgVoipFinalState();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
60
Telegram/SourceFiles/calls/calls_controller_webrtc.h
Normal file
60
Telegram/SourceFiles/calls/calls_controller_webrtc.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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/calls_controller.h"
|
||||
|
||||
namespace Webrtc {
|
||||
class CallContext;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class WebrtcController final : public Controller {
|
||||
public:
|
||||
WebrtcController(
|
||||
const TgVoipConfig &config,
|
||||
const TgVoipPersistentState &persistentState,
|
||||
const std::vector<TgVoipEndpoint> &endpoints,
|
||||
const TgVoipProxy *proxy,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
const TgVoipEncryptionKey &encryptionKey,
|
||||
Fn<void(QByteArray)> sendSignalingData,
|
||||
Fn<void(QImage)> displayNextFrame);
|
||||
~WebrtcController();
|
||||
|
||||
[[nodiscard]] static std::string Version();
|
||||
|
||||
std::string version() override;
|
||||
void setNetworkType(TgVoipNetworkType networkType) override;
|
||||
void setMuteMicrophone(bool muteMicrophone) override;
|
||||
void setAudioOutputGainControlEnabled(bool enabled) override;
|
||||
void setEchoCancellationStrength(int strength) override;
|
||||
void setAudioInputDevice(std::string id) override;
|
||||
void setAudioOutputDevice(std::string id) override;
|
||||
void setInputVolume(float level) override;
|
||||
void setOutputVolume(float level) override;
|
||||
void setAudioOutputDuckingEnabled(bool enabled) override;
|
||||
bool receiveSignalingData(const QByteArray &data) override;
|
||||
std::string getLastError() override;
|
||||
std::string getDebugInfo() override;
|
||||
int64_t getPreferredRelayId() override;
|
||||
TgVoipTrafficStats getTrafficStats() override;
|
||||
TgVoipPersistentState getPersistentState() override;
|
||||
void setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) override;
|
||||
void setOnSignalBarsUpdated(Fn<void(int)> onSignalBarsUpdated) override;
|
||||
TgVoipFinalState stop() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Webrtc::CallContext> _impl;
|
||||
|
||||
rpl::lifetime _stateUpdatedLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
837
Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp
Normal file
837
Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp
Normal file
@@ -0,0 +1,837 @@
|
||||
/*
|
||||
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/calls_emoji_fingerprint.h"
|
||||
|
||||
#include "base/random.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_user.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTooltipShowTimeoutMs = crl::time(1000);
|
||||
constexpr auto kCarouselOneDuration = crl::time(100);
|
||||
constexpr auto kStartTimeShift = crl::time(50);
|
||||
constexpr auto kEmojiInFingerprint = 4;
|
||||
constexpr auto kEmojiInCarousel = 10;
|
||||
|
||||
const ushort Data[] = {
|
||||
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
|
||||
0xd83d, 0xde0e, 0xd83d, 0xde34, 0xd83d, 0xde35, 0xd83d, 0xde08, 0xd83d, 0xde2c, 0xd83d, 0xde07,
|
||||
0xd83d, 0xde0f, 0xd83d, 0xdc6e, 0xd83d, 0xdc77, 0xd83d, 0xdc82, 0xd83d, 0xdc76, 0xd83d, 0xdc68,
|
||||
0xd83d, 0xdc69, 0xd83d, 0xdc74, 0xd83d, 0xdc75, 0xd83d, 0xde3b, 0xd83d, 0xde3d, 0xd83d, 0xde40,
|
||||
0xd83d, 0xdc7a, 0xd83d, 0xde48, 0xd83d, 0xde49, 0xd83d, 0xde4a, 0xd83d, 0xdc80, 0xd83d, 0xdc7d,
|
||||
0xd83d, 0xdca9, 0xd83d, 0xdd25, 0xd83d, 0xdca5, 0xd83d, 0xdca4, 0xd83d, 0xdc42, 0xd83d, 0xdc40,
|
||||
0xd83d, 0xdc43, 0xd83d, 0xdc45, 0xd83d, 0xdc44, 0xd83d, 0xdc4d, 0xd83d, 0xdc4e, 0xd83d, 0xdc4c,
|
||||
0xd83d, 0xdc4a, 0x270c, 0x270b, 0xd83d, 0xdc50, 0xd83d, 0xdc46, 0xd83d, 0xdc47, 0xd83d, 0xdc49,
|
||||
0xd83d, 0xdc48, 0xd83d, 0xde4f, 0xd83d, 0xdc4f, 0xd83d, 0xdcaa, 0xd83d, 0xdeb6, 0xd83c, 0xdfc3,
|
||||
0xd83d, 0xdc83, 0xd83d, 0xdc6b, 0xd83d, 0xdc6a, 0xd83d, 0xdc6c, 0xd83d, 0xdc6d, 0xd83d, 0xdc85,
|
||||
0xd83c, 0xdfa9, 0xd83d, 0xdc51, 0xd83d, 0xdc52, 0xd83d, 0xdc5f, 0xd83d, 0xdc5e, 0xd83d, 0xdc60,
|
||||
0xd83d, 0xdc55, 0xd83d, 0xdc57, 0xd83d, 0xdc56, 0xd83d, 0xdc59, 0xd83d, 0xdc5c, 0xd83d, 0xdc53,
|
||||
0xd83c, 0xdf80, 0xd83d, 0xdc84, 0xd83d, 0xdc9b, 0xd83d, 0xdc99, 0xd83d, 0xdc9c, 0xd83d, 0xdc9a,
|
||||
0xd83d, 0xdc8d, 0xd83d, 0xdc8e, 0xd83d, 0xdc36, 0xd83d, 0xdc3a, 0xd83d, 0xdc31, 0xd83d, 0xdc2d,
|
||||
0xd83d, 0xdc39, 0xd83d, 0xdc30, 0xd83d, 0xdc38, 0xd83d, 0xdc2f, 0xd83d, 0xdc28, 0xd83d, 0xdc3b,
|
||||
0xd83d, 0xdc37, 0xd83d, 0xdc2e, 0xd83d, 0xdc17, 0xd83d, 0xdc34, 0xd83d, 0xdc11, 0xd83d, 0xdc18,
|
||||
0xd83d, 0xdc3c, 0xd83d, 0xdc27, 0xd83d, 0xdc25, 0xd83d, 0xdc14, 0xd83d, 0xdc0d, 0xd83d, 0xdc22,
|
||||
0xd83d, 0xdc1b, 0xd83d, 0xdc1d, 0xd83d, 0xdc1c, 0xd83d, 0xdc1e, 0xd83d, 0xdc0c, 0xd83d, 0xdc19,
|
||||
0xd83d, 0xdc1a, 0xd83d, 0xdc1f, 0xd83d, 0xdc2c, 0xd83d, 0xdc0b, 0xd83d, 0xdc10, 0xd83d, 0xdc0a,
|
||||
0xd83d, 0xdc2b, 0xd83c, 0xdf40, 0xd83c, 0xdf39, 0xd83c, 0xdf3b, 0xd83c, 0xdf41, 0xd83c, 0xdf3e,
|
||||
0xd83c, 0xdf44, 0xd83c, 0xdf35, 0xd83c, 0xdf34, 0xd83c, 0xdf33, 0xd83c, 0xdf1e, 0xd83c, 0xdf1a,
|
||||
0xd83c, 0xdf19, 0xd83c, 0xdf0e, 0xd83c, 0xdf0b, 0x26a1, 0x2614, 0x2744, 0x26c4, 0xd83c, 0xdf00,
|
||||
0xd83c, 0xdf08, 0xd83c, 0xdf0a, 0xd83c, 0xdf93, 0xd83c, 0xdf86, 0xd83c, 0xdf83, 0xd83d, 0xdc7b,
|
||||
0xd83c, 0xdf85, 0xd83c, 0xdf84, 0xd83c, 0xdf81, 0xd83c, 0xdf88, 0xd83d, 0xdd2e, 0xd83c, 0xdfa5,
|
||||
0xd83d, 0xdcf7, 0xd83d, 0xdcbf, 0xd83d, 0xdcbb, 0x260e, 0xd83d, 0xdce1, 0xd83d, 0xdcfa, 0xd83d,
|
||||
0xdcfb, 0xd83d, 0xdd09, 0xd83d, 0xdd14, 0x23f3, 0x23f0, 0x231a, 0xd83d, 0xdd12, 0xd83d, 0xdd11,
|
||||
0xd83d, 0xdd0e, 0xd83d, 0xdca1, 0xd83d, 0xdd26, 0xd83d, 0xdd0c, 0xd83d, 0xdd0b, 0xd83d, 0xdebf,
|
||||
0xd83d, 0xdebd, 0xd83d, 0xdd27, 0xd83d, 0xdd28, 0xd83d, 0xdeaa, 0xd83d, 0xdeac, 0xd83d, 0xdca3,
|
||||
0xd83d, 0xdd2b, 0xd83d, 0xdd2a, 0xd83d, 0xdc8a, 0xd83d, 0xdc89, 0xd83d, 0xdcb0, 0xd83d, 0xdcb5,
|
||||
0xd83d, 0xdcb3, 0x2709, 0xd83d, 0xdceb, 0xd83d, 0xdce6, 0xd83d, 0xdcc5, 0xd83d, 0xdcc1, 0x2702,
|
||||
0xd83d, 0xdccc, 0xd83d, 0xdcce, 0x2712, 0x270f, 0xd83d, 0xdcd0, 0xd83d, 0xdcda, 0xd83d, 0xdd2c,
|
||||
0xd83d, 0xdd2d, 0xd83c, 0xdfa8, 0xd83c, 0xdfac, 0xd83c, 0xdfa4, 0xd83c, 0xdfa7, 0xd83c, 0xdfb5,
|
||||
0xd83c, 0xdfb9, 0xd83c, 0xdfbb, 0xd83c, 0xdfba, 0xd83c, 0xdfb8, 0xd83d, 0xdc7e, 0xd83c, 0xdfae,
|
||||
0xd83c, 0xdccf, 0xd83c, 0xdfb2, 0xd83c, 0xdfaf, 0xd83c, 0xdfc8, 0xd83c, 0xdfc0, 0x26bd, 0x26be,
|
||||
0xd83c, 0xdfbe, 0xd83c, 0xdfb1, 0xd83c, 0xdfc9, 0xd83c, 0xdfb3, 0xd83c, 0xdfc1, 0xd83c, 0xdfc7,
|
||||
0xd83c, 0xdfc6, 0xd83c, 0xdfca, 0xd83c, 0xdfc4, 0x2615, 0xd83c, 0xdf7c, 0xd83c, 0xdf7a, 0xd83c,
|
||||
0xdf77, 0xd83c, 0xdf74, 0xd83c, 0xdf55, 0xd83c, 0xdf54, 0xd83c, 0xdf5f, 0xd83c, 0xdf57, 0xd83c,
|
||||
0xdf71, 0xd83c, 0xdf5a, 0xd83c, 0xdf5c, 0xd83c, 0xdf61, 0xd83c, 0xdf73, 0xd83c, 0xdf5e, 0xd83c,
|
||||
0xdf69, 0xd83c, 0xdf66, 0xd83c, 0xdf82, 0xd83c, 0xdf70, 0xd83c, 0xdf6a, 0xd83c, 0xdf6b, 0xd83c,
|
||||
0xdf6d, 0xd83c, 0xdf6f, 0xd83c, 0xdf4e, 0xd83c, 0xdf4f, 0xd83c, 0xdf4a, 0xd83c, 0xdf4b, 0xd83c,
|
||||
0xdf52, 0xd83c, 0xdf47, 0xd83c, 0xdf49, 0xd83c, 0xdf53, 0xd83c, 0xdf51, 0xd83c, 0xdf4c, 0xd83c,
|
||||
0xdf50, 0xd83c, 0xdf4d, 0xd83c, 0xdf46, 0xd83c, 0xdf45, 0xd83c, 0xdf3d, 0xd83c, 0xdfe1, 0xd83c,
|
||||
0xdfe5, 0xd83c, 0xdfe6, 0x26ea, 0xd83c, 0xdff0, 0x26fa, 0xd83c, 0xdfed, 0xd83d, 0xddfb, 0xd83d,
|
||||
0xddfd, 0xd83c, 0xdfa0, 0xd83c, 0xdfa1, 0x26f2, 0xd83c, 0xdfa2, 0xd83d, 0xdea2, 0xd83d, 0xdea4,
|
||||
0x2693, 0xd83d, 0xde80, 0x2708, 0xd83d, 0xde81, 0xd83d, 0xde82, 0xd83d, 0xde8b, 0xd83d, 0xde8e,
|
||||
0xd83d, 0xde8c, 0xd83d, 0xde99, 0xd83d, 0xde97, 0xd83d, 0xde95, 0xd83d, 0xde9b, 0xd83d, 0xdea8,
|
||||
0xd83d, 0xde94, 0xd83d, 0xde92, 0xd83d, 0xde91, 0xd83d, 0xdeb2, 0xd83d, 0xdea0, 0xd83d, 0xde9c,
|
||||
0xd83d, 0xdea6, 0x26a0, 0xd83d, 0xdea7, 0x26fd, 0xd83c, 0xdfb0, 0xd83d, 0xddff, 0xd83c, 0xdfaa,
|
||||
0xd83c, 0xdfad, 0xd83c, 0xddef, 0xd83c, 0xddf5, 0xd83c, 0xddf0, 0xd83c, 0xddf7, 0xd83c, 0xdde9,
|
||||
0xd83c, 0xddea, 0xd83c, 0xdde8, 0xd83c, 0xddf3, 0xd83c, 0xddfa, 0xd83c, 0xddf8, 0xd83c, 0xddeb,
|
||||
0xd83c, 0xddf7, 0xd83c, 0xddea, 0xd83c, 0xddf8, 0xd83c, 0xddee, 0xd83c, 0xddf9, 0xd83c, 0xddf7,
|
||||
0xd83c, 0xddfa, 0xd83c, 0xddec, 0xd83c, 0xdde7, 0x0031, 0x20e3, 0x0032, 0x20e3, 0x0033, 0x20e3,
|
||||
0x0034, 0x20e3, 0x0035, 0x20e3, 0x0036, 0x20e3, 0x0037, 0x20e3, 0x0038, 0x20e3, 0x0039, 0x20e3,
|
||||
0x0030, 0x20e3, 0xd83d, 0xdd1f, 0x2757, 0x2753, 0x2665, 0x2666, 0xd83d, 0xdcaf, 0xd83d, 0xdd17,
|
||||
0xd83d, 0xdd31, 0xd83d, 0xdd34, 0xd83d, 0xdd35, 0xd83d, 0xdd36, 0xd83d, 0xdd37 };
|
||||
|
||||
const ushort Offsets[] = {
|
||||
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22,
|
||||
24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46,
|
||||
48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,
|
||||
72, 74, 76, 78, 80, 82, 84, 86, 87, 88, 90, 92,
|
||||
94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,
|
||||
118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140,
|
||||
142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164,
|
||||
166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188,
|
||||
190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212,
|
||||
214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236,
|
||||
238, 240, 242, 244, 246, 248, 250, 252, 254, 256, 258, 259,
|
||||
260, 261, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280,
|
||||
282, 284, 286, 288, 290, 292, 294, 295, 297, 299, 301, 303,
|
||||
305, 306, 307, 308, 310, 312, 314, 316, 318, 320, 322, 324,
|
||||
326, 328, 330, 332, 334, 336, 338, 340, 342, 344, 346, 348,
|
||||
350, 351, 353, 355, 357, 359, 360, 362, 364, 365, 366, 368,
|
||||
370, 372, 374, 376, 378, 380, 382, 384, 386, 388, 390, 392,
|
||||
394, 396, 398, 400, 402, 404, 406, 407, 408, 410, 412, 414,
|
||||
416, 418, 420, 422, 424, 426, 427, 429, 431, 433, 435, 437,
|
||||
439, 441, 443, 445, 447, 449, 451, 453, 455, 457, 459, 461,
|
||||
463, 465, 467, 469, 471, 473, 475, 477, 479, 481, 483, 485,
|
||||
487, 489, 491, 493, 495, 497, 499, 501, 503, 505, 507, 508,
|
||||
510, 511, 513, 515, 517, 519, 521, 522, 524, 526, 528, 529,
|
||||
531, 532, 534, 536, 538, 540, 542, 544, 546, 548, 550, 552,
|
||||
554, 556, 558, 560, 562, 564, 566, 567, 569, 570, 572, 574,
|
||||
576, 578, 582, 586, 590, 594, 598, 602, 606, 610, 614, 618,
|
||||
620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
|
||||
642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };
|
||||
|
||||
constexpr auto kEmojiCount = (base::array_size(Offsets) - 1);
|
||||
|
||||
uint64 ComputeEmojiIndex(bytes::const_span bytes) {
|
||||
Expects(bytes.size() == 8);
|
||||
|
||||
return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)
|
||||
| (gsl::to_integer<uint64>(bytes[1]) << 48)
|
||||
| (gsl::to_integer<uint64>(bytes[2]) << 40)
|
||||
| (gsl::to_integer<uint64>(bytes[3]) << 32)
|
||||
| (gsl::to_integer<uint64>(bytes[4]) << 24)
|
||||
| (gsl::to_integer<uint64>(bytes[5]) << 16)
|
||||
| (gsl::to_integer<uint64>(bytes[6]) << 8)
|
||||
| (gsl::to_integer<uint64>(bytes[7]));
|
||||
}
|
||||
|
||||
[[nodiscard]] EmojiPtr EmojiByIndex(int index) {
|
||||
Expects(index >= 0 && index < kEmojiCount);
|
||||
|
||||
const auto offset = Offsets[index];
|
||||
const auto size = Offsets[index + 1] - offset;
|
||||
const auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
return Ui::Emoji::Find(string);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
|
||||
if (!call->isKeyShaForFingerprintReady()) {
|
||||
return {};
|
||||
}
|
||||
return ComputeEmojiFingerprint(call->getKeyShaForFingerprint());
|
||||
}
|
||||
|
||||
std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
bytes::const_span fingerprint) {
|
||||
auto result = std::vector<EmojiPtr>();
|
||||
constexpr auto kPartSize = 8;
|
||||
for (auto partOffset = 0
|
||||
; partOffset != fingerprint.size()
|
||||
; partOffset += kPartSize) {
|
||||
const auto value = ComputeEmojiIndex(
|
||||
fingerprint.subspan(partOffset, kPartSize));
|
||||
result.push_back(EmojiByIndex(value % kEmojiCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Call*> call) {
|
||||
class EmojiTooltipShower final : public Ui::AbstractTooltipShower {
|
||||
public:
|
||||
EmojiTooltipShower(not_null<QWidget*> window, const QString &text)
|
||||
: _window(window)
|
||||
, _text(text) {
|
||||
}
|
||||
|
||||
QString tooltipText() const override {
|
||||
return _text;
|
||||
}
|
||||
QPoint tooltipPos() const override {
|
||||
return QCursor::pos();
|
||||
}
|
||||
bool tooltipWindowActive() const override {
|
||||
return _window->isActiveWindow();
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<QWidget*> _window;
|
||||
const QString _text;
|
||||
|
||||
};
|
||||
|
||||
auto result = base::make_unique_q<Ui::RpWidget>(parent);
|
||||
const auto raw = result.get();
|
||||
|
||||
// Emoji tooltip.
|
||||
const auto shower = raw->lifetime().make_state<EmojiTooltipShower>(
|
||||
parent->window(),
|
||||
tr::lng_call_fingerprint_tooltip(
|
||||
tr::now,
|
||||
lt_user,
|
||||
call->user()->name()));
|
||||
raw->setMouseTracking(true);
|
||||
raw->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseMove) {
|
||||
Ui::Tooltip::Show(kTooltipShowTimeoutMs, shower);
|
||||
} else if (e->type() == QEvent::Leave) {
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
// Signal bars.
|
||||
const auto bars = Ui::CreateChild<SignalBars>(
|
||||
raw,
|
||||
call,
|
||||
st::callPanelSignalBars);
|
||||
bars->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
// Geometry.
|
||||
const auto print = ComputeEmojiFingerprint(call);
|
||||
auto realSize = Ui::Emoji::GetSizeNormal();
|
||||
auto size = realSize / style::DevicePixelRatio();
|
||||
auto count = print.size();
|
||||
const auto printSize = QSize(
|
||||
count * size + (count - 1) * st::callFingerprintSkip,
|
||||
size);
|
||||
const auto fullPrintSize = QRect(
|
||||
QPoint(),
|
||||
printSize
|
||||
).marginsAdded(st::callFingerprintPadding).size();
|
||||
const auto fullBarsSize = bars->rect().marginsAdded(
|
||||
st::callSignalBarsPadding
|
||||
).size();
|
||||
const auto fullSize = QSize(
|
||||
(fullPrintSize.width()
|
||||
+ st::callFingerprintSignalBarsSkip
|
||||
+ fullBarsSize.width()),
|
||||
fullPrintSize.height());
|
||||
raw->resize(fullSize);
|
||||
bars->moveToRight(
|
||||
st::callSignalBarsPadding.right(),
|
||||
st::callSignalBarsPadding.top());
|
||||
|
||||
// Paint.
|
||||
const auto background = raw->lifetime().make_state<QImage>(
|
||||
fullSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
background->setDevicePixelRatio(style::DevicePixelRatio());
|
||||
rpl::merge(
|
||||
rpl::single(rpl::empty),
|
||||
Ui::Emoji::Updated(),
|
||||
style::PaletteChanged()
|
||||
) | rpl::on_next([=] {
|
||||
background->fill(Qt::transparent);
|
||||
|
||||
// Prepare.
|
||||
auto p = QPainter(background);
|
||||
const auto height = fullSize.height();
|
||||
const auto fullPrintRect = QRect(QPoint(), fullPrintSize);
|
||||
const auto fullBarsRect = QRect(
|
||||
fullSize.width() - fullBarsSize.width(),
|
||||
0,
|
||||
fullBarsSize.width(),
|
||||
height);
|
||||
const auto bigRadius = height / 2;
|
||||
const auto smallRadius = st::roundRadiusSmall;
|
||||
const auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::callBgButton);
|
||||
|
||||
// Fingerprint part.
|
||||
p.setClipRect(0, 0, fullPrintSize.width() / 2, height);
|
||||
p.drawRoundedRect(fullPrintRect, bigRadius, bigRadius);
|
||||
p.setClipRect(fullPrintSize.width() / 2, 0, fullSize.width(), height);
|
||||
p.drawRoundedRect(fullPrintRect, smallRadius, smallRadius);
|
||||
|
||||
// Signal bars part.
|
||||
const auto middle = fullBarsRect.center().x();
|
||||
p.setClipRect(0, 0, middle, height);
|
||||
p.drawRoundedRect(fullBarsRect, smallRadius, smallRadius);
|
||||
p.setClipRect(middle, 0, fullBarsRect.width(), height);
|
||||
p.drawRoundedRect(fullBarsRect, bigRadius, bigRadius);
|
||||
|
||||
// Emoji.
|
||||
const auto realSize = Ui::Emoji::GetSizeNormal();
|
||||
const auto size = realSize / style::DevicePixelRatio();
|
||||
auto left = st::callFingerprintPadding.left();
|
||||
const auto top = st::callFingerprintPadding.top();
|
||||
p.setClipping(false);
|
||||
for (const auto emoji : print) {
|
||||
Ui::Emoji::Draw(p, emoji, realSize, left, top);
|
||||
left += st::callFingerprintSkip + size;
|
||||
}
|
||||
|
||||
raw->update();
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(raw).drawImage(raw->rect(), *background);
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->show();
|
||||
return result;
|
||||
}
|
||||
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint) {
|
||||
struct State {
|
||||
FingerprintBadgeState data;
|
||||
Ui::Animations::Basic animation;
|
||||
Fn<void(crl::time)> update;
|
||||
rpl::event_stream<> repaints;
|
||||
};
|
||||
const auto state = on.make_state<State>();
|
||||
|
||||
state->data.speed = 1. / kCarouselOneDuration;
|
||||
state->update = [=](crl::time now) {
|
||||
// speed-up-duration = 2 * one / speed.
|
||||
const auto one = 1.;
|
||||
const auto speedUpDuration = 2 * kCarouselOneDuration;
|
||||
const auto speed0 = one / kCarouselOneDuration;
|
||||
|
||||
auto updated = false;
|
||||
auto animating = false;
|
||||
for (auto &entry : state->data.entries) {
|
||||
if (!entry.time) {
|
||||
continue;
|
||||
}
|
||||
animating = true;
|
||||
if (entry.time >= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
const auto elapsed = (now - entry.time) * 1.;
|
||||
entry.time = now;
|
||||
|
||||
Assert(!entry.emoji || entry.sliding.size() > 1);
|
||||
const auto slideCount = entry.emoji
|
||||
? (int(entry.sliding.size()) - 1) * one
|
||||
: (kEmojiInCarousel + (elapsed / kCarouselOneDuration));
|
||||
const auto finalPosition = slideCount * one;
|
||||
const auto distance = finalPosition - entry.position;
|
||||
|
||||
const auto accelerate0 = speed0 - entry.speed;
|
||||
const auto decelerate0 = speed0;
|
||||
const auto acceleration0 = speed0 / speedUpDuration;
|
||||
const auto taccelerate0 = accelerate0 / acceleration0;
|
||||
const auto tdecelerate0 = decelerate0 / acceleration0;
|
||||
const auto paccelerate0 = entry.speed * taccelerate0
|
||||
+ acceleration0 * taccelerate0 * taccelerate0 / 2.;
|
||||
const auto pdecelerate0 = 0
|
||||
+ acceleration0 * tdecelerate0 * tdecelerate0 / 2.;
|
||||
const auto ttozero = entry.speed / acceleration0;
|
||||
if (paccelerate0 + pdecelerate0 <= distance) {
|
||||
// We have time to accelerate to speed0,
|
||||
// maybe go some time on speed0 and then decelerate to 0.
|
||||
const auto uaccelerate0 = std::min(taccelerate0, elapsed);
|
||||
const auto left = distance - paccelerate0 - pdecelerate0;
|
||||
const auto tconstant = left / speed0;
|
||||
const auto uconstant = std::min(
|
||||
tconstant,
|
||||
elapsed - uaccelerate0);
|
||||
const auto udecelerate0 = std::min(
|
||||
tdecelerate0,
|
||||
elapsed - uaccelerate0 - uconstant);
|
||||
if (udecelerate0 >= tdecelerate0) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
entry.position += entry.speed * uaccelerate0
|
||||
+ acceleration0 * uaccelerate0 * uaccelerate0 / 2.
|
||||
+ speed0 * uconstant
|
||||
+ speed0 * udecelerate0
|
||||
- acceleration0 * udecelerate0 * udecelerate0 / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate0 - udecelerate0);
|
||||
}
|
||||
} else if (acceleration0 * ttozero * ttozero / 2 <= distance) {
|
||||
// We have time to accelerate at least for some time >= 0,
|
||||
// and then decelerate to 0 to make it to final position.
|
||||
//
|
||||
// peak = entry.speed + acceleration0 * t
|
||||
// tdecelerate = peak / acceleration0
|
||||
// distance = entry.speed * t
|
||||
// + acceleration0 * t * t / 2
|
||||
// + acceleration0 * tdecelerate * tdecelerate / 2
|
||||
const auto det = entry.speed * entry.speed / 2
|
||||
+ distance * acceleration0;
|
||||
const auto t = std::max(
|
||||
(sqrt(det) - entry.speed) / acceleration0,
|
||||
0.);
|
||||
|
||||
const auto taccelerate = t;
|
||||
const auto uaccelerate = std::min(taccelerate, elapsed);
|
||||
const auto tdecelerate = t + (entry.speed / acceleration0);
|
||||
const auto udecelerate = std::min(
|
||||
tdecelerate,
|
||||
elapsed - uaccelerate);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto topspeed = entry.speed
|
||||
+ acceleration0 * taccelerate;
|
||||
entry.position += entry.speed * uaccelerate
|
||||
+ acceleration0 * uaccelerate * uaccelerate / 2.
|
||||
+ topspeed * udecelerate
|
||||
- acceleration0 * udecelerate * udecelerate / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate - udecelerate);
|
||||
}
|
||||
} else {
|
||||
// We just need to decelerate to 0,
|
||||
// faster than acceleration0.
|
||||
Assert(entry.speed > 0);
|
||||
const auto tdecelerate = 2 * distance / entry.speed;
|
||||
const auto udecelerate = std::min(tdecelerate, elapsed);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto a = entry.speed / tdecelerate;
|
||||
entry.position += entry.speed * udecelerate
|
||||
- a * udecelerate * udecelerate / 2;
|
||||
entry.speed -= a * udecelerate;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.position >= kEmojiInCarousel) {
|
||||
entry.position -= qFloor(entry.position / kEmojiInCarousel)
|
||||
* kEmojiInCarousel;
|
||||
}
|
||||
while (entry.position >= 1.) {
|
||||
Assert(!entry.sliding.empty());
|
||||
entry.position -= 1.;
|
||||
entry.sliding.erase(begin(entry.sliding));
|
||||
if (entry.emoji && entry.sliding.size() < 2) {
|
||||
entry = { .emoji = entry.emoji };
|
||||
break;
|
||||
} else if (entry.sliding.empty()) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!entry.emoji
|
||||
&& entry.position > 0.
|
||||
&& entry.sliding.size() < 2) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!animating) {
|
||||
state->animation.stop();
|
||||
} else if (updated) {
|
||||
state->repaints.fire({});
|
||||
}
|
||||
};
|
||||
state->animation.init(state->update);
|
||||
state->data.entries.resize(kEmojiInFingerprint);
|
||||
|
||||
const auto fillCarousel = [=](
|
||||
int index,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
auto indices = std::vector<int>();
|
||||
indices.reserve(kEmojiInCarousel);
|
||||
auto count = kEmojiCount;
|
||||
for (auto i = 0; i != kEmojiInCarousel; ++i, --count) {
|
||||
auto index = base::RandomIndex(count, buffered);
|
||||
for (const auto &already : indices) {
|
||||
if (index >= already) {
|
||||
++index;
|
||||
}
|
||||
}
|
||||
indices.push_back(index);
|
||||
}
|
||||
|
||||
entry.carousel.clear();
|
||||
entry.carousel.reserve(kEmojiInCarousel);
|
||||
for (const auto index : indices) {
|
||||
entry.carousel.push_back(EmojiByIndex(index));
|
||||
}
|
||||
};
|
||||
|
||||
const auto startTo = [=](
|
||||
int index,
|
||||
EmojiPtr emoji,
|
||||
crl::time now,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
if ((entry.emoji == emoji) && (emoji || entry.time)) {
|
||||
return;
|
||||
} else if (!entry.time) {
|
||||
Assert(entry.sliding.empty());
|
||||
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else if (emoji) {
|
||||
// Just initialize if we get emoji right from the start.
|
||||
entry.emoji = emoji;
|
||||
return;
|
||||
}
|
||||
entry.time = now + index * kStartTimeShift;
|
||||
|
||||
fillCarousel(index, buffered);
|
||||
}
|
||||
entry.emoji = emoji;
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
};
|
||||
|
||||
std::move(
|
||||
fingerprint
|
||||
) | rpl::on_next([=](const QByteArray &fingerprint) {
|
||||
auto buffered = base::BufferedRandom<uint32>(
|
||||
kEmojiInCarousel * kEmojiInFingerprint);
|
||||
const auto now = crl::now();
|
||||
const auto emoji = (fingerprint.size() >= 32)
|
||||
? ComputeEmojiFingerprint(
|
||||
bytes::make_span(fingerprint).subspan(0, 32))
|
||||
: std::vector<EmojiPtr>();
|
||||
state->update(now);
|
||||
|
||||
if (emoji.size() == kEmojiInFingerprint) {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, emoji[i], now, buffered);
|
||||
}
|
||||
} else {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, nullptr, now, buffered);
|
||||
}
|
||||
}
|
||||
if (!state->animation.animating()) {
|
||||
state->animation.start();
|
||||
}
|
||||
}, on);
|
||||
|
||||
return { .state = &state->data, .repaints = state->repaints.events() };
|
||||
}
|
||||
|
||||
void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
|
||||
struct State {
|
||||
std::unique_ptr<Ui::ImportantTooltip> tooltip;
|
||||
Fn<void()> updateGeometry;
|
||||
Fn<void(bool)> toggleTooltip;
|
||||
bool tooltipShown = false;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>();
|
||||
state->updateGeometry = [=] {
|
||||
if (!state->tooltip.get()) {
|
||||
return;
|
||||
}
|
||||
const auto geometry = Ui::MapFrom(
|
||||
widget->window(),
|
||||
widget,
|
||||
widget->rect());
|
||||
if (geometry.isEmpty()) {
|
||||
state->toggleTooltip(false);
|
||||
return;
|
||||
}
|
||||
const auto weak = QPointer<QWidget>(state->tooltip.get());
|
||||
const auto countPosition = [=](QSize size) {
|
||||
const auto result = geometry.bottomLeft()
|
||||
+ QPoint(
|
||||
geometry.width() / 2,
|
||||
st::confcallFingerprintTooltipSkip)
|
||||
- QPoint(size.width() / 2, 0);
|
||||
return result;
|
||||
};
|
||||
state->tooltip.get()->pointAt(
|
||||
geometry,
|
||||
RectPart::Bottom,
|
||||
countPosition);
|
||||
};
|
||||
state->toggleTooltip = [=](bool show) {
|
||||
if (const auto was = state->tooltip.release()) {
|
||||
was->toggleAnimated(false);
|
||||
}
|
||||
if (!show) {
|
||||
return;
|
||||
}
|
||||
const auto text = tr::lng_confcall_e2e_about(
|
||||
tr::now,
|
||||
tr::marked);
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
state->tooltip = std::make_unique<Ui::ImportantTooltip>(
|
||||
widget->window(),
|
||||
Ui::MakeNiceTooltipLabel(
|
||||
widget,
|
||||
rpl::single(text),
|
||||
st::confcallFingerprintTooltipMaxWidth,
|
||||
st::confcallFingerprintTooltipLabel),
|
||||
st::confcallFingerprintTooltip);
|
||||
const auto raw = state->tooltip.get();
|
||||
const auto weak = base::make_weak(raw);
|
||||
const auto destroy = [=] {
|
||||
delete weak.get();
|
||||
};
|
||||
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
raw->setHiddenCallback(destroy);
|
||||
state->updateGeometry();
|
||||
raw->toggleAnimated(true);
|
||||
};
|
||||
|
||||
widget->events() | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter) {
|
||||
// Enter events may come from widget destructors,
|
||||
// in that case sync-showing tooltip (calling Grab)
|
||||
// crashes the whole thing.
|
||||
state->tooltipShown = true;
|
||||
crl::on_main(widget, [=] {
|
||||
if (state->tooltipShown) {
|
||||
state->toggleTooltip(true);
|
||||
}
|
||||
});
|
||||
} else if (type == QEvent::Leave) {
|
||||
state->tooltipShown = false;
|
||||
crl::on_main(widget, [=] {
|
||||
if (!state->tooltipShown) {
|
||||
state->toggleTooltip(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, widget->lifetime());
|
||||
}
|
||||
|
||||
QImage MakeVerticalShadow(int height) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto result = QImage(
|
||||
QSize(1, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
auto p = QPainter(&result);
|
||||
auto g = QLinearGradient(0, 0, 0, height);
|
||||
auto color = st::groupCallMembersBg->c;
|
||||
auto trans = color;
|
||||
trans.setAlpha(0);
|
||||
g.setStops({
|
||||
{ 0.0, color },
|
||||
{ 0.4, trans },
|
||||
{ 0.6, trans },
|
||||
{ 1.0, color },
|
||||
});
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.fillRect(0, 0, 1, height, g);
|
||||
p.end();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints) {
|
||||
auto &lifetime = widget->lifetime();
|
||||
|
||||
const auto button = Ui::CreateChild<Ui::RpWidget>(widget);
|
||||
button->show();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
QString(),
|
||||
st::confcallFingerprintText);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->show();
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto esize = Ui::Emoji::GetSizeNormal();
|
||||
const auto size = esize / ratio;
|
||||
widget->widthValue() | rpl::on_next([=](int width) {
|
||||
static_assert(!(kEmojiInFingerprint % 2));
|
||||
|
||||
const auto available = width
|
||||
- st::confcallFingerprintMargins.left()
|
||||
- st::confcallFingerprintMargins.right()
|
||||
- (kEmojiInFingerprint * size)
|
||||
- (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip
|
||||
- st::confcallFingerprintTextMargins.left()
|
||||
- st::confcallFingerprintTextMargins.right();
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
label->setText(tr::lng_confcall_e2e_badge(tr::now));
|
||||
if (label->textMaxWidth() > available) {
|
||||
label->setText(tr::lng_confcall_e2e_badge_small(tr::now));
|
||||
}
|
||||
const auto use = std::min(available, label->textMaxWidth());
|
||||
label->resizeToWidth(use);
|
||||
|
||||
const auto ontheleft = kEmojiInFingerprint / 2;
|
||||
const auto ontheside = ontheleft * size
|
||||
+ (ontheleft - 1) * st::confcallFingerprintSkip;
|
||||
const auto text = QRect(
|
||||
(width - use) / 2,
|
||||
(st::confcallFingerprintMargins.top()
|
||||
+ st::confcallFingerprintTextMargins.top()),
|
||||
use,
|
||||
label->height());
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto withEmoji = QRect(
|
||||
textOuter.x() - ontheside,
|
||||
textOuter.y(),
|
||||
textOuter.width() + ontheside * 2,
|
||||
size);
|
||||
const auto outer = withEmoji.marginsAdded(
|
||||
st::confcallFingerprintMargins);
|
||||
|
||||
button->setGeometry(outer);
|
||||
label->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width);
|
||||
|
||||
widget->resize(
|
||||
width,
|
||||
button->height() + st::confcallFingerprintBottomSkip);
|
||||
}, lifetime);
|
||||
|
||||
const auto cache = lifetime.make_state<FingerprintBadgeCache>();
|
||||
button->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(button);
|
||||
|
||||
const auto outer = button->rect();
|
||||
const auto radius = outer.height() / 2.;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::groupCallMembersBg);
|
||||
p.drawRoundedRect(outer, radius, radius);
|
||||
p.setClipRect(outer);
|
||||
|
||||
const auto withEmoji = outer.marginsRemoved(
|
||||
st::confcallFingerprintMargins);
|
||||
p.translate(withEmoji.topLeft());
|
||||
|
||||
const auto text = label->geometry();
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto count = int(state->entries.size());
|
||||
cache->entries.resize(count);
|
||||
cache->shadow = MakeVerticalShadow(outer.height());
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto &entry = state->entries[i];
|
||||
auto &cached = cache->entries[i];
|
||||
const auto shadowed = entry.speed / state->speed;
|
||||
PaintFingerprintEntry(p, entry, cached, esize);
|
||||
if (shadowed > 0.) {
|
||||
p.setOpacity(shadowed);
|
||||
p.drawImage(
|
||||
QRect(0, -st::confcallFingerprintMargins.top(), size, outer.height()),
|
||||
cache->shadow);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (i + 1 == count / 2) {
|
||||
p.translate(size + textOuter.width(), 0);
|
||||
} else {
|
||||
p.translate(size + st::confcallFingerprintSkip, 0);
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
std::move(repaints) | rpl::on_next([=] {
|
||||
button->update();
|
||||
}, lifetime);
|
||||
|
||||
SetupFingerprintTooltip(button);
|
||||
}
|
||||
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize) {
|
||||
const auto stationary = !entry.time;
|
||||
if (stationary) {
|
||||
Ui::Emoji::Draw(p, entry.emoji, esize, 0, 0);
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = esize / ratio;
|
||||
const auto add = 4;
|
||||
const auto height = size + 2 * add;
|
||||
const auto validateCache = [&](int index, EmojiPtr e) {
|
||||
if (cache.emoji.size() <= index) {
|
||||
cache.emoji.reserve(entry.carousel.size() + 2);
|
||||
cache.emoji.resize(index + 1);
|
||||
}
|
||||
auto &emoji = cache.emoji[index];
|
||||
if (emoji.ptr != e) {
|
||||
emoji.ptr = e;
|
||||
emoji.image = QImage(
|
||||
QSize(size, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
emoji.image.setDevicePixelRatio(ratio);
|
||||
emoji.image.fill(Qt::transparent);
|
||||
auto q = QPainter(&emoji.image);
|
||||
Ui::Emoji::Draw(q, e, esize, 0, add);
|
||||
q.end();
|
||||
|
||||
//emoji.image = Images::Blur(
|
||||
// std::move(emoji.image),
|
||||
// false,
|
||||
// Qt::Vertical);
|
||||
}
|
||||
return &emoji;
|
||||
};
|
||||
auto shift = entry.position * height - add;
|
||||
p.translate(0, shift);
|
||||
for (const auto &e : entry.sliding) {
|
||||
const auto index = [&] {
|
||||
const auto i = ranges::find(entry.carousel, e);
|
||||
if (i != end(entry.carousel)) {
|
||||
return int(i - begin(entry.carousel));
|
||||
}
|
||||
return int(entry.carousel.size())
|
||||
+ ((e == entry.sliding.back()) ? 1 : 0);
|
||||
}();
|
||||
const auto entry = validateCache(index, e);
|
||||
p.drawImage(0, 0, entry->image);
|
||||
p.translate(0, -height);
|
||||
shift -= height;
|
||||
}
|
||||
p.translate(0, -shift);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
72
Telegram/SourceFiles/calls/calls_emoji_fingerprint.h
Normal file
72
Telegram/SourceFiles/calls/calls_emoji_fingerprint.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Call;
|
||||
|
||||
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
not_null<Call*> call);
|
||||
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
bytes::const_span fingerprint);
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Call*> call);
|
||||
|
||||
struct FingerprintBadgeState {
|
||||
struct Entry {
|
||||
EmojiPtr emoji = nullptr;
|
||||
std::vector<EmojiPtr> sliding;
|
||||
std::vector<EmojiPtr> carousel;
|
||||
crl::time time = 0;
|
||||
float64 speed = 0.;
|
||||
float64 position = 0.;
|
||||
int added = 0;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
float64 speed = 1.;
|
||||
};
|
||||
struct FingerprintBadge {
|
||||
not_null<const FingerprintBadgeState*> state;
|
||||
rpl::producer<> repaints;
|
||||
};
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint);
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints);
|
||||
|
||||
struct FingerprintBadgeCache {
|
||||
struct Emoji {
|
||||
EmojiPtr ptr = nullptr;
|
||||
QImage image;
|
||||
};
|
||||
struct Entry {
|
||||
std::vector<Emoji> emoji;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
QImage shadow;
|
||||
};
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize);
|
||||
|
||||
} // namespace Calls
|
||||
1202
Telegram/SourceFiles/calls/calls_instance.cpp
Normal file
1202
Telegram/SourceFiles/calls/calls_instance.cpp
Normal file
File diff suppressed because it is too large
Load Diff
228
Telegram/SourceFiles/calls/calls_instance.h
Normal file
228
Telegram/SourceFiles/calls/calls_instance.h
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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 "mtproto/sender.h"
|
||||
|
||||
namespace crl {
|
||||
class semaphore;
|
||||
} // namespace crl
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Platform {
|
||||
enum class PermissionType;
|
||||
} // namespace Platform
|
||||
|
||||
namespace Media::Audio {
|
||||
class Track;
|
||||
} // namespace Media::Audio
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
struct JoinInfo;
|
||||
struct ConferenceInfo;
|
||||
class Panel;
|
||||
class ChooseJoinAsProcess;
|
||||
class StartRtmpProcess;
|
||||
} // namespace Calls::Group
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Call;
|
||||
enum class CallType;
|
||||
class GroupCall;
|
||||
class Panel;
|
||||
struct DhConfig;
|
||||
struct InviteRequest;
|
||||
struct StartConferenceInfo;
|
||||
|
||||
struct StartGroupCallArgs {
|
||||
enum class JoinConfirm {
|
||||
None,
|
||||
IfNowInAnother,
|
||||
Always,
|
||||
};
|
||||
QString joinHash;
|
||||
JoinConfirm confirm = JoinConfirm::IfNowInAnother;
|
||||
bool scheduleNeeded = false;
|
||||
};
|
||||
|
||||
struct ConferenceInviteMessages {
|
||||
base::flat_set<MsgId> incoming;
|
||||
base::flat_set<MsgId> outgoing;
|
||||
};
|
||||
|
||||
struct ConferenceInvites {
|
||||
base::flat_map<not_null<UserData*>, ConferenceInviteMessages> users;
|
||||
};
|
||||
|
||||
class Instance final : public base::has_weak_ptr {
|
||||
public:
|
||||
Instance();
|
||||
~Instance();
|
||||
|
||||
void startOutgoingCall(not_null<UserData*> user, bool video);
|
||||
void startOrJoinGroupCall(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
StartGroupCallArgs args);
|
||||
void startOrJoinConferenceCall(StartConferenceInfo args);
|
||||
void startedConferenceReady(
|
||||
not_null<GroupCall*> call,
|
||||
StartConferenceInfo args);
|
||||
void showStartWithRtmp(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer);
|
||||
void handleUpdate(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update);
|
||||
|
||||
// Called by Data::GroupCall when it is appropriate by the 'version'.
|
||||
void applyGroupCallUpdateChecked(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update);
|
||||
|
||||
void showInfoPanel(not_null<Call*> call);
|
||||
void showInfoPanel(not_null<GroupCall*> call);
|
||||
[[nodiscard]] Call *currentCall() const;
|
||||
[[nodiscard]] rpl::producer<Call*> currentCallValue() const;
|
||||
[[nodiscard]] GroupCall *currentGroupCall() const;
|
||||
[[nodiscard]] rpl::producer<GroupCall*> currentGroupCallValue() const;
|
||||
[[nodiscard]] bool inCall() const;
|
||||
[[nodiscard]] bool inGroupCall() const;
|
||||
[[nodiscard]] bool hasVisiblePanel(
|
||||
Main::Session *session = nullptr) const;
|
||||
[[nodiscard]] bool hasActivePanel(
|
||||
Main::Session *session = nullptr) const;
|
||||
bool activateCurrentCall(const QString &joinHash = QString());
|
||||
bool minimizeCurrentActiveCall();
|
||||
bool toggleFullScreenCurrentActiveCall();
|
||||
bool closeCurrentActiveCall();
|
||||
[[nodiscard]] auto getVideoCapture(
|
||||
std::optional<QString> deviceId = std::nullopt,
|
||||
bool isScreenCapture = false)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
|
||||
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
|
||||
|
||||
[[nodiscard]] const ConferenceInvites &conferenceInvites(
|
||||
CallId conferenceId) const;
|
||||
void registerConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming);
|
||||
void unregisterConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming,
|
||||
bool onlyStopCalling = false);
|
||||
void showConferenceInvite(
|
||||
not_null<UserData*> user,
|
||||
MsgId conferenceInviteMsgId);
|
||||
void declineIncomingConferenceInvites(CallId conferenceId);
|
||||
void declineOutgoingConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
bool discard = false);
|
||||
|
||||
[[nodiscard]] FnMut<void()> addAsyncWaiter();
|
||||
|
||||
void registerVideoStream(not_null<GroupCall*> call);
|
||||
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] bool isQuitPrevent();
|
||||
|
||||
private:
|
||||
class Delegate;
|
||||
friend class Delegate;
|
||||
|
||||
not_null<Media::Audio::Track*> ensureSoundLoaded(const QString &key);
|
||||
void playSoundOnce(const QString &key);
|
||||
|
||||
void createCall(not_null<UserData*> user, CallType type, bool isVideo);
|
||||
void destroyCall(not_null<Call*> call);
|
||||
void finishConferenceInvitations(const StartConferenceInfo &args);
|
||||
|
||||
void createGroupCall(
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
void destroyGroupCall(not_null<GroupCall*> call);
|
||||
void confirmLeaveCurrent(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
StartGroupCallArgs args,
|
||||
Fn<void(StartGroupCallArgs)> confirmed);
|
||||
|
||||
void requestPermissionOrFail(
|
||||
Platform::PermissionType type,
|
||||
Fn<void()> onSuccess);
|
||||
|
||||
void refreshDhConfig();
|
||||
void refreshServerConfig(not_null<Main::Session*> session);
|
||||
bytes::const_span updateDhConfig(const MTPmessages_DhConfig &data);
|
||||
|
||||
void destroyCurrentCall(
|
||||
Data::GroupCall *migrateCall = nullptr,
|
||||
const QString &migrateSlug = QString());
|
||||
void handleCallUpdate(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPPhoneCall &call);
|
||||
void handleSignalingData(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPDupdatePhoneCallSignalingData &data);
|
||||
void handleGroupCallUpdate(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update);
|
||||
|
||||
const std::unique_ptr<Delegate> _delegate;
|
||||
const std::unique_ptr<DhConfig> _cachedDhConfig;
|
||||
|
||||
crl::time _lastServerConfigUpdateTime = 0;
|
||||
base::weak_ptr<Main::Session> _serverConfigRequestSession;
|
||||
std::weak_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
|
||||
|
||||
std::unique_ptr<Call> _currentCall;
|
||||
rpl::event_stream<Call*> _currentCallChanges;
|
||||
std::unique_ptr<Panel> _currentCallPanel;
|
||||
|
||||
std::unique_ptr<GroupCall> _currentGroupCall;
|
||||
std::unique_ptr<GroupCall> _startingGroupCall;
|
||||
rpl::event_stream<GroupCall*> _currentGroupCallChanges;
|
||||
std::unique_ptr<Group::Panel> _currentGroupCallPanel;
|
||||
|
||||
base::flat_map<QString, std::unique_ptr<Media::Audio::Track>> _tracks;
|
||||
|
||||
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
|
||||
const std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;
|
||||
|
||||
base::flat_map<CallId, ConferenceInvites> _conferenceInvites;
|
||||
|
||||
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
|
||||
|
||||
base::flat_map<
|
||||
not_null<Main::Session*>,
|
||||
std::vector<base::weak_ptr<GroupCall>>> _streams;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
1444
Telegram/SourceFiles/calls/calls_panel.cpp
Normal file
1444
Telegram/SourceFiles/calls/calls_panel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
225
Telegram/SourceFiles/calls/calls_panel.h
Normal file
225
Telegram/SourceFiles/calls/calls_panel.h
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
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/calls_call.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class Image;
|
||||
|
||||
namespace base {
|
||||
class PowerSaveBlocker;
|
||||
} // namespace base
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class LayerWidget;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
class IconButton;
|
||||
class CallButton;
|
||||
class LayerManager;
|
||||
class FlatLabel;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
class RpWindow;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
class Instance;
|
||||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Ui::Platform {
|
||||
struct SeparateTitleControls;
|
||||
} // namespace Ui::Platform
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
struct CallBodyLayout;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Window;
|
||||
class Userpic;
|
||||
class SignalBars;
|
||||
class VideoBubble;
|
||||
class PanelBackground;
|
||||
struct DeviceSelection;
|
||||
struct ConferencePanelMigration;
|
||||
|
||||
class Panel final
|
||||
: public base::has_weak_ptr
|
||||
, private Group::Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
Panel(not_null<Call*> call);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
[[nodiscard]] not_null<UserData*> user() const;
|
||||
[[nodiscard]] bool isVisible() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
[[nodiscard]] ConferencePanelMigration migrationInfo() const;
|
||||
|
||||
void showAndActivate();
|
||||
void minimize();
|
||||
void toggleFullScreen();
|
||||
void replaceCall(not_null<Call*> call);
|
||||
void closeBeforeDestroy(bool windowIsReused = false);
|
||||
|
||||
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;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> startOutgoingRequests() const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
|
||||
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
class Incoming;
|
||||
using State = Call::State;
|
||||
using Type = Call::Type;
|
||||
enum class AnswerHangupRedialState : uchar {
|
||||
Answer,
|
||||
Hangup,
|
||||
Redial,
|
||||
StartCall,
|
||||
};
|
||||
|
||||
void paint(QRect clip);
|
||||
|
||||
void initWindow();
|
||||
void initWidget();
|
||||
void initControls();
|
||||
void initConferenceInvite();
|
||||
void reinitWithCall(Call *call);
|
||||
void initLayout();
|
||||
void initMediaDeviceToggles();
|
||||
void initGeometry();
|
||||
|
||||
[[nodiscard]] bool handleClose() const;
|
||||
|
||||
void requestControlsHidden(bool hidden);
|
||||
void controlsShownForce(bool shown);
|
||||
void updateControlsShown();
|
||||
void updateControlsGeometry();
|
||||
void updateHangupGeometry();
|
||||
void updateStatusGeometry();
|
||||
void updateOutgoingVideoBubbleGeometry();
|
||||
void stateChanged(State state);
|
||||
void showControls();
|
||||
void updateStatusText(State state);
|
||||
void updateTextColors();
|
||||
void startDurationUpdateTimer(crl::time currentDuration);
|
||||
void setIncomingSize(QSize size);
|
||||
void refreshIncomingGeometry();
|
||||
|
||||
void refreshOutgoingPreviewInBody(State state);
|
||||
void toggleFullScreen(bool fullscreen);
|
||||
void createRemoteAudioMute();
|
||||
void createRemoteLowBattery();
|
||||
void showRemoteLowBattery();
|
||||
void refreshAnswerHangupRedialLabel();
|
||||
|
||||
void showDevicesMenu(
|
||||
not_null<QWidget*> button,
|
||||
std::vector<DeviceSelection> types);
|
||||
|
||||
[[nodiscard]] QRect incomingFrameGeometry() const;
|
||||
[[nodiscard]] QRect outgoingFrameGeometry() const;
|
||||
|
||||
Call *_call = nullptr;
|
||||
not_null<UserData*> _user;
|
||||
|
||||
std::shared_ptr<Window> _window;
|
||||
std::unique_ptr<Incoming> _incoming;
|
||||
|
||||
QSize _incomingFrameSize;
|
||||
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
not_null<const style::CallBodyLayout*> _bodySt;
|
||||
base::unique_qptr<Ui::CallButton> _answerHangupRedial;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _decline;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _cancel;
|
||||
bool _hangupShown = false;
|
||||
bool _conferenceSupported = false;
|
||||
bool _outgoingPreviewInBody = false;
|
||||
std::optional<AnswerHangupRedialState> _answerHangupRedialState;
|
||||
Ui::Animations::Simple _hangupShownProgress;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _screencast;
|
||||
base::unique_qptr<Ui::CallButton> _camera;
|
||||
Ui::CallButton *_cameraDeviceToggle = nullptr;
|
||||
base::unique_qptr<Ui::CallButton> _startVideo;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _mute;
|
||||
Ui::CallButton *_audioDeviceToggle = nullptr;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _addPeople;
|
||||
base::unique_qptr<Ui::FlatLabel> _name;
|
||||
base::unique_qptr<Ui::FlatLabel> _status;
|
||||
base::unique_qptr<Ui::RpWidget> _conferenceParticipants;
|
||||
base::unique_qptr<Ui::RpWidget> _fingerprint;
|
||||
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute;
|
||||
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery;
|
||||
std::unique_ptr<Userpic> _userpic;
|
||||
std::unique_ptr<VideoBubble> _outgoingVideoBubble;
|
||||
QPixmap _bottomShadow;
|
||||
int _bodyTop = 0;
|
||||
int _buttonsTopShown = 0;
|
||||
int _buttonsTop = 0;
|
||||
|
||||
base::Timer _hideControlsTimer;
|
||||
base::Timer _controlsShownForceTimer;
|
||||
std::unique_ptr<QObject> _hideControlsFilter;
|
||||
bool _hideControlsRequested = false;
|
||||
rpl::variable<bool> _fullScreenOrMaximized;
|
||||
Ui::Animations::Simple _controlsShownAnimation;
|
||||
bool _controlsShownForce = false;
|
||||
bool _controlsShown = true;
|
||||
bool _mouseInside = false;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _devicesMenu;
|
||||
|
||||
base::Timer _updateDurationTimer;
|
||||
base::Timer _updateOuterRippleTimer;
|
||||
|
||||
rpl::event_stream<bool> _startOutgoingRequests;
|
||||
|
||||
std::unique_ptr<PanelBackground> _background;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
225
Telegram/SourceFiles/calls/calls_panel_background.cpp
Normal file
225
Telegram/SourceFiles/calls/calls_panel_background.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
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/calls_panel_background.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_peer_colors.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_emoji_statuses.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/color_contrast.h"
|
||||
#include "ui/top_background_gradient.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
PanelBackground::PanelBackground(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> updateCallback)
|
||||
: _peer(peer)
|
||||
, _updateCallback(std::move(updateCallback)) {
|
||||
updateColors();
|
||||
updateEmojiId();
|
||||
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::ColorProfile
|
||||
| Data::PeerUpdate::Flag::EmojiStatus
|
||||
) | rpl::on_next([=] {
|
||||
updateColors();
|
||||
_brushSize = QSize();
|
||||
if (_updateCallback) {
|
||||
_updateCallback();
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::BackgroundEmoji
|
||||
| Data::PeerUpdate::Flag::EmojiStatus
|
||||
) | rpl::on_next([=] {
|
||||
updateEmojiId();
|
||||
if (_updateCallback) {
|
||||
_updateCallback();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void PanelBackground::paint(
|
||||
QPainter &p,
|
||||
QSize widgetSize,
|
||||
int bodyTop,
|
||||
int photoTop,
|
||||
int photoSize,
|
||||
const QRegion ®ion) {
|
||||
if (!_colors || _colors->bg.empty()) {
|
||||
for (const auto &rect : region) {
|
||||
p.fillRect(rect, st::callBgOpaque);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &colors = _colors->bg;
|
||||
if (colors.size() == 1) {
|
||||
for (const auto &rect : region) {
|
||||
p.fillRect(rect, colors.front());
|
||||
}
|
||||
} else {
|
||||
const auto center = QPoint(
|
||||
widgetSize.width() / 2,
|
||||
bodyTop + photoTop + photoSize / 2);
|
||||
const auto radius = std::max(
|
||||
std::hypot(center.x(), center.y()),
|
||||
std::hypot(widgetSize.width() - center.x(),
|
||||
widgetSize.height() - center.y()));
|
||||
if (_brushSize != widgetSize) {
|
||||
updateBrush(widgetSize, center, radius, colors);
|
||||
}
|
||||
for (const auto &rect : region) {
|
||||
p.fillRect(rect, _brush);
|
||||
}
|
||||
}
|
||||
|
||||
const auto emojiId = _currentEmojiId;
|
||||
if (!emojiId) {
|
||||
_emoji = nullptr;
|
||||
_cachedImage = QImage();
|
||||
_cachedEmojiId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto userpicX = (widgetSize.width() - photoSize) / 2;
|
||||
const auto userpicY = bodyTop + photoTop;
|
||||
const auto padding = photoSize;
|
||||
const auto patternRect = QRect(
|
||||
userpicX - padding,
|
||||
userpicY - padding / 2,
|
||||
photoSize + padding * 2,
|
||||
photoSize + padding);
|
||||
|
||||
if (!_emoji
|
||||
|| _emoji->entityData()
|
||||
!= Data::SerializeCustomEmojiId(emojiId)) {
|
||||
const auto document = _peer->owner().document(emojiId);
|
||||
_emoji = document->owner().customEmojiManager().create(
|
||||
document,
|
||||
[=] {
|
||||
_cachedImage = QImage();
|
||||
if (_updateCallback) {
|
||||
_updateCallback();
|
||||
}
|
||||
},
|
||||
Data::CustomEmojiSizeTag::Large);
|
||||
_cachedImage = QImage();
|
||||
_cachedEmojiId = 0;
|
||||
}
|
||||
|
||||
if (_emoji && _emoji->ready()) {
|
||||
if (_cachedImage.isNull()
|
||||
|| _cachedRect != patternRect
|
||||
|| _cachedEmojiId != emojiId) {
|
||||
renderPattern(patternRect, emojiId);
|
||||
}
|
||||
p.drawImage(patternRect, _cachedImage);
|
||||
}
|
||||
}
|
||||
|
||||
void PanelBackground::updateBrush(
|
||||
QSize widgetSize,
|
||||
QPoint center,
|
||||
float64 radius,
|
||||
const std::vector<QColor> &colors) {
|
||||
_brushSize = widgetSize;
|
||||
auto gradient = QRadialGradient(center, radius);
|
||||
const auto step = 1.0 / (colors.size() - 1);
|
||||
for (auto i = 0; i < colors.size(); ++i) {
|
||||
gradient.setColorAt(i * step, colors[colors.size() - 1 - i]);
|
||||
}
|
||||
_brush = QBrush(gradient);
|
||||
}
|
||||
|
||||
void PanelBackground::renderPattern(const QRect &rect, DocumentId emojiId) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
_cachedImage = QImage(
|
||||
rect.size() * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_cachedImage.setDevicePixelRatio(ratio);
|
||||
_cachedImage.fill(Qt::transparent);
|
||||
|
||||
auto painter = QPainter(&_cachedImage);
|
||||
const auto patternColor = QColor(0, 0, 0, int(0.6 * 255));
|
||||
const auto &points = Ui::PatternBgPoints();
|
||||
const auto localRect = QRect(QPoint(), rect.size());
|
||||
Ui::PaintBgPoints(
|
||||
painter,
|
||||
points,
|
||||
_cache,
|
||||
_emoji.get(),
|
||||
patternColor,
|
||||
localRect,
|
||||
1.);
|
||||
|
||||
_cachedRect = rect;
|
||||
_cachedEmojiId = emojiId;
|
||||
}
|
||||
|
||||
void PanelBackground::updateColors() {
|
||||
const auto collectible = _peer->emojiStatusId().collectible;
|
||||
if (collectible && collectible->centerColor.isValid()) {
|
||||
_colors = Data::ColorProfileSet{
|
||||
.bg = { collectible->edgeColor, collectible->centerColor },
|
||||
};
|
||||
} else {
|
||||
_colors = _peer->session().api().peerColors().colorProfileFor(_peer);
|
||||
}
|
||||
}
|
||||
|
||||
void PanelBackground::updateEmojiId() {
|
||||
const auto collectible = _peer->emojiStatusId().collectible;
|
||||
_currentEmojiId = (collectible && collectible->patternDocumentId)
|
||||
? collectible->patternDocumentId
|
||||
: _peer->profileBackgroundEmojiId();
|
||||
}
|
||||
|
||||
std::optional<QColor> PanelBackground::edgeColor() const {
|
||||
const auto collectible = _peer->emojiStatusId().collectible;
|
||||
if (collectible && collectible->edgeColor.isValid()) {
|
||||
return collectible->edgeColor;
|
||||
}
|
||||
if (_colors && !_colors->bg.empty()) {
|
||||
return _colors->bg.front();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<QColor> PanelBackground::textColorOverride(
|
||||
const style::color &defaultColor) const {
|
||||
const auto collectible = _peer->emojiStatusId().collectible;
|
||||
if (collectible && collectible->textColor.isValid()) {
|
||||
return collectible->textColor;
|
||||
}
|
||||
const auto edge = edgeColor();
|
||||
if (!edge) {
|
||||
return std::nullopt;
|
||||
}
|
||||
constexpr auto kMinContrast = 5.5;
|
||||
if (kMinContrast > Ui::CountContrast(defaultColor->c, *edge)) {
|
||||
return st::groupCallMembersFg->c;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
rpl::lifetime &PanelBackground::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
75
Telegram/SourceFiles/calls/calls_panel_background.h
Normal file
75
Telegram/SourceFiles/calls/calls_panel_background.h
Normal file
@@ -0,0 +1,75 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "data/data_peer_colors.h"
|
||||
|
||||
class DocumentData;
|
||||
class PeerData;
|
||||
|
||||
namespace Data {
|
||||
class CustomEmoji;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
namespace Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Text
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class PanelBackground final {
|
||||
public:
|
||||
explicit PanelBackground(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> updateCallback);
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
QSize widgetSize,
|
||||
int bodyTop,
|
||||
int photoTop,
|
||||
int photoSize,
|
||||
const QRegion ®ion);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
[[nodiscard]] std::optional<QColor> textColorOverride(
|
||||
const style::color &defaultColor) const;
|
||||
|
||||
private:
|
||||
void updateBrush(
|
||||
QSize widgetSize,
|
||||
QPoint center,
|
||||
float64 radius,
|
||||
const std::vector<QColor> &colors);
|
||||
void renderPattern(const QRect &rect, DocumentId emojiId);
|
||||
void updateColors();
|
||||
void updateEmojiId();
|
||||
[[nodiscard]] std::optional<QColor> edgeColor() const;
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
const Fn<void()> _updateCallback;
|
||||
|
||||
QBrush _brush;
|
||||
QSize _brushSize;
|
||||
|
||||
std::optional<Data::ColorProfileSet> _colors;
|
||||
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> _emoji;
|
||||
base::flat_map<float64, QImage> _cache;
|
||||
QImage _cachedImage;
|
||||
QRect _cachedRect;
|
||||
DocumentId _cachedEmojiId = 0;
|
||||
DocumentId _currentEmojiId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
65
Telegram/SourceFiles/calls/calls_signal_bars.cpp
Normal file
65
Telegram/SourceFiles/calls/calls_signal_bars.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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/calls_signal_bars.h"
|
||||
|
||||
#include "calls/calls_call.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
SignalBars::SignalBars(
|
||||
QWidget *parent,
|
||||
not_null<Call*> call,
|
||||
const style::CallSignalBars &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _count(Call::kSignalBarStarting) {
|
||||
resize(
|
||||
_st.width + (_st.width + _st.skip) * (Call::kSignalBarCount - 1),
|
||||
_st.max);
|
||||
call->signalBarCountValue(
|
||||
) | rpl::on_next([=](int count) {
|
||||
changed(count);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SignalBars::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_st.color);
|
||||
for (auto i = 0; i < Call::kSignalBarCount; ++i) {
|
||||
p.setOpacity((i < _count) ? 1. : _st.inactiveOpacity);
|
||||
const auto barHeight = _st.min
|
||||
+ (_st.max - _st.min) * (i / float64(Call::kSignalBarCount - 1));
|
||||
const auto barLeft = i * (_st.width + _st.skip);
|
||||
const auto barTop = height() - barHeight;
|
||||
p.drawRoundedRect(
|
||||
QRectF(
|
||||
barLeft,
|
||||
barTop,
|
||||
_st.width,
|
||||
barHeight),
|
||||
_st.radius,
|
||||
_st.radius);
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
void SignalBars::changed(int count) {
|
||||
if (_count == Call::kSignalBarFinished) {
|
||||
return;
|
||||
} else if (_count != count) {
|
||||
_count = count;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
37
Telegram/SourceFiles/calls/calls_signal_bars.h
Normal file
37
Telegram/SourceFiles/calls/calls_signal_bars.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Call;
|
||||
|
||||
class SignalBars final : public Ui::RpWidget {
|
||||
public:
|
||||
SignalBars(
|
||||
QWidget *parent,
|
||||
not_null<Call*> call,
|
||||
const style::CallSignalBars &st);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void changed(int count);
|
||||
|
||||
const style::CallSignalBars &_st;
|
||||
int _count = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
837
Telegram/SourceFiles/calls/calls_top_bar.cpp
Normal file
837
Telegram/SourceFiles/calls/calls_top_bar.cpp
Normal file
@@ -0,0 +1,837 @@
|
||||
/*
|
||||
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/calls_top_bar.h"
|
||||
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/paint/blobs_linear.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/chat/group_call_userpics.h" // Ui::GroupCallUser.
|
||||
#include "ui/chat/group_call_bar.h" // Ui::GroupCallBarContent.
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/application.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h" // Group::LeaveBox.
|
||||
#include "history/view/history_view_group_call_bar.h" // ContentByCall.
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "base/timer.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h" // style::GroupCallUserpics
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
enum class BarState {
|
||||
Connecting,
|
||||
Active,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kUpdateDebugTimeoutMs = crl::time(500);
|
||||
|
||||
constexpr auto kMinorBlobAlpha = 76. / 255.;
|
||||
|
||||
constexpr auto kHideBlobsDuration = crl::time(500);
|
||||
constexpr auto kBlobLevelDuration = crl::time(250);
|
||||
constexpr auto kBlobUpdateInterval = crl::time(100);
|
||||
|
||||
auto BarStateFromMuteState(
|
||||
MuteState state,
|
||||
GroupCall::InstanceState instanceState,
|
||||
TimeId scheduledDate) {
|
||||
return scheduledDate
|
||||
? BarState::ForceMuted
|
||||
: (instanceState == GroupCall::InstanceState::Disconnected)
|
||||
? BarState::Connecting
|
||||
: (state == MuteState::ForceMuted || state == MuteState::RaisedHand)
|
||||
? BarState::ForceMuted
|
||||
: (state == MuteState::Muted)
|
||||
? BarState::Muted
|
||||
: BarState::Active;
|
||||
};
|
||||
|
||||
auto LinearBlobs() {
|
||||
return std::vector<Ui::Paint::LinearBlobs::BlobData>{
|
||||
{
|
||||
.segmentsCount = 5,
|
||||
.minRadius = 0.,
|
||||
.maxRadius = (float)st::groupCallMajorBlobMaxRadius,
|
||||
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
|
||||
.speedScale = .3,
|
||||
.alpha = 1.,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 7,
|
||||
.minRadius = 0.,
|
||||
.maxRadius = (float)st::groupCallMinorBlobMaxRadius,
|
||||
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
|
||||
.speedScale = .7,
|
||||
.alpha = kMinorBlobAlpha,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minRadius = 0.,
|
||||
.maxRadius = (float)st::groupCallMinorBlobMaxRadius,
|
||||
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
|
||||
.speedScale = .7,
|
||||
.alpha = kMinorBlobAlpha,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
auto Colors() {
|
||||
using Vector = std::vector<QColor>;
|
||||
using Colors = anim::gradient_colors;
|
||||
return base::flat_map<BarState, Colors>{
|
||||
{
|
||||
BarState::ForceMuted,
|
||||
Colors(QGradientStops{
|
||||
{ 0.0, st::groupCallForceMutedBar1->c },
|
||||
{ .35, st::groupCallForceMutedBar2->c },
|
||||
{ 1.0, st::groupCallForceMutedBar3->c } })
|
||||
},
|
||||
{
|
||||
BarState::Active,
|
||||
Colors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })
|
||||
},
|
||||
{
|
||||
BarState::Muted,
|
||||
Colors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })
|
||||
},
|
||||
{
|
||||
BarState::Connecting,
|
||||
Colors(st::callBarBgMuted->c)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class DebugInfoBox : public Ui::BoxContent {
|
||||
public:
|
||||
DebugInfoBox(QWidget*, base::weak_ptr<Call> call);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
void updateText();
|
||||
|
||||
base::weak_ptr<Call> _call;
|
||||
QPointer<Ui::FlatLabel> _text;
|
||||
base::Timer _updateTextTimer;
|
||||
|
||||
};
|
||||
|
||||
DebugInfoBox::DebugInfoBox(QWidget*, base::weak_ptr<Call> call)
|
||||
: _call(call) {
|
||||
}
|
||||
|
||||
void DebugInfoBox::prepare() {
|
||||
setTitle(rpl::single(u"Call Debug"_q));
|
||||
|
||||
addButton(tr::lng_close(), [this] { closeBox(); });
|
||||
_text = setInnerWidget(
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
this,
|
||||
object_ptr<Ui::FlatLabel>(this, st::callDebugLabel),
|
||||
st::callDebugPadding))->entity();
|
||||
_text->setSelectable(true);
|
||||
updateText();
|
||||
_updateTextTimer.setCallback([this] { updateText(); });
|
||||
_updateTextTimer.callEach(kUpdateDebugTimeoutMs);
|
||||
setDimensions(st::boxWideWidth, st::boxMaxListHeight);
|
||||
}
|
||||
|
||||
void DebugInfoBox::updateText() {
|
||||
if (auto call = _call.get()) {
|
||||
_text->setText(call->getDebugLog());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct TopBar::User {
|
||||
Ui::GroupCallUser data;
|
||||
};
|
||||
|
||||
class Mute final : public Ui::IconButton {
|
||||
public:
|
||||
Mute(QWidget *parent, const style::IconButton &st)
|
||||
: Ui::IconButton(parent, st)
|
||||
, _st(st)
|
||||
, _crossLineMuteAnimation(st::callTopBarMuteCrossLine) {
|
||||
resize(_st.width, _st.height);
|
||||
installEventFilter(this);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_crossLineMuteAnimation.invalidate();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void setProgress(float64 progress) {
|
||||
if (_progress == progress) {
|
||||
return;
|
||||
}
|
||||
_progress = progress;
|
||||
update();
|
||||
}
|
||||
|
||||
void setRippleColorOverride(const style::color *colorOverride) {
|
||||
_rippleColorOverride = colorOverride;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) {
|
||||
if (event->type() == QEvent::Paint) {
|
||||
auto p = QPainter(this);
|
||||
paintRipple(
|
||||
p,
|
||||
_st.rippleAreaPosition.x(),
|
||||
_st.rippleAreaPosition.y(),
|
||||
_rippleColorOverride ? &(*_rippleColorOverride)->c : nullptr);
|
||||
_crossLineMuteAnimation.paint(p, _st.iconPosition, _progress);
|
||||
return true;
|
||||
}
|
||||
return QObject::eventFilter(object, event);
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _progress = 0.;
|
||||
|
||||
const style::IconButton &_st;
|
||||
Ui::CrossLineAnimation _crossLineMuteAnimation;
|
||||
const style::color *_rippleColorOverride = nullptr;
|
||||
|
||||
};
|
||||
|
||||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
Call *call,
|
||||
std::shared_ptr<Ui::Show> show)
|
||||
: TopBar(parent, show, call, nullptr) {
|
||||
}
|
||||
|
||||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
GroupCall *call,
|
||||
std::shared_ptr<Ui::Show> show)
|
||||
: TopBar(parent, show, nullptr, call) {
|
||||
}
|
||||
|
||||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Call *call,
|
||||
GroupCall *groupCall)
|
||||
: RpWidget(parent)
|
||||
, _call(call)
|
||||
, _groupCall(groupCall)
|
||||
, _show(show)
|
||||
, _userpics(call
|
||||
? nullptr
|
||||
: std::make_unique<Ui::GroupCallUserpics>(
|
||||
st::groupCallTopBarUserpics,
|
||||
rpl::single(true),
|
||||
[=] { updateUserpics(); }))
|
||||
, _durationLabel(_call
|
||||
? object_ptr<Ui::LabelSimple>(this, st::callBarLabel)
|
||||
: object_ptr<Ui::LabelSimple>(nullptr))
|
||||
, _signalBars(_call
|
||||
? object_ptr<SignalBars>(this, _call.get(), st::callBarSignalBars)
|
||||
: object_ptr<SignalBars>(nullptr))
|
||||
, _fullInfoLabel(this, st::callBarInfoLabel)
|
||||
, _shortInfoLabel(this, st::callBarInfoLabel)
|
||||
, _hangupLabel(_call
|
||||
? object_ptr<Ui::LabelSimple>(
|
||||
this,
|
||||
st::callBarLabel,
|
||||
tr::lng_call_bar_hangup(tr::now))
|
||||
: object_ptr<Ui::LabelSimple>(nullptr))
|
||||
, _mute(this, st::callBarMuteToggle)
|
||||
, _info(this)
|
||||
, _hangup(this, st::callBarHangup)
|
||||
, _gradients(Colors(), QPointF(), QPointF())
|
||||
, _updateDurationTimer([=] { updateDurationText(); }) {
|
||||
initControls();
|
||||
resize(width(), st::callBarHeight);
|
||||
setupInitialBrush();
|
||||
}
|
||||
|
||||
void TopBar::setupInitialBrush() {
|
||||
Expects(_switchStateCallback != nullptr);
|
||||
|
||||
_switchStateAnimation.stop();
|
||||
_switchStateCallback(1.);
|
||||
}
|
||||
|
||||
void TopBar::initControls() {
|
||||
_mute->setClickedCallback([=] {
|
||||
if (const auto call = _call.get()) {
|
||||
call->setMuted(!call->muted());
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
if (group->mutedByAdmin()) {
|
||||
_show->showToast(
|
||||
tr::lng_group_call_force_muted_sub(tr::now));
|
||||
} else {
|
||||
group->setMuted((group->muted() == MuteState::Muted)
|
||||
? MuteState::Active
|
||||
: MuteState::Muted);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const auto mapToState = [](bool muted) {
|
||||
return muted ? MuteState::Muted : MuteState::Active;
|
||||
};
|
||||
const auto fromState = _mute->lifetime().make_state<BarState>(
|
||||
BarStateFromMuteState(
|
||||
_call
|
||||
? mapToState(_call->muted())
|
||||
: _groupCall->muted(),
|
||||
GroupCall::InstanceState::Connected,
|
||||
_call ? TimeId(0) : _groupCall->scheduleDate()));
|
||||
using namespace rpl::mappers;
|
||||
auto muted = _call
|
||||
? rpl::combine(
|
||||
_call->mutedValue() | rpl::map(mapToState),
|
||||
rpl::single(GroupCall::InstanceState::Connected),
|
||||
rpl::single(TimeId(0))
|
||||
) | rpl::type_erased
|
||||
: rpl::combine(
|
||||
(_groupCall->mutedValue()
|
||||
| MapPushToTalkToActive()
|
||||
| rpl::distinct_until_changed()
|
||||
| rpl::type_erased),
|
||||
rpl::single(
|
||||
_groupCall->instanceState()
|
||||
) | rpl::then(_groupCall->instanceStateValue() | rpl::filter(
|
||||
_1 != GroupCall::InstanceState::TransitionToRtc)),
|
||||
rpl::single(
|
||||
_groupCall->scheduleDate()
|
||||
) | rpl::then(_groupCall->real(
|
||||
) | rpl::map([](not_null<Data::GroupCall*> call) {
|
||||
return call->scheduleDateValue();
|
||||
}) | rpl::flatten_latest()));
|
||||
std::move(
|
||||
muted
|
||||
) | rpl::map(
|
||||
BarStateFromMuteState
|
||||
) | rpl::on_next([=](BarState state) {
|
||||
_isGroupConnecting = (state == BarState::Connecting);
|
||||
setMuted(state != BarState::Active);
|
||||
update();
|
||||
|
||||
const auto isForceMuted = (state == BarState::ForceMuted);
|
||||
if (isForceMuted) {
|
||||
_mute->clearState();
|
||||
}
|
||||
_mute->setPointerCursor(!isForceMuted);
|
||||
|
||||
const auto to = 1.;
|
||||
const auto from = _switchStateAnimation.animating()
|
||||
? (to - _switchStateAnimation.value(0.))
|
||||
: 0.;
|
||||
const auto fromMuted = *fromState;
|
||||
const auto toMuted = state;
|
||||
*fromState = state;
|
||||
|
||||
const auto crossFrom = (fromMuted != BarState::Active) ? 1. : 0.;
|
||||
const auto crossTo = (toMuted != BarState::Active) ? 1. : 0.;
|
||||
|
||||
_switchStateCallback = [=](float64 value) {
|
||||
if (_groupCall) {
|
||||
_groupBrush = QBrush(
|
||||
_gradients.gradient(fromMuted, toMuted, value));
|
||||
update();
|
||||
}
|
||||
|
||||
const auto crossProgress = (crossFrom == crossTo)
|
||||
? crossTo
|
||||
: anim::interpolateToF(crossFrom, crossTo, value);
|
||||
_mute->setProgress(crossProgress);
|
||||
};
|
||||
|
||||
_switchStateAnimation.stop();
|
||||
const auto duration = (to - from) * st::universalDuration;
|
||||
_switchStateAnimation.start(
|
||||
_switchStateCallback,
|
||||
from,
|
||||
to,
|
||||
duration);
|
||||
}, _mute->lifetime());
|
||||
|
||||
if (const auto group = _groupCall.get()) {
|
||||
subscribeToMembersChanges(group);
|
||||
|
||||
_isGroupConnecting.value(
|
||||
) | rpl::on_next([=](bool isConnecting) {
|
||||
_mute->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
isConnecting);
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
if (const auto call = _call.get()) {
|
||||
call->user()->session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::Name
|
||||
) | rpl::filter([=](const Data::PeerUpdate &update) {
|
||||
// _user may change for the same Panel.
|
||||
return (_call != nullptr) && (update.peer == _call->user());
|
||||
}) | rpl::on_next([=] {
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
setInfoLabels();
|
||||
_info->setClickedCallback([=] {
|
||||
if (const auto call = _call.get()) {
|
||||
if (Logs::DebugEnabled()
|
||||
&& (_info->clickModifiers() & Qt::ControlModifier)) {
|
||||
_show->showBox(
|
||||
Box<DebugInfoBox>(_call),
|
||||
Ui::LayerOption::CloseOther);
|
||||
} else {
|
||||
Core::App().calls().showInfoPanel(call);
|
||||
}
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
Core::App().calls().showInfoPanel(group);
|
||||
}
|
||||
});
|
||||
_hangup->setClickedCallback([this] {
|
||||
if (const auto call = _call.get()) {
|
||||
call->hangup();
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
if (!group->canManage()) {
|
||||
group->hangup();
|
||||
} else {
|
||||
_show->showBox(
|
||||
Box(
|
||||
Group::LeaveBox,
|
||||
group,
|
||||
false,
|
||||
Group::BoxContext::MainWindow),
|
||||
Ui::LayerOption::CloseOther);
|
||||
}
|
||||
}
|
||||
});
|
||||
updateDurationText();
|
||||
}
|
||||
|
||||
void TopBar::initBlobsUnder(
|
||||
QWidget *blobsParent,
|
||||
rpl::producer<QRect> barGeometry) {
|
||||
const auto group = _groupCall.get();
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct State {
|
||||
Ui::Paint::LinearBlobs paint = {
|
||||
LinearBlobs(),
|
||||
kBlobLevelDuration,
|
||||
1.,
|
||||
Ui::Paint::LinearBlob::Direction::TopDown
|
||||
};
|
||||
Ui::Animations::Simple hideAnimation;
|
||||
Ui::Animations::Basic animation;
|
||||
base::Timer levelTimer;
|
||||
crl::time hideLastTime = 0;
|
||||
crl::time lastTime = 0;
|
||||
float lastLevel = 0.;
|
||||
float levelBeforeLast = 0.;
|
||||
};
|
||||
|
||||
_blobs = base::make_unique_q<Ui::RpWidget>(blobsParent);
|
||||
|
||||
const auto state = _blobs->lifetime().make_state<State>();
|
||||
state->levelTimer.setCallback([=] {
|
||||
state->levelBeforeLast = state->lastLevel;
|
||||
state->lastLevel = 0.;
|
||||
if (state->levelBeforeLast == 0.) {
|
||||
state->paint.setLevel(0.);
|
||||
state->levelTimer.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
state->animation.init([=](crl::time now) {
|
||||
if (const auto last = state->hideLastTime; (last > 0)
|
||||
&& (now - last >= kHideBlobsDuration)) {
|
||||
state->animation.stop();
|
||||
return false;
|
||||
}
|
||||
state->paint.updateLevel(now - state->lastTime);
|
||||
state->lastTime = now;
|
||||
|
||||
_blobs->update();
|
||||
return true;
|
||||
});
|
||||
|
||||
group->stateValue(
|
||||
) | rpl::on_next([=](Calls::GroupCall::State state) {
|
||||
if (state == Calls::GroupCall::State::HangingUp) {
|
||||
_blobs->hide();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
using namespace rpl::mappers;
|
||||
auto hideBlobs = rpl::combine(
|
||||
PowerSaving::OnValue(PowerSaving::kCalls),
|
||||
Core::App().appDeactivatedValue(),
|
||||
group->instanceStateValue()
|
||||
) | rpl::map(_1 || _2 || _3 == GroupCall::InstanceState::Disconnected);
|
||||
|
||||
std::move(
|
||||
hideBlobs
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](bool hide) {
|
||||
if (hide) {
|
||||
state->paint.setLevel(0.);
|
||||
}
|
||||
state->hideLastTime = hide ? crl::now() : 0;
|
||||
if (!hide && !state->animation.animating()) {
|
||||
state->animation.start();
|
||||
}
|
||||
if (hide) {
|
||||
state->levelTimer.cancel();
|
||||
} else {
|
||||
state->lastLevel = 0.;
|
||||
}
|
||||
|
||||
const auto from = hide ? 0. : 1.;
|
||||
const auto to = hide ? 1. : 0.;
|
||||
state->hideAnimation.start([=](float64) {
|
||||
_blobs->update();
|
||||
}, from, to, kHideBlobsDuration);
|
||||
}, lifetime());
|
||||
|
||||
std::move(
|
||||
barGeometry
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
_blobs->resize(
|
||||
rect.width(),
|
||||
(int)state->paint.maxRadius());
|
||||
_blobs->moveToLeft(rect.x(), rect.y() + rect.height());
|
||||
}, lifetime());
|
||||
|
||||
shownValue(
|
||||
) | rpl::on_next([=](bool shown) {
|
||||
_blobs->setVisible(shown);
|
||||
}, lifetime());
|
||||
|
||||
_blobs->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
const auto hidden = state->hideAnimation.value(
|
||||
state->hideLastTime ? 1. : 0.);
|
||||
if (hidden == 1.) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto p = QPainter(_blobs);
|
||||
if (hidden > 0.) {
|
||||
p.setOpacity(1. - hidden);
|
||||
}
|
||||
const auto top = -_blobs->height() * hidden;
|
||||
const auto width = _blobs->width();
|
||||
p.translate(0, top);
|
||||
state->paint.paint(p, _groupBrush, width);
|
||||
}, _blobs->lifetime());
|
||||
|
||||
group->levelUpdates(
|
||||
) | rpl::filter([=](const LevelUpdate &update) {
|
||||
return !state->hideLastTime && (update.value > state->lastLevel);
|
||||
}) | rpl::on_next([=](const LevelUpdate &update) {
|
||||
if (state->lastLevel == 0.) {
|
||||
state->levelTimer.callEach(kBlobUpdateInterval);
|
||||
}
|
||||
state->lastLevel = update.value;
|
||||
state->paint.setLevel(update.value);
|
||||
}, _blobs->lifetime());
|
||||
|
||||
_blobs->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_blobs->show();
|
||||
|
||||
if (!state->hideLastTime) {
|
||||
state->animation.start();
|
||||
}
|
||||
}
|
||||
|
||||
void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
|
||||
const auto peer = call->peer();
|
||||
const auto group = _groupCall.get();
|
||||
const auto conference = group && group->conference();
|
||||
auto realValue = conference
|
||||
? (rpl::single(group->sharedCall().get()) | rpl::type_erased)
|
||||
: peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::GroupCall
|
||||
) | rpl::map([=] {
|
||||
return peer->groupCall();
|
||||
}) | rpl::filter([=](Data::GroupCall *real) {
|
||||
const auto call = _groupCall.get();
|
||||
return call && real && (real->id() == call->id());
|
||||
}) | rpl::take(1);
|
||||
std::move(
|
||||
realValue
|
||||
) | rpl::before_next([=](not_null<Data::GroupCall*> real) {
|
||||
real->titleValue() | rpl::on_next([=] {
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
}) | rpl::map([=](not_null<Data::GroupCall*> real) {
|
||||
return HistoryView::GroupCallBarContentByCall(
|
||||
real,
|
||||
st::groupCallTopBarUserpics.size);
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=](const Ui::GroupCallBarContent &content) {
|
||||
if (_users.size() != content.users.size()
|
||||
|| (conference && _usersCount != content.count)) {
|
||||
return true;
|
||||
}
|
||||
for (auto i = 0, count = int(_users.size()); i != count; ++i) {
|
||||
if (_users[i].userpicKey != content.users[i].userpicKey
|
||||
|| _users[i].id != content.users[i].id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) | rpl::on_next([=](const Ui::GroupCallBarContent &content) {
|
||||
_users = content.users;
|
||||
_usersCount = content.count;
|
||||
for (auto &user : _users) {
|
||||
user.speaking = false;
|
||||
}
|
||||
_userpics->update(_users, !isHidden());
|
||||
if (conference) {
|
||||
updateInfoLabels();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_userpics->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_userpicsWidth = width;
|
||||
updateControlsGeometry();
|
||||
}, lifetime());
|
||||
|
||||
call->peer()->session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::Name
|
||||
) | rpl::filter([=](const Data::PeerUpdate &update) {
|
||||
// _peer may change for the same Panel.
|
||||
const auto call = _groupCall.get();
|
||||
return (call != nullptr) && (update.peer == call->peer());
|
||||
}) | rpl::on_next([=] {
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void TopBar::updateUserpics() {
|
||||
update(_mute->width(), 0, _userpics->maxWidth(), height());
|
||||
}
|
||||
|
||||
void TopBar::updateInfoLabels() {
|
||||
setInfoLabels();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void TopBar::setInfoLabels() {
|
||||
if (const auto call = _call.get()) {
|
||||
const auto user = call->user();
|
||||
const auto fullName = user->name();
|
||||
const auto shortName = user->firstName;
|
||||
_fullInfoLabel->setText(fullName);
|
||||
_shortInfoLabel->setText(shortName);
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
const auto peer = group->peer();
|
||||
const auto real = peer->groupCall();
|
||||
const auto connecting = _isGroupConnecting.current();
|
||||
if (!group->conference()) {
|
||||
_shortInfoLabel.destroy();
|
||||
}
|
||||
if (!group->conference() || connecting) {
|
||||
const auto name = peer->name();
|
||||
const auto title = (real && real->id() == group->id())
|
||||
? real->title()
|
||||
: QString();
|
||||
const auto text = _isGroupConnecting.current()
|
||||
? tr::lng_group_call_connecting(tr::now)
|
||||
: !title.isEmpty()
|
||||
? title
|
||||
: name;
|
||||
_fullInfoLabel->setText(text);
|
||||
if (_shortInfoLabel) {
|
||||
_shortInfoLabel->setText(text);
|
||||
}
|
||||
} else if (!_usersCount
|
||||
|| _users.empty()
|
||||
|| (_users.size() == 1
|
||||
&& _users.front().id == peer->session().userPeerId().value
|
||||
&& _usersCount == 1)) {
|
||||
_fullInfoLabel->setText(tr::lng_confcall_join_title(tr::now));
|
||||
_shortInfoLabel->setText(tr::lng_confcall_join_title(tr::now));
|
||||
} else {
|
||||
const auto textWithUserpics = [&](int userpics) {
|
||||
const auto other = std::max(_usersCount - userpics, 0);
|
||||
auto names = QStringList();
|
||||
for (const auto &entry : _users) {
|
||||
const auto user = peer->owner().peer(PeerId(entry.id));
|
||||
names.push_back(user->shortName());
|
||||
if (names.size() >= userpics) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (other > 0) {
|
||||
return tr::lng_forwarding_from(
|
||||
tr::now,
|
||||
lt_count,
|
||||
other,
|
||||
lt_user,
|
||||
names.join(u", "_q));
|
||||
} else if (userpics > 1) {
|
||||
return tr::lng_forwarding_from_two(
|
||||
tr::now,
|
||||
lt_user,
|
||||
names.mid(0, userpics - 1).join(u", "_q),
|
||||
lt_second_user,
|
||||
names.back());
|
||||
}
|
||||
return names.back();
|
||||
};
|
||||
_fullInfoLabel->setText(textWithUserpics(int(_users.size())));
|
||||
_shortInfoLabel->setText(textWithUserpics(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopBar::setMuted(bool mute) {
|
||||
_mute->setRippleColorOverride(&st::shadowFg);
|
||||
_hangup->setRippleColorOverride(&st::shadowFg);
|
||||
_muted = mute;
|
||||
}
|
||||
|
||||
void TopBar::updateDurationText() {
|
||||
if (!_call || !_durationLabel) {
|
||||
return;
|
||||
}
|
||||
auto wasWidth = _durationLabel->width();
|
||||
auto durationMs = _call->getDurationMs();
|
||||
auto durationSeconds = durationMs / 1000;
|
||||
startDurationUpdateTimer(durationMs);
|
||||
_durationLabel->setText(Ui::FormatDurationText(durationSeconds));
|
||||
if (_durationLabel->width() != wasWidth) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void TopBar::startDurationUpdateTimer(crl::time currentDuration) {
|
||||
auto msTillNextSecond = 1000 - (currentDuration % 1000);
|
||||
_updateDurationTimer.callOnce(msTillNextSecond + 5);
|
||||
}
|
||||
|
||||
void TopBar::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void TopBar::updateControlsGeometry() {
|
||||
auto left = 0;
|
||||
_mute->moveToLeft(left, 0);
|
||||
left += _mute->width();
|
||||
if (_durationLabel) {
|
||||
_durationLabel->moveToLeft(left, st::callBarLabelTop);
|
||||
left += _durationLabel->width() + st::callBarSkip;
|
||||
}
|
||||
if (_userpicsWidth) {
|
||||
const auto single = st::groupCallTopBarUserpics.size;
|
||||
const auto skip = anim::interpolate(
|
||||
0,
|
||||
st::callBarSkip,
|
||||
std::min(_userpicsWidth, single) / float64(single));
|
||||
left += _userpicsWidth + skip;
|
||||
}
|
||||
if (_signalBars) {
|
||||
_signalBars->moveToLeft(left, (height() - _signalBars->height()) / 2);
|
||||
left += _signalBars->width() + st::callBarSkip;
|
||||
}
|
||||
|
||||
auto right = st::callBarRightSkip;
|
||||
if (_hangupLabel) {
|
||||
_hangupLabel->moveToRight(right, st::callBarLabelTop);
|
||||
right += _hangupLabel->width();
|
||||
} else {
|
||||
//right -= st::callBarRightSkip;
|
||||
}
|
||||
right += st::callBarHangup.width;
|
||||
_hangup->setGeometryToRight(0, 0, right, height());
|
||||
_info->setGeometryToLeft(
|
||||
_mute->width(),
|
||||
0,
|
||||
width() - _mute->width() - _hangup->width(),
|
||||
height());
|
||||
|
||||
auto fullWidth = _fullInfoLabel->textMaxWidth();
|
||||
auto showFull = !_shortInfoLabel
|
||||
|| (left + fullWidth + right <= width());
|
||||
auto setInfoLabelGeometry = [this, left, right](auto &&infoLabel) {
|
||||
auto minPadding = qMax(left, right);
|
||||
auto infoWidth = infoLabel->textMaxWidth();
|
||||
auto infoLeft = (width() - infoWidth) / 2;
|
||||
if (infoLeft < minPadding) {
|
||||
infoLeft = left;
|
||||
infoWidth = width() - left - right;
|
||||
}
|
||||
infoLabel->setGeometryToLeft(infoLeft, st::callBarLabelTop, infoWidth, st::callBarInfoLabel.style.font->height);
|
||||
};
|
||||
|
||||
_fullInfoLabel->setVisible(showFull);
|
||||
setInfoLabelGeometry(_fullInfoLabel);
|
||||
if (_shortInfoLabel) {
|
||||
_shortInfoLabel->setVisible(!showFull);
|
||||
setInfoLabelGeometry(_shortInfoLabel);
|
||||
}
|
||||
|
||||
_gradients.set_points(
|
||||
QPointF(0, st::callBarHeight / 2),
|
||||
QPointF(width(), st::callBarHeight / 2));
|
||||
if (!_switchStateAnimation.animating()) {
|
||||
_switchStateCallback(1.);
|
||||
}
|
||||
}
|
||||
|
||||
void TopBar::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
auto brush = _groupCall
|
||||
? _groupBrush
|
||||
: (_muted ? st::callBarBgMuted : st::callBarBg);
|
||||
p.fillRect(e->rect(), std::move(brush));
|
||||
|
||||
if (_userpicsWidth) {
|
||||
const auto size = st::groupCallTopBarUserpics.size;
|
||||
const auto top = (height() - size) / 2;
|
||||
_userpics->paint(p, _mute->width(), top, size);
|
||||
}
|
||||
}
|
||||
|
||||
TopBar::~TopBar() = default;
|
||||
|
||||
} // namespace Calls
|
||||
115
Telegram/SourceFiles/calls/calls_top_bar.h
Normal file
115
Telegram/SourceFiles/calls/calls_top_bar.h
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/gradient.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class IconButton;
|
||||
class AbstractButton;
|
||||
class LabelSimple;
|
||||
class FlatLabel;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Call;
|
||||
class GroupCall;
|
||||
class SignalBars;
|
||||
class Mute;
|
||||
enum class MuteState;
|
||||
enum class BarState;
|
||||
|
||||
class TopBar : public Ui::RpWidget {
|
||||
public:
|
||||
TopBar(
|
||||
QWidget *parent,
|
||||
Call *call,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
TopBar(
|
||||
QWidget *parent,
|
||||
GroupCall *call,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
~TopBar();
|
||||
|
||||
void initBlobsUnder(
|
||||
QWidget *blobsParent,
|
||||
rpl::producer<QRect> barGeometry);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
struct User;
|
||||
|
||||
TopBar(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Call *call,
|
||||
GroupCall *groupCall);
|
||||
|
||||
void initControls();
|
||||
void setupInitialBrush();
|
||||
void updateInfoLabels();
|
||||
void setInfoLabels();
|
||||
void updateDurationText();
|
||||
void updateControlsGeometry();
|
||||
void startDurationUpdateTimer(crl::time currentDuration);
|
||||
void setMuted(bool mute);
|
||||
|
||||
void subscribeToMembersChanges(not_null<GroupCall*> call);
|
||||
void updateUserpics();
|
||||
|
||||
const base::weak_ptr<Call> _call;
|
||||
const base::weak_ptr<GroupCall> _groupCall;
|
||||
const std::shared_ptr<Ui::Show> _show;
|
||||
|
||||
bool _muted = false;
|
||||
std::vector<Ui::GroupCallUser> _users;
|
||||
int _usersCount = 0;
|
||||
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
|
||||
int _userpicsWidth = 0;
|
||||
object_ptr<Ui::LabelSimple> _durationLabel;
|
||||
object_ptr<SignalBars> _signalBars;
|
||||
object_ptr<Ui::FlatLabel> _fullInfoLabel;
|
||||
object_ptr<Ui::FlatLabel> _shortInfoLabel;
|
||||
object_ptr<Ui::LabelSimple> _hangupLabel;
|
||||
object_ptr<Mute> _mute;
|
||||
object_ptr<Ui::AbstractButton> _info;
|
||||
object_ptr<Ui::IconButton> _hangup;
|
||||
base::unique_qptr<Ui::RpWidget> _blobs;
|
||||
|
||||
rpl::variable<bool> _isGroupConnecting = false;
|
||||
|
||||
std::vector<not_null<PeerData*>> _conferenceFirstUsers;
|
||||
int _conferenceUsersCount = 0;
|
||||
|
||||
QBrush _groupBrush;
|
||||
anim::linear_gradients<BarState> _gradients;
|
||||
Ui::Animations::Simple _switchStateAnimation;
|
||||
Fn<void(float64)> _switchStateCallback;
|
||||
|
||||
base::Timer _updateDurationTimer;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
231
Telegram/SourceFiles/calls/calls_userpic.cpp
Normal file
231
Telegram/SourceFiles/calls/calls_userpic.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
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/calls_userpic.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/painter.h"
|
||||
#include "apiwrap.h" // requestFullPeer.
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Userpic::Userpic(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<PeerData*> peer,
|
||||
rpl::producer<bool> muted)
|
||||
: _content(parent)
|
||||
, _peer(peer) {
|
||||
setGeometry(0, 0, 0);
|
||||
setup(std::move(muted));
|
||||
}
|
||||
|
||||
Userpic::~Userpic() = default;
|
||||
|
||||
void Userpic::setVisible(bool visible) {
|
||||
_content.setVisible(visible);
|
||||
}
|
||||
|
||||
void Userpic::setGeometry(int x, int y, int size) {
|
||||
if (this->size() != size) {
|
||||
_userPhoto = QPixmap();
|
||||
_userPhotoFull = false;
|
||||
}
|
||||
_content.setGeometry(x, y, size, size);
|
||||
_content.update();
|
||||
if (_userPhoto.isNull()) {
|
||||
refreshPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
void Userpic::setup(rpl::producer<bool> muted) {
|
||||
_content.show();
|
||||
_content.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_content.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
paint();
|
||||
}, lifetime());
|
||||
|
||||
std::move(
|
||||
muted
|
||||
) | rpl::on_next([=](bool muted) {
|
||||
setMuted(muted);
|
||||
}, lifetime());
|
||||
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
) | rpl::on_next([=] {
|
||||
processPhoto();
|
||||
}, lifetime());
|
||||
|
||||
_peer->session().downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
refreshPhoto();
|
||||
}, lifetime());
|
||||
|
||||
_mutedAnimation.stop();
|
||||
}
|
||||
|
||||
void Userpic::setMuteLayout(QPoint position, int size, int stroke) {
|
||||
_mutePosition = position;
|
||||
_muteSize = size;
|
||||
_muteStroke = stroke;
|
||||
_content.update();
|
||||
}
|
||||
|
||||
void Userpic::paint() {
|
||||
auto p = QPainter(&_content);
|
||||
|
||||
p.drawPixmap(0, 0, _userPhoto);
|
||||
if (_muted && _muteSize > 0) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::callBgOpaque->p;
|
||||
pen.setWidth(_muteStroke);
|
||||
p.setPen(pen);
|
||||
p.setBrush(st::callHangupBg);
|
||||
const auto rect = QRect(
|
||||
_mutePosition.x() - _muteSize / 2,
|
||||
_mutePosition.y() - _muteSize / 2,
|
||||
_muteSize,
|
||||
_muteSize);
|
||||
p.drawEllipse(rect);
|
||||
st::callMutedPeerIcon.paintInCenter(p, rect);
|
||||
}
|
||||
}
|
||||
|
||||
void Userpic::setMuted(bool muted) {
|
||||
if (_muted == muted) {
|
||||
return;
|
||||
}
|
||||
_muted = muted;
|
||||
_content.update();
|
||||
//_mutedAnimation.start(
|
||||
// [=] { _content.update(); },
|
||||
// _muted ? 0. : 1.,
|
||||
// _muted ? 1. : 0.,
|
||||
// st::fadeWrapDuration);
|
||||
}
|
||||
|
||||
int Userpic::size() const {
|
||||
return _content.width();
|
||||
}
|
||||
|
||||
void Userpic::processPhoto() {
|
||||
_userpic = _peer->createUserpicView();
|
||||
_peer->loadUserpic();
|
||||
const auto photo = _peer->userpicPhotoId()
|
||||
? _peer->owner().photo(_peer->userpicPhotoId()).get()
|
||||
: nullptr;
|
||||
if (isGoodPhoto(photo)) {
|
||||
_photo = photo->createMediaView();
|
||||
_photo->wanted(Data::PhotoSize::Thumbnail, _peer->userpicPhotoOrigin());
|
||||
} else {
|
||||
_photo = nullptr;
|
||||
if (_peer->userpicPhotoUnknown() || (photo && photo->isNull())) {
|
||||
_peer->session().api().requestFullPeer(_peer);
|
||||
}
|
||||
}
|
||||
refreshPhoto();
|
||||
}
|
||||
|
||||
void Userpic::refreshPhoto() {
|
||||
if (!size()) {
|
||||
return;
|
||||
}
|
||||
const auto isNewBigPhoto = [&] {
|
||||
return _photo
|
||||
&& (_photo->image(Data::PhotoSize::Thumbnail) != nullptr)
|
||||
&& (_photo->owner()->id != _userPhotoId || !_userPhotoFull);
|
||||
}();
|
||||
if (isNewBigPhoto) {
|
||||
_userPhotoId = _photo->owner()->id;
|
||||
_userPhotoFull = true;
|
||||
createCache(_photo->image(Data::PhotoSize::Thumbnail));
|
||||
} else if (_userPhoto.isNull()) {
|
||||
if (const auto cloud = _peer->userpicCloudImage(_userpic)) {
|
||||
auto image = Image(base::duplicate(*cloud));
|
||||
createCache(&image);
|
||||
} else {
|
||||
createCache(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Userpic::createCache(Image *image) {
|
||||
const auto size = this->size();
|
||||
const auto real = size * style::DevicePixelRatio();
|
||||
//_useTransparency
|
||||
// ? (Images::Option::RoundLarge
|
||||
// | Images::Option::RoundSkipBottomLeft
|
||||
// | Images::Option::RoundSkipBottomRight)
|
||||
// : Images::Option::None;
|
||||
if (image) {
|
||||
auto width = image->width();
|
||||
auto height = image->height();
|
||||
if (width > height) {
|
||||
width = qMax((width * real) / height, 1);
|
||||
height = real;
|
||||
} else {
|
||||
height = qMax((height * real) / width, 1);
|
||||
width = real;
|
||||
}
|
||||
_userPhoto = image->pixNoCache(
|
||||
{ width, height },
|
||||
{
|
||||
.options = Images::Option::RoundCircle,
|
||||
.outer = { size, size },
|
||||
});
|
||||
_userPhoto.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
} else {
|
||||
auto filled = QImage(
|
||||
QSize(real, real),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
filled.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
filled.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&filled);
|
||||
Ui::EmptyUserpic(
|
||||
Ui::EmptyUserpic::UserpicColor(_peer->colorIndex()),
|
||||
_peer->name()
|
||||
).paintCircle(p, 0, 0, size, size);
|
||||
}
|
||||
//_userPhoto = Images::PixmapFast(Images::Round(
|
||||
// std::move(filled),
|
||||
// ImageRoundRadius::Large,
|
||||
// RectPart::TopLeft | RectPart::TopRight));
|
||||
_userPhoto = Images::PixmapFast(std::move(filled));
|
||||
}
|
||||
|
||||
_content.update();
|
||||
}
|
||||
|
||||
bool Userpic::isGoodPhoto(PhotoData *photo) const {
|
||||
if (!photo || photo->isNull()) {
|
||||
return false;
|
||||
}
|
||||
const auto badAspect = [](int a, int b) {
|
||||
return a > 10 * b;
|
||||
};
|
||||
const auto width = photo->width();
|
||||
const auto height = photo->height();
|
||||
return !badAspect(width, height) && !badAspect(height, width);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
67
Telegram/SourceFiles/calls/calls_userpic.h
Normal file
67
Telegram/SourceFiles/calls/calls_userpic.h
Normal 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 "ui/rp_widget.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class PeerData;
|
||||
class Image;
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Userpic final {
|
||||
public:
|
||||
Userpic(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<PeerData*> peer,
|
||||
rpl::producer<bool> muted);
|
||||
~Userpic();
|
||||
|
||||
void setVisible(bool visible);
|
||||
void setGeometry(int x, int y, int size);
|
||||
void setMuteLayout(QPoint position, int size, int stroke);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _content.lifetime();
|
||||
}
|
||||
|
||||
private:
|
||||
void setup(rpl::producer<bool> muted);
|
||||
|
||||
void paint();
|
||||
void setMuted(bool muted);
|
||||
[[nodiscard]] int size() const;
|
||||
|
||||
void processPhoto();
|
||||
void refreshPhoto();
|
||||
[[nodiscard]] bool isGoodPhoto(PhotoData *photo) const;
|
||||
void createCache(Image *image);
|
||||
|
||||
Ui::RpWidget _content;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
Ui::PeerUserpicView _userpic;
|
||||
std::shared_ptr<Data::PhotoMedia> _photo;
|
||||
Ui::Animations::Simple _mutedAnimation;
|
||||
QPixmap _userPhoto;
|
||||
PhotoId _userPhotoId = 0;
|
||||
QPoint _mutePosition;
|
||||
int _muteSize = 0;
|
||||
int _muteStroke = 0;
|
||||
bool _userPhotoFull = false;
|
||||
bool _muted = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
268
Telegram/SourceFiles/calls/calls_video_bubble.cpp
Normal file
268
Telegram/SourceFiles/calls/calls_video_bubble.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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/calls_video_bubble.h"
|
||||
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
VideoBubble::VideoBubble(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track)
|
||||
: _content(parent)
|
||||
, _track(track)
|
||||
, _state(Webrtc::VideoState::Inactive) {
|
||||
setup();
|
||||
}
|
||||
|
||||
void VideoBubble::setup() {
|
||||
_content.show();
|
||||
applyDragMode(_dragMode);
|
||||
|
||||
_content.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
paint();
|
||||
}, lifetime());
|
||||
|
||||
_track->stateValue(
|
||||
) | rpl::on_next([=](Webrtc::VideoState state) {
|
||||
setState(state);
|
||||
}, lifetime());
|
||||
|
||||
_track->renderNextFrame(
|
||||
) | rpl::on_next([=] {
|
||||
if (_track->frameSize().isEmpty()) {
|
||||
_track->markFrameShown();
|
||||
} else {
|
||||
updateVisibility();
|
||||
// We update whole parent widget in this case.
|
||||
// In case we update only bubble without the parent incoming
|
||||
// video frame we may get full parent of old frame with a
|
||||
// rectangular piece of a new frame rendered with that update().
|
||||
//_content.update();
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void VideoBubble::updateGeometry(
|
||||
DragMode mode,
|
||||
QRect boundingRect,
|
||||
QSize sizeMin,
|
||||
QSize sizeMax) {
|
||||
Expects(!boundingRect.isEmpty());
|
||||
Expects(sizeMax.isEmpty() || !sizeMin.isEmpty());
|
||||
Expects(sizeMax.isEmpty() || sizeMin.width() <= sizeMax.width());
|
||||
Expects(sizeMax.isEmpty() || sizeMin.height() <= sizeMax.height());
|
||||
|
||||
if (sizeMin.isEmpty()) {
|
||||
sizeMin = boundingRect.size();
|
||||
}
|
||||
if (sizeMax.isEmpty()) {
|
||||
sizeMax = sizeMin;
|
||||
}
|
||||
if (_dragMode != mode) {
|
||||
applyDragMode(mode);
|
||||
}
|
||||
if (_boundingRect != boundingRect) {
|
||||
applyBoundingRect(boundingRect);
|
||||
}
|
||||
if (_min != sizeMin || _max != sizeMax) {
|
||||
applySizeConstraints(sizeMin, sizeMax);
|
||||
}
|
||||
if (_geometryDirty && !_lastFrameSize.isEmpty()) {
|
||||
updateSizeToFrame(base::take(_lastFrameSize));
|
||||
}
|
||||
}
|
||||
|
||||
QRect VideoBubble::geometry() const {
|
||||
return _content.isHidden() ? QRect() : _content.geometry();
|
||||
}
|
||||
|
||||
void VideoBubble::applyBoundingRect(QRect rect) {
|
||||
_boundingRect = rect;
|
||||
_geometryDirty = true;
|
||||
}
|
||||
|
||||
void VideoBubble::applyDragMode(DragMode mode) {
|
||||
_dragMode = mode;
|
||||
if (_dragMode == DragMode::None) {
|
||||
_dragging = false;
|
||||
_content.setCursor(style::cur_default);
|
||||
}
|
||||
_content.setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
true/*(_dragMode == DragMode::None)*/);
|
||||
if (_dragMode == DragMode::SnapToCorners) {
|
||||
_corner = RectPart::BottomRight;
|
||||
} else {
|
||||
_corner = RectPart::None;
|
||||
_lastDraggableSize = _size;
|
||||
}
|
||||
_size = QSize();
|
||||
_geometryDirty = true;
|
||||
}
|
||||
|
||||
void VideoBubble::applySizeConstraints(QSize min, QSize max) {
|
||||
_min = min;
|
||||
_max = max;
|
||||
_geometryDirty = true;
|
||||
}
|
||||
|
||||
void VideoBubble::paint() {
|
||||
auto p = QPainter(&_content);
|
||||
|
||||
prepareFrame();
|
||||
if (!_frame.isNull()) {
|
||||
const auto padding = st::boxRoundShadow.extend;
|
||||
const auto inner = _content.rect().marginsRemoved(padding);
|
||||
Ui::Shadow::paint(p, inner, _content.width(), st::boxRoundShadow);
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto left = _mirrored
|
||||
? (_frame.width() - (inner.width() * factor))
|
||||
: 0;
|
||||
p.drawImage(
|
||||
inner,
|
||||
_frame,
|
||||
QRect(QPoint(left, 0), inner.size() * factor));
|
||||
}
|
||||
_track->markFrameShown();
|
||||
}
|
||||
|
||||
void VideoBubble::prepareFrame() {
|
||||
const auto original = _track->frameSize();
|
||||
if (original.isEmpty()) {
|
||||
_frame = QImage();
|
||||
return;
|
||||
}
|
||||
const auto padding = st::boxRoundShadow.extend;
|
||||
const auto size = (_content.rect() - padding).size()
|
||||
* style::DevicePixelRatio();
|
||||
|
||||
// Should we check 'original' and 'size' aspect ratios?..
|
||||
const auto request = Webrtc::FrameRequest{
|
||||
.resize = size,
|
||||
.outer = size,
|
||||
};
|
||||
const auto frame = _track->frame(request);
|
||||
if (_frame.width() < size.width() || _frame.height() < size.height()) {
|
||||
_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
_frame.fill(Qt::transparent);
|
||||
}
|
||||
Assert(_frame.width() >= frame.width()
|
||||
&& _frame.height() >= frame.height());
|
||||
const auto dstPerLine = _frame.bytesPerLine();
|
||||
const auto srcPerLine = frame.bytesPerLine();
|
||||
const auto lineSize = frame.width() * 4;
|
||||
auto dst = _frame.bits();
|
||||
auto src = frame.bits();
|
||||
const auto till = src + frame.height() * srcPerLine;
|
||||
for (; src != till; src += srcPerLine, dst += dstPerLine) {
|
||||
memcpy(dst, src, lineSize);
|
||||
}
|
||||
_frame = Images::Round(
|
||||
std::move(_frame),
|
||||
ImageRoundRadius::Large,
|
||||
RectPart::AllCorners,
|
||||
QRect(QPoint(), size)
|
||||
).mirrored(_mirrored, false);
|
||||
}
|
||||
|
||||
void VideoBubble::setState(Webrtc::VideoState state) {
|
||||
if (state == Webrtc::VideoState::Paused) {
|
||||
using namespace Images;
|
||||
static constexpr auto kRadius = 24;
|
||||
_pausedFrame = Images::BlurLargeImage(_track->frame({}), kRadius);
|
||||
if (_pausedFrame.isNull()) {
|
||||
state = Webrtc::VideoState::Inactive;
|
||||
}
|
||||
}
|
||||
_state = state;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
void VideoBubble::updateSizeToFrame(QSize frame) {
|
||||
Expects(!frame.isEmpty());
|
||||
|
||||
if (_lastFrameSize == frame) {
|
||||
return;
|
||||
}
|
||||
_lastFrameSize = frame;
|
||||
|
||||
auto size = !_size.isEmpty()
|
||||
? QSize(
|
||||
std::clamp(_size.width(), _min.width(), _max.width()),
|
||||
std::clamp(_size.height(), _min.height(), _max.height()))
|
||||
: (_dragMode == DragMode::None || _lastDraggableSize.isEmpty())
|
||||
? QSize()
|
||||
: _lastDraggableSize;
|
||||
if (size.isEmpty()) {
|
||||
size = frame.scaled((_min + _max) / 2, Qt::KeepAspectRatio);
|
||||
} else {
|
||||
const auto area = size.width() * size.height();
|
||||
const auto w = int(base::SafeRound(std::max(
|
||||
std::sqrt((frame.width() * float64(area)) / (frame.height() * 1.)),
|
||||
1.)));
|
||||
const auto h = area / w;
|
||||
size = QSize(w, h);
|
||||
if (w > _max.width() || h > _max.height()) {
|
||||
size = size.scaled(_max, Qt::KeepAspectRatio);
|
||||
}
|
||||
}
|
||||
size = QSize(std::max(1, size.width()), std::max(1, size.height()));
|
||||
setInnerSize(size);
|
||||
}
|
||||
|
||||
void VideoBubble::setInnerSize(QSize size) {
|
||||
if (_size == size && !_geometryDirty) {
|
||||
return;
|
||||
}
|
||||
_geometryDirty = false;
|
||||
_size = size;
|
||||
const auto topLeft = [&] {
|
||||
switch (_corner) {
|
||||
case RectPart::None:
|
||||
return _boundingRect.topLeft() + QPoint(
|
||||
(_boundingRect.width() - size.width()) / 2,
|
||||
(_boundingRect.height() - size.height()) / 2);
|
||||
case RectPart::TopLeft:
|
||||
return _boundingRect.topLeft();
|
||||
case RectPart::TopRight:
|
||||
return QPoint(
|
||||
_boundingRect.x() + _boundingRect.width() - size.width(),
|
||||
_boundingRect.y());
|
||||
case RectPart::BottomRight:
|
||||
return QPoint(
|
||||
_boundingRect.x() + _boundingRect.width() - size.width(),
|
||||
_boundingRect.y() + _boundingRect.height() - size.height());
|
||||
case RectPart::BottomLeft:
|
||||
return QPoint(
|
||||
_boundingRect.x(),
|
||||
_boundingRect.y() + _boundingRect.height() - size.height());
|
||||
}
|
||||
Unexpected("Corner value in VideoBubble::setInnerSize.");
|
||||
}();
|
||||
const auto inner = QRect(topLeft, size);
|
||||
_content.setGeometry(inner.marginsAdded(st::boxRoundShadow.extend));
|
||||
}
|
||||
|
||||
void VideoBubble::updateVisibility() {
|
||||
const auto size = _track->frameSize();
|
||||
const auto visible = (_state != Webrtc::VideoState::Inactive)
|
||||
&& !size.isEmpty();
|
||||
if (visible) {
|
||||
updateSizeToFrame(size);
|
||||
}
|
||||
_content.setVisible(visible);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
71
Telegram/SourceFiles/calls/calls_video_bubble.h
Normal file
71
Telegram/SourceFiles/calls/calls_video_bubble.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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/rect_part.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Webrtc {
|
||||
class VideoTrack;
|
||||
enum class VideoState;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class VideoBubble final {
|
||||
public:
|
||||
VideoBubble(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track);
|
||||
|
||||
enum class DragMode {
|
||||
None,
|
||||
SnapToCorners,
|
||||
};
|
||||
void updateGeometry(
|
||||
DragMode mode,
|
||||
QRect boundingRect,
|
||||
QSize sizeMin = QSize(),
|
||||
QSize sizeMax = QSize());
|
||||
[[nodiscard]] QRect geometry() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _content.lifetime();
|
||||
}
|
||||
|
||||
void setMirrored(bool mirrored) {
|
||||
_mirrored = mirrored;
|
||||
}
|
||||
|
||||
private:
|
||||
void setup();
|
||||
void paint();
|
||||
void setState(Webrtc::VideoState state);
|
||||
void applyDragMode(DragMode mode);
|
||||
void applyBoundingRect(QRect rect);
|
||||
void applySizeConstraints(QSize min, QSize max);
|
||||
void updateSizeToFrame(QSize frame);
|
||||
void updateVisibility();
|
||||
void setInnerSize(QSize size);
|
||||
void prepareFrame();
|
||||
|
||||
Ui::RpWidget _content;
|
||||
const not_null<Webrtc::VideoTrack*> _track;
|
||||
Webrtc::VideoState _state = Webrtc::VideoState();
|
||||
QImage _frame, _pausedFrame;
|
||||
QSize _min, _max, _size, _lastDraggableSize, _lastFrameSize;
|
||||
QRect _boundingRect;
|
||||
DragMode _dragMode = DragMode::None;
|
||||
RectPart _corner = RectPart::None;
|
||||
bool _dragging = false;
|
||||
bool _geometryDirty = false;
|
||||
bool _mirrored = true;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
573
Telegram/SourceFiles/calls/calls_video_incoming.cpp
Normal file
573
Telegram/SourceFiles/calls/calls_video_incoming.cpp
Normal file
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
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/calls_video_incoming.h"
|
||||
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_shader.h"
|
||||
#include "ui/gl/gl_image.h"
|
||||
#include "ui/gl/gl_primitives.h"
|
||||
#include "ui/painter.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QOpenGLShader>
|
||||
#include <QOpenGLBuffer>
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBottomShadowAlphaMax = 74;
|
||||
|
||||
using namespace Ui::GL;
|
||||
|
||||
[[nodiscard]] ShaderPart FragmentBottomShadow() {
|
||||
return {
|
||||
.header = R"(
|
||||
uniform vec3 shadow; // fullHeight, shadowTop, maxOpacity
|
||||
)",
|
||||
.body = R"(
|
||||
float shadowCoord = shadow.y - gl_FragCoord.y;
|
||||
float shadowValue = clamp(shadowCoord / shadow.x, 0., 1.);
|
||||
float shadowShown = shadowValue * shadow.z;
|
||||
result = vec4(min(result.rgb, vec3(1.)) * (1. - shadowShown), result.a);
|
||||
)",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Panel::Incoming::RendererGL final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererGL(not_null<Incoming*> owner);
|
||||
|
||||
void init(QOpenGLFunctions &f) override;
|
||||
|
||||
void deinit(QOpenGLFunctions *f) override;
|
||||
|
||||
void paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
private:
|
||||
void uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const;
|
||||
void validateShadowImage();
|
||||
|
||||
const not_null<Incoming*> _owner;
|
||||
|
||||
QSize _viewport;
|
||||
float _factor = 1.;
|
||||
int _ifactor = 1;
|
||||
QVector2D _uniformViewport;
|
||||
|
||||
std::optional<QOpenGLBuffer> _contentBuffer;
|
||||
std::optional<QOpenGLShaderProgram> _argb32Program;
|
||||
QOpenGLShader *_texturedVertexShader = nullptr;
|
||||
std::optional<QOpenGLShaderProgram> _yuv420Program;
|
||||
std::optional<QOpenGLShaderProgram> _imageProgram;
|
||||
Ui::GL::Textures<4> _textures;
|
||||
QSize _rgbaSize;
|
||||
QSize _lumaSize;
|
||||
QSize _chromaSize;
|
||||
int _trackFrameIndex = 0;
|
||||
|
||||
Ui::GL::Image _controlsShadowImage;
|
||||
QRect _controlsShadowLeft;
|
||||
QRect _controlsShadowRight;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
class Panel::Incoming::RendererSW final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererSW(not_null<Incoming*> owner);
|
||||
|
||||
void paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) override;
|
||||
|
||||
private:
|
||||
void initBottomShadow();
|
||||
void fillTopShadow(QPainter &p);
|
||||
void fillBottomShadow(QPainter &p);
|
||||
|
||||
const not_null<Incoming*> _owner;
|
||||
|
||||
QImage _bottomShadow;
|
||||
|
||||
};
|
||||
|
||||
Panel::Incoming::RendererGL::RendererGL(not_null<Incoming*> owner)
|
||||
: _owner(owner) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_controlsShadowImage.invalidate();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::init(QOpenGLFunctions &f) {
|
||||
constexpr auto kQuads = 2;
|
||||
constexpr auto kQuadVertices = kQuads * 4;
|
||||
constexpr auto kQuadValues = kQuadVertices * 4;
|
||||
|
||||
_contentBuffer.emplace();
|
||||
_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||||
_contentBuffer->create();
|
||||
_contentBuffer->bind();
|
||||
_contentBuffer->allocate(kQuadValues * sizeof(GLfloat));
|
||||
|
||||
_textures.ensureCreated(f);
|
||||
|
||||
_imageProgram.emplace();
|
||||
_texturedVertexShader = LinkProgram(
|
||||
&*_imageProgram,
|
||||
VertexShader({
|
||||
VertexViewportTransform(),
|
||||
VertexPassTextureCoord(),
|
||||
}),
|
||||
FragmentShader({
|
||||
FragmentSampleARGB32Texture(),
|
||||
})).vertex;
|
||||
|
||||
_argb32Program.emplace();
|
||||
LinkProgram(
|
||||
&*_argb32Program,
|
||||
_texturedVertexShader,
|
||||
FragmentShader({
|
||||
FragmentSampleARGB32Texture(),
|
||||
FragmentBottomShadow(),
|
||||
}));
|
||||
|
||||
_yuv420Program.emplace();
|
||||
LinkProgram(
|
||||
&*_yuv420Program,
|
||||
_texturedVertexShader,
|
||||
FragmentShader({
|
||||
FragmentSampleYUV420Texture(),
|
||||
FragmentBottomShadow(),
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::deinit(QOpenGLFunctions *f) {
|
||||
_controlsShadowImage.destroy(f);
|
||||
_textures.destroy(f);
|
||||
_imageProgram = std::nullopt;
|
||||
_texturedVertexShader = nullptr;
|
||||
_argb32Program = std::nullopt;
|
||||
_yuv420Program = std::nullopt;
|
||||
_contentBuffer = std::nullopt;
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) {
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
_owner->_track->markFrameShown();
|
||||
});
|
||||
const auto data = _owner->_track->frameWithInfo(false);
|
||||
if (data.format == Webrtc::FrameFormat::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto factor = widget->devicePixelRatioF();
|
||||
if (_factor != factor) {
|
||||
_factor = factor;
|
||||
_ifactor = int(std::ceil(_factor));
|
||||
_controlsShadowImage.invalidate();
|
||||
}
|
||||
_viewport = widget->size();
|
||||
_uniformViewport = QVector2D(
|
||||
_viewport.width() * _factor,
|
||||
_viewport.height() * _factor);
|
||||
|
||||
const auto rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32);
|
||||
const auto upload = (_trackFrameIndex != data.index);
|
||||
_trackFrameIndex = data.index;
|
||||
auto &program = rgbaFrame ? _argb32Program : _yuv420Program;
|
||||
program->bind();
|
||||
if (rgbaFrame) {
|
||||
Assert(!data.original.isNull());
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_textures.bind(f, 0);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
Ui::GL::kFormatRGBA,
|
||||
Ui::GL::kFormatRGBA,
|
||||
data.original.size(),
|
||||
_rgbaSize,
|
||||
data.original.bytesPerLine() / 4,
|
||||
data.original.constBits());
|
||||
_rgbaSize = data.original.size();
|
||||
}
|
||||
program->setUniformValue("s_texture", GLint(0));
|
||||
} else {
|
||||
Assert(data.format == Webrtc::FrameFormat::YUV420);
|
||||
Assert(!data.yuv420->size.isEmpty());
|
||||
const auto yuv = data.yuv420;
|
||||
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_textures.bind(f, 1);
|
||||
if (upload) {
|
||||
f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_ALPHA,
|
||||
GL_ALPHA,
|
||||
yuv->size,
|
||||
_lumaSize,
|
||||
yuv->y.stride,
|
||||
yuv->y.data);
|
||||
_lumaSize = yuv->size;
|
||||
}
|
||||
f.glActiveTexture(GL_TEXTURE1);
|
||||
_textures.bind(f, 2);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_ALPHA,
|
||||
GL_ALPHA,
|
||||
yuv->chromaSize,
|
||||
_chromaSize,
|
||||
yuv->u.stride,
|
||||
yuv->u.data);
|
||||
}
|
||||
f.glActiveTexture(GL_TEXTURE2);
|
||||
_textures.bind(f, 3);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_ALPHA,
|
||||
GL_ALPHA,
|
||||
yuv->chromaSize,
|
||||
_chromaSize,
|
||||
yuv->v.stride,
|
||||
yuv->v.data);
|
||||
_chromaSize = yuv->chromaSize;
|
||||
f.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
}
|
||||
program->setUniformValue("y_texture", GLint(0));
|
||||
program->setUniformValue("u_texture", GLint(1));
|
||||
program->setUniformValue("v_texture", GLint(2));
|
||||
}
|
||||
const auto rect = TransformRect(
|
||||
widget->rect(),
|
||||
_viewport,
|
||||
_factor);
|
||||
std::array<std::array<GLfloat, 2>, 4> texcoords = { {
|
||||
{ { 0.f, 1.f } },
|
||||
{ { 1.f, 1.f } },
|
||||
{ { 1.f, 0.f } },
|
||||
{ { 0.f, 0.f } },
|
||||
} };
|
||||
if (const auto shift = (data.rotation / 90); shift != 0) {
|
||||
std::rotate(
|
||||
begin(texcoords),
|
||||
begin(texcoords) + shift,
|
||||
end(texcoords));
|
||||
}
|
||||
|
||||
const auto width = widget->parentWidget()->width();
|
||||
const auto left = (_owner->_topControlsAlignment == style::al_left);
|
||||
validateShadowImage();
|
||||
const auto position = left
|
||||
? QPoint()
|
||||
: QPoint(width - st::callTitleShadowRight.width(), 0);
|
||||
const auto translated = position - widget->pos();
|
||||
const auto shadowArea = QRect(translated, st::callTitleShadowLeft.size());
|
||||
const auto shadow = _controlsShadowImage.texturedRect(
|
||||
shadowArea,
|
||||
(left ? _controlsShadowLeft : _controlsShadowRight),
|
||||
widget->rect());
|
||||
const auto shadowRect = TransformRect(
|
||||
shadow.geometry,
|
||||
_viewport,
|
||||
_factor);
|
||||
|
||||
const GLfloat coords[] = {
|
||||
rect.left(), rect.top(),
|
||||
texcoords[0][0], texcoords[0][1],
|
||||
|
||||
rect.right(), rect.top(),
|
||||
texcoords[1][0], texcoords[1][1],
|
||||
|
||||
rect.right(), rect.bottom(),
|
||||
texcoords[2][0], texcoords[2][1],
|
||||
|
||||
rect.left(), rect.bottom(),
|
||||
texcoords[3][0], texcoords[3][1],
|
||||
|
||||
shadowRect.left(), shadowRect.top(),
|
||||
shadow.texture.left(), shadow.texture.bottom(),
|
||||
|
||||
shadowRect.right(), shadowRect.top(),
|
||||
shadow.texture.right(), shadow.texture.bottom(),
|
||||
|
||||
shadowRect.right(), shadowRect.bottom(),
|
||||
shadow.texture.right(), shadow.texture.top(),
|
||||
|
||||
shadowRect.left(), shadowRect.bottom(),
|
||||
shadow.texture.left(), shadow.texture.top(),
|
||||
};
|
||||
|
||||
_contentBuffer->bind();
|
||||
_contentBuffer->write(0, coords, sizeof(coords));
|
||||
|
||||
const auto bottomShadowArea = QRect(
|
||||
0,
|
||||
widget->parentWidget()->height() - st::callBottomShadowSize,
|
||||
widget->parentWidget()->width(),
|
||||
st::callBottomShadowSize);
|
||||
const auto bottomShadowFill = bottomShadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
const auto shadowHeight = bottomShadowFill.height();
|
||||
const auto shadowAlpha = (shadowHeight * kBottomShadowAlphaMax)
|
||||
/ (st::callBottomShadowSize * 255.);
|
||||
|
||||
program->setUniformValue("viewport", _uniformViewport);
|
||||
program->setUniformValue("shadow", QVector3D(
|
||||
shadowHeight * _factor,
|
||||
TransformRect(bottomShadowFill, _viewport, _factor).bottom(),
|
||||
shadowAlpha));
|
||||
|
||||
FillTexturedRectangle(f, &*program);
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
if (!shadowRect.empty()) {
|
||||
f.glEnable(GL_BLEND);
|
||||
f.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
const auto guard = gsl::finally([&] {
|
||||
f.glDisable(GL_BLEND);
|
||||
});
|
||||
|
||||
_imageProgram->bind();
|
||||
_imageProgram->setUniformValue("viewport", _uniformViewport);
|
||||
_imageProgram->setUniformValue("s_texture", GLint(0));
|
||||
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_controlsShadowImage.bind(f);
|
||||
|
||||
FillTexturedRectangle(f, &*_imageProgram, 4);
|
||||
}
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::validateShadowImage() {
|
||||
if (_controlsShadowImage) {
|
||||
return;
|
||||
}
|
||||
const auto size = st::callTitleShadowLeft.size();
|
||||
const auto full = QSize(size.width(), 2 * size.height()) * _ifactor;
|
||||
auto image = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(_ifactor);
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
st::callTitleShadowLeft.paint(p, 0, 0, size.width());
|
||||
_controlsShadowLeft = QRect(0, 0, full.width(), full.height() / 2);
|
||||
st::callTitleShadowRight.paint(p, 0, size.height(), size.width());
|
||||
_controlsShadowRight = QRect(
|
||||
0,
|
||||
full.height() / 2,
|
||||
full.width(),
|
||||
full.height() / 2);
|
||||
}
|
||||
_controlsShadowImage.setImage(std::move(image));
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const {
|
||||
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);
|
||||
if (hasSize != size) {
|
||||
f.glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
internalformat,
|
||||
size.width(),
|
||||
size.height(),
|
||||
0,
|
||||
format,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
} else {
|
||||
f.glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
size.width(),
|
||||
size.height(),
|
||||
format,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
}
|
||||
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
|
||||
Panel::Incoming::RendererSW::RendererSW(not_null<Incoming*> owner)
|
||||
: _owner(owner) {
|
||||
initBottomShadow();
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) {
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
_owner->_track->markFrameShown();
|
||||
});
|
||||
const auto data = _owner->_track->frameWithInfo(true);
|
||||
const auto &image = data.original;
|
||||
const auto rotation = data.rotation;
|
||||
if (image.isNull()) {
|
||||
p.fillRect(clip.boundingRect(), Qt::black);
|
||||
} else {
|
||||
const auto rect = _owner->widget()->rect();
|
||||
using namespace Media::View;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
if (UsePainterRotation(rotation)) {
|
||||
if (rotation) {
|
||||
p.save();
|
||||
p.rotate(rotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(rect, rotation), image);
|
||||
if (rotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (rotation) {
|
||||
p.drawImage(rect, RotateFrameImage(image, rotation));
|
||||
} else {
|
||||
p.drawImage(rect, image);
|
||||
}
|
||||
fillBottomShadow(p);
|
||||
fillTopShadow(p);
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::initBottomShadow() {
|
||||
auto image = QImage(
|
||||
QSize(1, st::callBottomShadowSize) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
const auto colorFrom = uint32(0);
|
||||
const auto colorTill = uint32(kBottomShadowAlphaMax);
|
||||
const auto rows = image.height();
|
||||
const auto step = (uint64(colorTill - colorFrom) << 32) / rows;
|
||||
auto accumulated = uint64();
|
||||
auto bytes = image.bits();
|
||||
for (auto y = 0; y != rows; ++y) {
|
||||
accumulated += step;
|
||||
const auto color = (colorFrom + uint32(accumulated >> 32)) << 24;
|
||||
for (auto x = 0; x != image.width(); ++x) {
|
||||
*(reinterpret_cast<uint32*>(bytes) + x) = color;
|
||||
}
|
||||
bytes += image.bytesPerLine();
|
||||
}
|
||||
_bottomShadow = std::move(image);
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::fillTopShadow(QPainter &p) {
|
||||
#ifndef Q_OS_MAC
|
||||
const auto widget = _owner->widget();
|
||||
const auto width = widget->parentWidget()->width();
|
||||
const auto left = (_owner->_topControlsAlignment == style::al_left);
|
||||
const auto &icon = left
|
||||
? st::callTitleShadowLeft
|
||||
: st::callTitleShadowRight;
|
||||
const auto position = left
|
||||
? QPoint()
|
||||
: QPoint(width - icon.width(), 0);
|
||||
const auto shadowArea = QRect(position, icon.size());
|
||||
const auto fill = shadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.save();
|
||||
p.setClipRect(fill);
|
||||
icon.paint(p, position - widget->pos(), width);
|
||||
p.restore();
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::fillBottomShadow(QPainter &p) {
|
||||
const auto widget = _owner->widget();
|
||||
const auto shadowArea = QRect(
|
||||
0,
|
||||
widget->parentWidget()->height() - st::callBottomShadowSize,
|
||||
widget->parentWidget()->width(),
|
||||
st::callBottomShadowSize);
|
||||
const auto fill = shadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
fill,
|
||||
_bottomShadow,
|
||||
QRect(
|
||||
0,
|
||||
(factor
|
||||
* (fill.y() - shadowArea.translated(-widget->pos()).y())),
|
||||
factor,
|
||||
factor * fill.height()));
|
||||
}
|
||||
|
||||
Panel::Incoming::Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track,
|
||||
Ui::GL::Backend backend)
|
||||
: _surface(Ui::GL::CreateSurface(parent, chooseRenderer(backend)))
|
||||
, _track(track) {
|
||||
widget()->setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
widget()->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
not_null<QWidget*> Panel::Incoming::widget() const {
|
||||
return _surface->rpWidget();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidgetWrap*> Panel::Incoming::rp() const {
|
||||
return _surface.get();
|
||||
}
|
||||
|
||||
void Panel::Incoming::setControlsAlignment(style::align align) {
|
||||
if (_topControlsAlignment != align) {
|
||||
_topControlsAlignment = align;
|
||||
widget()->update();
|
||||
}
|
||||
}
|
||||
|
||||
Ui::GL::ChosenRenderer Panel::Incoming::chooseRenderer(
|
||||
Ui::GL::Backend backend) {
|
||||
_opengl = (backend == Ui::GL::Backend::OpenGL);
|
||||
return {
|
||||
.renderer = (_opengl
|
||||
? std::unique_ptr<Ui::GL::Renderer>(
|
||||
std::make_unique<RendererGL>(this))
|
||||
: std::make_unique<RendererSW>(this)),
|
||||
.backend = backend,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
45
Telegram/SourceFiles/calls/calls_video_incoming.h
Normal file
45
Telegram/SourceFiles/calls/calls_video_incoming.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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/calls_panel.h"
|
||||
|
||||
namespace Ui::GL {
|
||||
enum class Backend;
|
||||
struct ChosenRenderer;
|
||||
} // namespace Ui::GL
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Panel::Incoming final {
|
||||
public:
|
||||
Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track,
|
||||
Ui::GL::Backend backend);
|
||||
|
||||
[[nodiscard]] not_null<QWidget*> widget() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
|
||||
|
||||
void setControlsAlignment(style::align align);
|
||||
|
||||
private:
|
||||
class RendererGL;
|
||||
class RendererSW;
|
||||
|
||||
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
|
||||
Ui::GL::Backend backend);
|
||||
|
||||
const std::unique_ptr<Ui::RpWidgetWrap> _surface;
|
||||
const not_null<Webrtc::VideoTrack*> _track;
|
||||
style::align _topControlsAlignment = style::al_left;
|
||||
bool _opengl = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
250
Telegram/SourceFiles/calls/calls_window.cpp
Normal file
250
Telegram/SourceFiles/calls/calls_window.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
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/calls_window.h"
|
||||
|
||||
#include "base/power_save_blocker.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/layers/layer_manager.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
class Show final : public Ui::Show {
|
||||
public:
|
||||
explicit Show(not_null<Window*> window);
|
||||
~Show();
|
||||
|
||||
void showOrHideBoxOrLayer(
|
||||
std::variant<
|
||||
v::null_t,
|
||||
object_ptr<Ui::BoxContent>,
|
||||
std::unique_ptr<Ui::LayerWidget>> &&layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) const override;
|
||||
[[nodiscard]] not_null<QWidget*> toastParent() const override;
|
||||
[[nodiscard]] bool valid() const override;
|
||||
operator bool() const override;
|
||||
|
||||
private:
|
||||
const base::weak_ptr<Window> _window;
|
||||
|
||||
};
|
||||
|
||||
Show::Show(not_null<Window*> window)
|
||||
: _window(base::make_weak(window)) {
|
||||
}
|
||||
|
||||
Show::~Show() = default;
|
||||
|
||||
void Show::showOrHideBoxOrLayer(
|
||||
std::variant<
|
||||
v::null_t,
|
||||
object_ptr<Ui::BoxContent>,
|
||||
std::unique_ptr<Ui::LayerWidget>> &&layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) const {
|
||||
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
|
||||
using ObjectBox = object_ptr<Ui::BoxContent>;
|
||||
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
|
||||
if (const auto window = _window.get()) {
|
||||
window->showLayer(std::move(*layerWidget), options, animated);
|
||||
}
|
||||
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
|
||||
if (const auto window = _window.get()) {
|
||||
window->showBox(std::move(*box), options, animated);
|
||||
}
|
||||
} else if (const auto window = _window.get()) {
|
||||
window->hideLayer(animated);
|
||||
}
|
||||
}
|
||||
|
||||
not_null<QWidget*> Show::toastParent() const {
|
||||
const auto window = _window.get();
|
||||
Assert(window != nullptr);
|
||||
return window->widget();
|
||||
}
|
||||
|
||||
bool Show::valid() const {
|
||||
return !_window.empty();
|
||||
}
|
||||
|
||||
Show::operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Window::Window()
|
||||
: _layerBg(std::make_unique<Ui::LayerManager>(widget()))
|
||||
#ifndef Q_OS_MAC
|
||||
, _controls(Ui::Platform::SetupSeparateTitleControls(
|
||||
window(),
|
||||
st::callTitle,
|
||||
[=](bool maximized) { _maximizeRequests.fire_copy(maximized); },
|
||||
_controlsTop.value()))
|
||||
#endif // !Q_OS_MAC
|
||||
{
|
||||
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
|
||||
_layerBg->setHideByBackgroundClick(true);
|
||||
}
|
||||
|
||||
Window::~Window() = default;
|
||||
|
||||
Ui::GL::Backend Window::backend() const {
|
||||
return _window.backend();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWindow*> Window::window() const {
|
||||
return _window.window();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> Window::widget() const {
|
||||
return _window.widget();
|
||||
}
|
||||
|
||||
void Window::raiseControls() {
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->wrap.raise();
|
||||
#endif // !Q_OS_MAC
|
||||
}
|
||||
|
||||
void Window::setControlsStyle(const style::WindowTitle &st) {
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->controls.setStyle(st);
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void Window::setControlsShown(float64 shown) {
|
||||
#ifndef Q_OS_MAC
|
||||
_controlsTop = anim::interpolate(-_controls->wrap.height(), 0, shown);
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
int Window::controlsWrapTop() const {
|
||||
#ifndef Q_OS_MAC
|
||||
return _controls->wrap.y();
|
||||
#else // Q_OS_MAC
|
||||
return 0;
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
QRect Window::controlsGeometry() const {
|
||||
#ifndef Q_OS_MAC
|
||||
return _controls->controls.geometry();
|
||||
#else // Q_OS_MAC
|
||||
return QRect();
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
auto Window::controlsLayoutChanges() const
|
||||
-> rpl::producer<Ui::Platform::TitleLayout> {
|
||||
#ifndef Q_OS_MAC
|
||||
return _controls->controls.layout().changes();
|
||||
#else // Q_OS_MAC
|
||||
return rpl::never<Ui::Platform::TitleLayout>();
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
bool Window::controlsHasHitTest(QPoint widgetPoint) const {
|
||||
#ifndef Q_OS_MAC
|
||||
using Result = Ui::Platform::HitTestResult;
|
||||
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
|
||||
return (_controls->controls.hitTest(windowPoint) != Result::None);
|
||||
#else // Q_OS_MAC
|
||||
return false;
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
rpl::producer<bool> Window::maximizeRequests() const {
|
||||
return _maximizeRequests.events();
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
|
||||
const QString &text,
|
||||
crl::time duration) {
|
||||
return Show(this).showToast(text, duration);
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
|
||||
TextWithEntities &&text,
|
||||
crl::time duration) {
|
||||
return Show(this).showToast(std::move(text), duration);
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
|
||||
Ui::Toast::Config &&config) {
|
||||
return Show(this).showToast(std::move(config));
|
||||
}
|
||||
|
||||
void Window::raiseLayers() {
|
||||
_layerBg->raise();
|
||||
}
|
||||
|
||||
const Ui::LayerWidget *Window::topShownLayer() const {
|
||||
return _layerBg->topShownLayer();
|
||||
}
|
||||
|
||||
void Window::showBox(object_ptr<Ui::BoxContent> box) {
|
||||
showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
|
||||
}
|
||||
|
||||
void Window::showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) {
|
||||
_showingLayer.fire({});
|
||||
if (window()->width() < st::groupCallWidth
|
||||
|| window()->height() < st::groupCallWidth) {
|
||||
window()->resize(
|
||||
std::max(window()->width(), st::groupCallWidth),
|
||||
std::max(window()->height(), st::groupCallWidth));
|
||||
}
|
||||
_layerBg->showBox(std::move(box), options, animated);
|
||||
}
|
||||
|
||||
void Window::showLayer(
|
||||
std::unique_ptr<Ui::LayerWidget> layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) {
|
||||
_showingLayer.fire({});
|
||||
if (window()->width() < st::groupCallWidth
|
||||
|| window()->height() < st::groupCallWidth) {
|
||||
window()->resize(
|
||||
std::max(window()->width(), st::groupCallWidth),
|
||||
std::max(window()->height(), st::groupCallWidth));
|
||||
}
|
||||
_layerBg->showLayer(std::move(layer), options, animated);
|
||||
}
|
||||
|
||||
void Window::hideLayer(anim::type animated) {
|
||||
_layerBg->hideAll(animated);
|
||||
}
|
||||
|
||||
bool Window::isLayerShown() const {
|
||||
return _layerBg->topShownLayer() != nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::Show> Window::uiShow() {
|
||||
return std::make_shared<Show>(this);
|
||||
}
|
||||
|
||||
void Window::togglePowerSaveBlocker(bool enabled) {
|
||||
if (!enabled) {
|
||||
_powerSaveBlocker = nullptr;
|
||||
} else if (!_powerSaveBlocker) {
|
||||
_powerSaveBlocker = std::make_unique<base::PowerSaveBlocker>(
|
||||
base::PowerSaveBlockType::PreventDisplaySleep,
|
||||
u"Video call is active"_q,
|
||||
window()->windowHandle());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
112
Telegram/SourceFiles/calls/calls_window.h
Normal file
112
Telegram/SourceFiles/calls/calls_window.h
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
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 "ui/effects/animations.h"
|
||||
#include "ui/gl/gl_window.h"
|
||||
|
||||
namespace base {
|
||||
class PowerSaveBlocker;
|
||||
} // namespace base
|
||||
|
||||
namespace style {
|
||||
struct WindowTitle;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class RpWindow;
|
||||
class RpWidget;
|
||||
class LayerManager;
|
||||
class LayerWidget;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Platform {
|
||||
struct SeparateTitleControls;
|
||||
struct TitleLayout;
|
||||
} // namespace Ui::Platform
|
||||
|
||||
namespace Ui::Toast {
|
||||
struct Config;
|
||||
class Instance;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Window final : public base::has_weak_ptr {
|
||||
public:
|
||||
Window();
|
||||
~Window();
|
||||
|
||||
[[nodiscard]] Ui::GL::Backend backend() const;
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
|
||||
void raiseControls();
|
||||
void setControlsStyle(const style::WindowTitle &st);
|
||||
void setControlsShown(float64 shown);
|
||||
[[nodiscard]] int controlsWrapTop() const;
|
||||
[[nodiscard]] QRect controlsGeometry() const;
|
||||
[[nodiscard]] auto controlsLayoutChanges() const
|
||||
-> rpl::producer<Ui::Platform::TitleLayout>;
|
||||
[[nodiscard]] bool controlsHasHitTest(QPoint widgetPoint) const;
|
||||
[[nodiscard]] rpl::producer<bool> maximizeRequests() const;
|
||||
|
||||
void raiseLayers();
|
||||
[[nodiscard]] const Ui::LayerWidget *topShownLayer() const;
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
const QString &text,
|
||||
crl::time duration = 0);
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
TextWithEntities &&text,
|
||||
crl::time duration = 0);
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
Ui::Toast::Config &&config);
|
||||
|
||||
void showBox(object_ptr<Ui::BoxContent> box);
|
||||
void showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated = anim::type::normal);
|
||||
void showLayer(
|
||||
std::unique_ptr<Ui::LayerWidget> layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated = anim::type::normal);
|
||||
void hideLayer(anim::type animated = anim::type::normal);
|
||||
[[nodiscard]] bool isLayerShown() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> showingLayer() const {
|
||||
return _showingLayer.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
|
||||
|
||||
void togglePowerSaveBlocker(bool enabled);
|
||||
|
||||
private:
|
||||
Ui::GL::Window _window;
|
||||
const std::unique_ptr<Ui::LayerManager> _layerBg;
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
rpl::variable<int> _controlsTop = 0;
|
||||
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
|
||||
#endif // !Q_OS_MAC
|
||||
|
||||
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
|
||||
|
||||
rpl::event_stream<bool> _maximizeRequests;
|
||||
rpl::event_stream<> _showingLayer;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
501
Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp
Normal file
501
Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "apiwrap.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/boxes/choose_date_time.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
|
||||
|
||||
using Context = ChooseJoinAsProcess::Context;
|
||||
|
||||
class ListController : public PeerListController {
|
||||
public:
|
||||
ListController(
|
||||
std::vector<not_null<PeerData*>> list,
|
||||
not_null<PeerData*> selected);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> selected() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);
|
||||
|
||||
std::vector<not_null<PeerData*>> _list;
|
||||
not_null<PeerData*> _selected;
|
||||
|
||||
};
|
||||
|
||||
ListController::ListController(
|
||||
std::vector<not_null<PeerData*>> list,
|
||||
not_null<PeerData*> selected)
|
||||
: PeerListController()
|
||||
, _list(std::move(list))
|
||||
, _selected(selected) {
|
||||
}
|
||||
|
||||
Main::Session &ListController::session() const {
|
||||
return _selected->session();
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> ListController::createRow(
|
||||
not_null<PeerData*> peer) {
|
||||
auto result = std::make_unique<PeerListRow>(peer);
|
||||
if (peer->isSelf()) {
|
||||
result->setCustomStatus(
|
||||
tr::lng_group_call_join_as_personal(tr::now));
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
result->setCustomStatus(
|
||||
(channel->isMegagroup()
|
||||
? tr::lng_chat_status_members
|
||||
: tr::lng_chat_status_subscribers)(
|
||||
tr::now,
|
||||
lt_count,
|
||||
channel->membersCount()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ListController::prepare() {
|
||||
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
|
||||
for (const auto &peer : _list) {
|
||||
auto row = createRow(peer);
|
||||
const auto raw = row.get();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (peer == _selected) {
|
||||
delegate()->peerListSetRowChecked(raw, true);
|
||||
raw->finishCheckedAnimation();
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void ListController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto peer = row->peer();
|
||||
if (peer == _selected) {
|
||||
return;
|
||||
}
|
||||
const auto previous = delegate()->peerListFindRow(_selected->id.value);
|
||||
Assert(previous != nullptr);
|
||||
delegate()->peerListSetRowChecked(previous, false);
|
||||
delegate()->peerListSetRowChecked(row, true);
|
||||
_selected = peer;
|
||||
}
|
||||
|
||||
not_null<PeerData*> ListController::selected() const {
|
||||
return _selected;
|
||||
}
|
||||
|
||||
void ScheduleGroupCallBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const JoinInfo &info,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
const auto send = [=](TimeId date) {
|
||||
box->closeBox();
|
||||
|
||||
auto copy = info;
|
||||
copy.scheduleDate = date;
|
||||
done(std::move(copy));
|
||||
};
|
||||
const auto livestream = info.peer->isBroadcast();
|
||||
const auto duration = box->lifetime().make_state<
|
||||
rpl::variable<QString>>();
|
||||
auto description = (info.peer->isBroadcast()
|
||||
? tr::lng_group_call_schedule_notified_channel
|
||||
: tr::lng_group_call_schedule_notified_group)(
|
||||
lt_duration,
|
||||
duration->value());
|
||||
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto min = [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime::currentDateTime().addSecs(12));
|
||||
};
|
||||
const auto max = [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime(QDate::currentDate().addDays(8), QTime(0, 0))) - 1;
|
||||
};
|
||||
|
||||
// At least half an hour later, at zero minutes/seconds.
|
||||
const auto schedule = QDateTime(
|
||||
now.date(),
|
||||
QTime(now.time().hour(), 0)
|
||||
).addSecs(60 * 60 * (now.time().minute() < 30 ? 1 : 2));
|
||||
|
||||
auto descriptor = Ui::ChooseDateTimeBox(box, {
|
||||
.title = (livestream
|
||||
? tr::lng_group_call_schedule_title_channel()
|
||||
: tr::lng_group_call_schedule_title()),
|
||||
.submit = tr::lng_schedule_button(),
|
||||
.done = send,
|
||||
.min = min,
|
||||
.time = base::unixtime::serialize(schedule),
|
||||
.max = max,
|
||||
.description = std::move(description),
|
||||
});
|
||||
|
||||
using namespace rpl::mappers;
|
||||
*duration = rpl::combine(
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
base::timer_each(kLabelRefreshInterval)
|
||||
),
|
||||
std::move(descriptor.values) | rpl::filter(_1 != 0),
|
||||
_2
|
||||
) | rpl::map([](TimeId date) {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto duration = (date - now);
|
||||
if (duration >= 24 * 60 * 60) {
|
||||
return tr::lng_days(tr::now, lt_count, duration / (24 * 60 * 60));
|
||||
} else if (duration >= 60 * 60) {
|
||||
return tr::lng_hours(tr::now, lt_count, duration / (60 * 60));
|
||||
}
|
||||
return tr::lng_minutes(tr::now, lt_count, std::max(duration / 60, 1));
|
||||
});
|
||||
}
|
||||
|
||||
void ChooseJoinAsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Context context,
|
||||
JoinInfo info,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
box->setWidth(st::groupCallJoinAsWidth);
|
||||
const auto livestream = info.peer->isBroadcast();
|
||||
box->setTitle([&] {
|
||||
switch (context) {
|
||||
case Context::Create: return livestream
|
||||
? tr::lng_group_call_start_as_header_channel()
|
||||
: tr::lng_group_call_start_as_header();
|
||||
case Context::Join:
|
||||
case Context::JoinWithConfirm: return livestream
|
||||
? tr::lng_group_call_join_as_header_channel()
|
||||
: tr::lng_group_call_join_as_header();
|
||||
case Context::Switch: return tr::lng_group_call_display_as_header();
|
||||
}
|
||||
Unexpected("Context in ChooseJoinAsBox.");
|
||||
}());
|
||||
const auto &labelSt = (context == Context::Switch)
|
||||
? st::groupCallJoinAsLabel
|
||||
: st::confirmPhoneAboutLabel;
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_group_call_join_as_about(),
|
||||
labelSt));
|
||||
|
||||
auto &lifetime = box->lifetime();
|
||||
const auto delegate = lifetime.make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = lifetime.make_state<ListController>(
|
||||
info.possibleJoinAs,
|
||||
info.joinAs);
|
||||
if (context == Context::Switch) {
|
||||
controller->setStyleOverrides(
|
||||
&st::groupCallJoinAsList,
|
||||
&st::groupCallMultiSelect);
|
||||
} else {
|
||||
controller->setStyleOverrides(
|
||||
&st::defaultChooseSendAs.list,
|
||||
nullptr);
|
||||
}
|
||||
const auto content = box->addRow(
|
||||
object_ptr<PeerListContent>(box, controller),
|
||||
style::margins());
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
const auto &peer = info.peer;
|
||||
if ((context == Context::Create)
|
||||
&& (peer->isChannel() && peer->asChannel()->hasAdminRights())) {
|
||||
const auto makeLink = [](const QString &text) {
|
||||
return tr::link(text);
|
||||
};
|
||||
const auto label = box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_group_call_or_schedule(
|
||||
lt_link,
|
||||
(livestream
|
||||
? tr::lng_group_call_schedule_channel
|
||||
: tr::lng_group_call_schedule)(makeLink),
|
||||
tr::marked),
|
||||
labelSt));
|
||||
label->overrideLinkClickHandler([=] {
|
||||
auto withJoinAs = info;
|
||||
withJoinAs.joinAs = controller->selected();
|
||||
box->getDelegate()->show(
|
||||
Box(ScheduleGroupCallBox, withJoinAs, done));
|
||||
});
|
||||
}
|
||||
auto next = (context == Context::Switch)
|
||||
? tr::lng_settings_save()
|
||||
: tr::lng_continue();
|
||||
box->addButton(std::move(next), [=] {
|
||||
auto copy = info;
|
||||
copy.joinAs = controller->selected();
|
||||
done(std::move(copy));
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities CreateOrJoinConfirmation(
|
||||
not_null<PeerData*> peer,
|
||||
ChooseJoinAsProcess::Context context,
|
||||
bool joinAsAlreadyUsed) {
|
||||
const auto existing = peer->groupCall();
|
||||
if (!existing) {
|
||||
return { peer->isBroadcast()
|
||||
? tr::lng_group_call_create_sure_channel(tr::now)
|
||||
: tr::lng_group_call_create_sure(tr::now) };
|
||||
}
|
||||
const auto channel = peer->asChannel();
|
||||
const auto anonymouseAdmin = channel
|
||||
&& ((channel->isMegagroup() && channel->amAnonymous())
|
||||
|| (channel->isBroadcast()
|
||||
&& (channel->amCreator() || channel->hasAdminRights())));
|
||||
if (anonymouseAdmin && !joinAsAlreadyUsed) {
|
||||
return { tr::lng_group_call_join_sure_personal(tr::now) };
|
||||
} else if (context != ChooseJoinAsProcess::Context::JoinWithConfirm) {
|
||||
return {};
|
||||
}
|
||||
const auto name = !existing->title().isEmpty()
|
||||
? existing->title()
|
||||
: peer->name();
|
||||
return (peer->isBroadcast()
|
||||
? tr::lng_group_call_join_confirm_channel
|
||||
: tr::lng_group_call_join_confirm)(
|
||||
tr::now,
|
||||
lt_chat,
|
||||
tr::bold(name),
|
||||
tr::marked);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChooseJoinAsProcess::~ChooseJoinAsProcess() {
|
||||
if (_request) {
|
||||
_request->peer->session().api().request(_request->id).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::start(
|
||||
not_null<PeerData*> peer,
|
||||
Context context,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done,
|
||||
PeerData *changingJoinAsFrom) {
|
||||
Expects(done != nullptr);
|
||||
|
||||
const auto isScheduled = (context == Context::CreateScheduled);
|
||||
|
||||
const auto session = &peer->session();
|
||||
if (_request) {
|
||||
if (_request->peer == peer && !isScheduled) {
|
||||
_request->context = context;
|
||||
_request->show = std::move(show);
|
||||
_request->done = std::move(done);
|
||||
_request->changingJoinAsFrom = changingJoinAsFrom;
|
||||
return;
|
||||
}
|
||||
session->api().request(_request->id).cancel();
|
||||
_request = nullptr;
|
||||
}
|
||||
|
||||
const auto createRequest = [=, done = std::move(done)] {
|
||||
_request = std::make_unique<ChannelsListRequest>(ChannelsListRequest{
|
||||
.peer = peer,
|
||||
.show = show,
|
||||
.done = std::move(done),
|
||||
.context = context,
|
||||
.changingJoinAsFrom = changingJoinAsFrom });
|
||||
};
|
||||
|
||||
if (isScheduled) {
|
||||
auto box = Box(
|
||||
ScheduleGroupCallBox,
|
||||
JoinInfo{ .peer = peer, .joinAs = peer },
|
||||
[=, createRequest = std::move(createRequest)](JoinInfo info) {
|
||||
createRequest();
|
||||
finish(info);
|
||||
});
|
||||
show->showBox(std::move(box));
|
||||
return;
|
||||
}
|
||||
|
||||
createRequest();
|
||||
session->account().sessionChanges(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
requestList();
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::requestList() {
|
||||
const auto session = &_request->peer->session();
|
||||
_request->id = session->api().request(MTPphone_GetGroupCallJoinAs(
|
||||
_request->peer->input()
|
||||
)).done([=](const MTPphone_JoinAsPeers &result) {
|
||||
auto list = result.match([&](const MTPDphone_joinAsPeers &data) {
|
||||
session->data().processUsers(data.vusers());
|
||||
session->data().processChats(data.vchats());
|
||||
const auto &peers = data.vpeers().v;
|
||||
auto list = std::vector<not_null<PeerData*>>();
|
||||
list.reserve(peers.size());
|
||||
for (const auto &peer : peers) {
|
||||
const auto peerId = peerFromMTP(peer);
|
||||
if (const auto peer = session->data().peerLoaded(peerId)) {
|
||||
if (!ranges::contains(list, not_null{ peer })) {
|
||||
list.push_back(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
processList(std::move(list));
|
||||
}).fail([=] {
|
||||
finish({
|
||||
.peer = _request->peer,
|
||||
.joinAs = _request->peer->session().user(),
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::finish(JoinInfo info) {
|
||||
const auto done = std::move(_request->done);
|
||||
const auto box = _request->box;
|
||||
_request = nullptr;
|
||||
done(std::move(info));
|
||||
if (const auto strong = box.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseJoinAsProcess::processList(
|
||||
std::vector<not_null<PeerData*>> &&list) {
|
||||
const auto session = &_request->peer->session();
|
||||
const auto peer = _request->peer;
|
||||
const auto self = peer->session().user();
|
||||
auto info = JoinInfo{ .peer = peer, .joinAs = self };
|
||||
const auto selectedId = peer->groupCallDefaultJoinAs();
|
||||
if (list.empty()) {
|
||||
_request->show->showToast(Lang::Hard::ServerError());
|
||||
return;
|
||||
}
|
||||
info.joinAs = [&]() -> not_null<PeerData*> {
|
||||
const auto loaded = selectedId
|
||||
? session->data().peerLoaded(selectedId)
|
||||
: nullptr;
|
||||
const auto changingJoinAsFrom = _request->changingJoinAsFrom;
|
||||
return (changingJoinAsFrom
|
||||
&& ranges::contains(list, not_null{ changingJoinAsFrom }))
|
||||
? not_null(changingJoinAsFrom)
|
||||
: (loaded && ranges::contains(list, not_null{ loaded }))
|
||||
? not_null(loaded)
|
||||
: ranges::contains(list, self)
|
||||
? self
|
||||
: list.front();
|
||||
}();
|
||||
info.possibleJoinAs = std::move(list);
|
||||
|
||||
const auto onlyByMe = (info.possibleJoinAs.size() == 1)
|
||||
&& (info.possibleJoinAs.front() == self);
|
||||
|
||||
// We already joined this voice chat, just rejoin with the same.
|
||||
const auto byAlreadyUsed = selectedId
|
||||
&& (info.joinAs->id == selectedId)
|
||||
&& (peer->groupCall() != nullptr);
|
||||
|
||||
if (!_request->changingJoinAsFrom && (onlyByMe || byAlreadyUsed)) {
|
||||
auto confirmation = CreateOrJoinConfirmation(
|
||||
peer,
|
||||
_request->context,
|
||||
byAlreadyUsed);
|
||||
if (confirmation.text.isEmpty()) {
|
||||
finish(info);
|
||||
return;
|
||||
}
|
||||
const auto livestream = peer->isBroadcast();
|
||||
const auto creating = !peer->groupCall();
|
||||
if (creating) {
|
||||
confirmation
|
||||
.append("\n\n")
|
||||
.append(tr::lng_group_call_or_schedule(
|
||||
tr::now,
|
||||
lt_link,
|
||||
tr::link((livestream
|
||||
? tr::lng_group_call_schedule_channel
|
||||
: tr::lng_group_call_schedule)(tr::now)),
|
||||
tr::marked));
|
||||
}
|
||||
const auto guard = base::make_weak(&_request->guard);
|
||||
const auto safeFinish = crl::guard(guard, [=] { finish(info); });
|
||||
const auto filter = [=](const auto &...) {
|
||||
if (guard) {
|
||||
_request->show->showBox(Box(
|
||||
ScheduleGroupCallBox,
|
||||
info,
|
||||
crl::guard(guard, [=](auto info) { finish(info); })));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto box = Ui::MakeConfirmBox({
|
||||
.text = confirmation,
|
||||
.confirmed = crl::guard(guard, [=] { finish(info); }),
|
||||
.confirmText = (creating
|
||||
? tr::lng_create_group_create()
|
||||
: tr::lng_group_call_join()),
|
||||
.labelFilter = filter,
|
||||
});
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
_request->box = box.data();
|
||||
_request->show->showBox(std::move(box));
|
||||
return;
|
||||
}
|
||||
auto box = Box(
|
||||
ChooseJoinAsBox,
|
||||
_request->context,
|
||||
std::move(info),
|
||||
crl::guard(&_request->guard, [=](auto info) { finish(info); }));
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
_request->box = box.data();
|
||||
_request->show->showBox(std::move(box));
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
64
Telegram/SourceFiles/calls/group/calls_choose_join_as.h
Normal file
64
Telegram/SourceFiles/calls/group/calls_choose_join_as.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct JoinInfo;
|
||||
|
||||
class ChooseJoinAsProcess final {
|
||||
public:
|
||||
ChooseJoinAsProcess() = default;
|
||||
~ChooseJoinAsProcess();
|
||||
|
||||
enum class Context {
|
||||
Create,
|
||||
CreateScheduled,
|
||||
Join,
|
||||
JoinWithConfirm,
|
||||
Switch,
|
||||
};
|
||||
|
||||
void start(
|
||||
not_null<PeerData*> peer,
|
||||
Context context,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done,
|
||||
PeerData *changingJoinAsFrom = nullptr);
|
||||
|
||||
private:
|
||||
void requestList();
|
||||
void processList(std::vector<not_null<PeerData*>> &&list);
|
||||
void finish(JoinInfo info);
|
||||
|
||||
struct ChannelsListRequest {
|
||||
not_null<PeerData*> peer;
|
||||
std::shared_ptr<Ui::Show> show;
|
||||
Fn<void(JoinInfo)> done;
|
||||
base::has_weak_ptr guard;
|
||||
base::weak_qptr<Ui::BoxContent> box;
|
||||
rpl::lifetime lifetime;
|
||||
Context context = Context();
|
||||
mtpRequestId id = 0;
|
||||
PeerData *changingJoinAsFrom = nullptr;
|
||||
};
|
||||
std::unique_ptr<ChannelsListRequest> _request;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
92
Telegram/SourceFiles/calls/group/calls_cover_item.cpp
Normal file
92
Telegram/SourceFiles/calls/group/calls_cover_item.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_cover_item.h"
|
||||
|
||||
#include "boxes/peers/prepare_short_info_box.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
CoverItem::CoverItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &stMenu,
|
||||
const style::ShortInfoCover &st,
|
||||
rpl::producer<QString> name,
|
||||
rpl::producer<QString> status,
|
||||
PreparedShortInfoUserpic userpic)
|
||||
: Ui::Menu::ItemBase(parent, stMenu)
|
||||
, _cover(
|
||||
this,
|
||||
st,
|
||||
std::move(name),
|
||||
std::move(status),
|
||||
std::move(userpic.value),
|
||||
[] { return false; })
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_cover.widget());
|
||||
|
||||
_cover.widget()->move(0, 0);
|
||||
_cover.moveRequests(
|
||||
) | rpl::on_next(userpic.move, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> CoverItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool CoverItem::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int CoverItem::contentHeight() const {
|
||||
return _st.size + st::groupCallMenu.separator.padding.bottom();
|
||||
}
|
||||
|
||||
AboutItem::AboutItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
TextWithEntities &&about)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _st(st)
|
||||
, _text(base::make_unique_q<Ui::FlatLabel>(
|
||||
this,
|
||||
rpl::single(std::move(about)),
|
||||
st::groupCallMenuAbout))
|
||||
, _dummyAction(new QAction(parent)) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_text.get());
|
||||
|
||||
_text->setSelectable(true);
|
||||
_text->resizeToWidth(st::groupCallMenuAbout.minWidth);
|
||||
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
|
||||
}
|
||||
|
||||
not_null<QAction*> AboutItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool AboutItem::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int AboutItem::contentHeight() const {
|
||||
return _st.itemPadding.top()
|
||||
+ _text->height()
|
||||
+ _st.itemPadding.bottom();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
69
Telegram/SourceFiles/calls/group/calls_cover_item.h
Normal file
69
Telegram/SourceFiles/calls/group/calls_cover_item.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "boxes/peers/peer_short_info_box.h"
|
||||
|
||||
struct PreparedShortInfoUserpic;
|
||||
|
||||
namespace style {
|
||||
struct ShortInfoCover;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
class CoverItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
CoverItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &stMenu,
|
||||
const style::ShortInfoCover &st,
|
||||
rpl::producer<QString> name,
|
||||
rpl::producer<QString> status,
|
||||
PreparedShortInfoUserpic userpic);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
|
||||
const PeerShortInfoCover _cover;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::ShortInfoCover &_st;
|
||||
|
||||
};
|
||||
|
||||
class AboutItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
AboutItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
TextWithEntities &&about);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
|
||||
const style::Menu &_st;
|
||||
const base::unique_qptr<Ui::FlatLabel> _text;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
4357
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
4357
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
File diff suppressed because it is too large
Load Diff
815
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
815
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/bytes.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "mtproto/mtproto_auth_key.h"
|
||||
#include "webrtc/webrtc_device_common.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace tgcalls {
|
||||
class GroupInstanceCustomImpl;
|
||||
struct GroupLevelsUpdate;
|
||||
struct GroupNetworkState;
|
||||
struct GroupParticipantDescription;
|
||||
class VideoCaptureInterface;
|
||||
enum class VideoCodecName;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
class GlobalShortcutManager;
|
||||
class GlobalShortcutValue;
|
||||
} // namespace base
|
||||
|
||||
namespace Webrtc {
|
||||
class MediaDevices;
|
||||
class VideoTrack;
|
||||
enum class VideoState;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Data {
|
||||
struct LastSpokeTimes;
|
||||
struct GroupCallParticipant;
|
||||
class GroupCall;
|
||||
enum class GroupCallOrigin : uchar;
|
||||
} // namespace Data
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
class EncryptDecrypt;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace Calls::Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
struct JoinInfo;
|
||||
struct ConferenceInfo;
|
||||
struct RejoinEvent;
|
||||
struct RtmpInfo;
|
||||
enum class VideoQuality;
|
||||
enum class Error;
|
||||
class Messages;
|
||||
} // namespace Calls::Group
|
||||
|
||||
namespace Calls {
|
||||
|
||||
struct InviteRequest;
|
||||
struct InviteResult;
|
||||
struct StartConferenceInfo;
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
RaisedHand,
|
||||
};
|
||||
|
||||
[[nodiscard]] inline auto MapPushToTalkToActive() {
|
||||
return rpl::map([=](MuteState state) {
|
||||
return (state == MuteState::PushToTalk) ? MuteState::Active : state;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGroupCallAdmin(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PeerData*> participantPeer);
|
||||
|
||||
struct LevelUpdate {
|
||||
uint32 ssrc = 0;
|
||||
float value = 0.;
|
||||
bool voice = false;
|
||||
bool me = false;
|
||||
};
|
||||
|
||||
enum class VideoEndpointType {
|
||||
Camera,
|
||||
Screen,
|
||||
};
|
||||
|
||||
struct VideoEndpoint {
|
||||
VideoEndpoint() = default;
|
||||
VideoEndpoint(
|
||||
VideoEndpointType type,
|
||||
not_null<PeerData*> peer,
|
||||
std::string id)
|
||||
: type(type)
|
||||
, peer(peer)
|
||||
, id(std::move(id)) {
|
||||
}
|
||||
|
||||
VideoEndpointType type = VideoEndpointType::Camera;
|
||||
PeerData *peer = nullptr;
|
||||
std::string id;
|
||||
|
||||
[[nodiscard]] bool rtmp() const noexcept;
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
Expects(id.empty() || peer != nullptr);
|
||||
|
||||
return id.empty();
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const noexcept {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
inline bool operator==(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.id == b.id);
|
||||
}
|
||||
|
||||
inline bool operator!=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator<(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.peer < b.peer)
|
||||
|| (a.peer == b.peer && a.id < b.id);
|
||||
}
|
||||
|
||||
inline bool operator>(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (b < a);
|
||||
}
|
||||
|
||||
inline bool operator<=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
inline bool operator>=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct VideoStateToggle {
|
||||
VideoEndpoint endpoint;
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
struct VideoQualityRequest {
|
||||
VideoEndpoint endpoint;
|
||||
Group::VideoQuality quality = Group::VideoQuality();
|
||||
};
|
||||
|
||||
struct ParticipantVideoParams;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ParticipantVideoParams> ParseVideoParams(
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &camera,
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &screen,
|
||||
const std::shared_ptr<ParticipantVideoParams> &existing);
|
||||
|
||||
[[nodiscard]] const std::string &GetCameraEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] const std::string &GetScreenEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsCameraPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsScreenPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] uint32 GetAdditionalAudioSsrc(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
|
||||
class GroupCall final
|
||||
: public base::has_weak_ptr
|
||||
, private Webrtc::CaptureMuteTracker {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
virtual void groupCallFinished(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) = 0;
|
||||
|
||||
enum class GroupCallSound {
|
||||
Started,
|
||||
Connecting,
|
||||
AllowedToSpeak,
|
||||
Ended,
|
||||
RecordingStarted,
|
||||
};
|
||||
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
|
||||
virtual auto groupCallGetVideoCapture(const QString &deviceId)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
[[nodiscard]] virtual FnMut<void()> groupCallAddAsyncWaiter() = 0;
|
||||
};
|
||||
|
||||
using GlobalShortcutManager = base::GlobalShortcutManager;
|
||||
|
||||
struct VideoTrack;
|
||||
|
||||
[[nodiscard]] static not_null<PeerData*> TrackPeer(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
[[nodiscard]] static not_null<Webrtc::VideoTrack*> TrackPointer(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
[[nodiscard]] static rpl::producer<QSize> TrackSizeValue(
|
||||
const std::unique_ptr<VideoTrack> &track);
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
GroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] CallId id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> joinAs() const {
|
||||
return _joinAs.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> joinAsValue() const {
|
||||
return _joinAs.value();
|
||||
}
|
||||
[[nodiscard]] not_null<Group::Messages*> messages() const {
|
||||
return _messages.get();
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> messagesFrom() const;
|
||||
[[nodiscard]] bool showChooseJoinAs() const;
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate;
|
||||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] bool conference() const;
|
||||
[[nodiscard]] bool videoStream() const;
|
||||
[[nodiscard]] Data::GroupCallOrigin origin() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool emptyRtmp() const;
|
||||
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
|
||||
[[nodiscard]] int rtmpVolume() const;
|
||||
|
||||
[[nodiscard]] Group::RtmpInfo rtmpInfo() const;
|
||||
|
||||
void setRtmpInfo(const Group::RtmpInfo &value);
|
||||
|
||||
[[nodiscard]] Data::GroupCall *lookupReal() const;
|
||||
[[nodiscard]] std::shared_ptr<Data::GroupCall> sharedCall() const;
|
||||
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
|
||||
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
|
||||
|
||||
void applyInputCall(const MTPInputGroupCall &inputCall);
|
||||
void startConference();
|
||||
void start(TimeId scheduleDate, bool rtmp);
|
||||
void hangup();
|
||||
void discard();
|
||||
void rejoinAs(Group::JoinInfo info);
|
||||
void rejoinWithHash(const QString &hash);
|
||||
void initialJoin();
|
||||
void initialJoinRequested();
|
||||
void handleUpdate(const MTPUpdate &update);
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void handlePossibleCreateOrJoinResponse(
|
||||
const MTPDupdateGroupCallConnection &data);
|
||||
void handleIncomingMessage(const MTPDupdateGroupCallMessage &data);
|
||||
void handleIncomingMessage(
|
||||
const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
void handleDeleteMessages(const MTPDupdateDeleteGroupCallMessages &data);
|
||||
void handleMessageSent(const MTPDupdateMessageID &data);
|
||||
void changeTitle(const QString &title);
|
||||
void toggleRecording(
|
||||
bool enabled,
|
||||
const QString &title,
|
||||
bool video,
|
||||
bool videoPortrait);
|
||||
void playSoundRecordingStarted() const;
|
||||
[[nodiscard]] bool recordingStoppedByMe() const {
|
||||
return _recordingStoppedByMe;
|
||||
}
|
||||
void startScheduledNow();
|
||||
void toggleScheduleStartSubscribed(bool subscribed);
|
||||
void setNoiseSuppression(bool enabled);
|
||||
void removeConferenceParticipants(
|
||||
const base::flat_set<UserId> userIds,
|
||||
bool removingStale = false);
|
||||
|
||||
bool emitShareScreenError();
|
||||
bool emitShareCameraError();
|
||||
|
||||
void joinDone(
|
||||
int64 serverTimeMs,
|
||||
const MTPUpdates &result,
|
||||
MuteState wasMuteState,
|
||||
bool wasVideoStopped,
|
||||
bool justCreated = false);
|
||||
void joinFail(const QString &error);
|
||||
|
||||
[[nodiscard]] rpl::producer<Group::Error> errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
void addVideoOutput(
|
||||
const std::string &endpoint,
|
||||
not_null<Webrtc::VideoTrack*> track);
|
||||
|
||||
void setMuted(MuteState mute);
|
||||
void setMutedAndUpdate(MuteState mute);
|
||||
[[nodiscard]] MuteState muted() const {
|
||||
return _muted.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<MuteState> mutedValue() const {
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState>;
|
||||
|
||||
enum State {
|
||||
Creating,
|
||||
Waiting,
|
||||
Joining,
|
||||
Connecting,
|
||||
Joined,
|
||||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
Ended,
|
||||
};
|
||||
[[nodiscard]] State state() const {
|
||||
return _state.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
|
||||
enum class InstanceState {
|
||||
Disconnected,
|
||||
TransitionToRtc,
|
||||
Connected,
|
||||
};
|
||||
[[nodiscard]] InstanceState instanceState() const {
|
||||
return _instanceState.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {
|
||||
return _instanceState.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
|
||||
return _levelUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamActiveUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamActiveUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamShownUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamShownUpdates.events();
|
||||
}
|
||||
void requestVideoQuality(
|
||||
const VideoEndpoint &endpoint,
|
||||
Group::VideoQuality quality);
|
||||
|
||||
[[nodiscard]] bool videoEndpointPinned() const {
|
||||
return _videoEndpointPinned.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {
|
||||
return _videoEndpointPinned.value();
|
||||
}
|
||||
void pinVideoEndpoint(VideoEndpoint endpoint);
|
||||
|
||||
void showVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {
|
||||
return _videoEndpointLarge.current();
|
||||
}
|
||||
[[nodiscard]] auto videoEndpointLargeValue() const
|
||||
-> rpl::producer<VideoEndpoint> {
|
||||
return _videoEndpointLarge.value();
|
||||
}
|
||||
[[nodiscard]] auto activeVideoTracks() const
|
||||
-> const base::flat_map<VideoEndpoint, std::unique_ptr<VideoTrack>> & {
|
||||
return _activeVideoTracks;
|
||||
}
|
||||
[[nodiscard]] auto shownVideoTracks() const
|
||||
-> const base::flat_set<VideoEndpoint> & {
|
||||
return _shownVideoTracks;
|
||||
}
|
||||
[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {
|
||||
return _rejoinEvents.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {
|
||||
return _allowedToSpeakNotifications.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> titleChanged() const {
|
||||
return _titleChanged.events();
|
||||
}
|
||||
static constexpr auto kSpeakLevelThreshold = 0.2;
|
||||
|
||||
[[nodiscard]] bool mutedByAdmin() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] rpl::producer<bool> canManageValue() const;
|
||||
[[nodiscard]] bool videoIsWorking() const {
|
||||
return _videoIsWorking.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {
|
||||
return _videoIsWorking.value();
|
||||
}
|
||||
[[nodiscard]] bool messagesEnabled() const {
|
||||
return _messagesEnabled.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {
|
||||
return _messagesEnabled.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;
|
||||
[[nodiscard]] bool isScreenPaused() const;
|
||||
[[nodiscard]] const std::string &screenSharingEndpoint() const;
|
||||
[[nodiscard]] bool isSharingCamera() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingCameraValue() const;
|
||||
[[nodiscard]] bool isCameraPaused() const;
|
||||
[[nodiscard]] const std::string &cameraSharingEndpoint() const;
|
||||
[[nodiscard]] QString screenSharingDeviceId() const;
|
||||
[[nodiscard]] bool screenSharingWithAudio() const;
|
||||
void toggleVideo(bool active);
|
||||
void toggleScreenSharing(
|
||||
std::optional<QString> uniqueId,
|
||||
bool withAudio = false);
|
||||
[[nodiscard]] bool hasVideoWithFrames() const;
|
||||
[[nodiscard]] rpl::producer<bool> hasVideoWithFramesValue() const;
|
||||
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
|
||||
void inviteUsers(
|
||||
const std::vector<InviteRequest> &requests,
|
||||
Fn<void(InviteResult)> done);
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
void setNotRequireARGB32();
|
||||
|
||||
[[nodiscard]] std::function<std::vector<uint8_t>(
|
||||
std::vector<uint8_t> const &,
|
||||
int64_t, bool,
|
||||
int32_t)> e2eEncryptDecrypt() const;
|
||||
void sendMessage(TextWithTags message);
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
class LoadPartTask;
|
||||
class MediaChannelDescriptionsTask;
|
||||
class RequestCurrentTimeTask;
|
||||
using GlobalShortcutValue = base::GlobalShortcutValue;
|
||||
using Error = Group::Error;
|
||||
struct SinkPointer;
|
||||
|
||||
static constexpr uint32 kDisabledSsrc = uint32(-1);
|
||||
static constexpr int kSubChainsCount = 2;
|
||||
|
||||
struct LoadingPart {
|
||||
std::shared_ptr<LoadPartTask> task;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
enum class FinishType {
|
||||
None,
|
||||
Ended,
|
||||
Failed,
|
||||
};
|
||||
enum class InstanceMode {
|
||||
None,
|
||||
Rtc,
|
||||
Stream,
|
||||
};
|
||||
enum class SendUpdateType {
|
||||
Mute = 0x01,
|
||||
RaiseHand = 0x02,
|
||||
CameraStopped = 0x04,
|
||||
CameraPaused = 0x08,
|
||||
ScreenPaused = 0x10,
|
||||
};
|
||||
enum class JoinAction {
|
||||
None,
|
||||
Joining,
|
||||
Leaving,
|
||||
};
|
||||
struct JoinPayload {
|
||||
uint32 ssrc = 0;
|
||||
QByteArray json;
|
||||
};
|
||||
struct JoinState {
|
||||
uint32 ssrc = 0;
|
||||
JoinAction action = JoinAction::None;
|
||||
JoinPayload payload;
|
||||
bool nextActionPending = false;
|
||||
|
||||
void finish(uint32 updatedSsrc = 0) {
|
||||
action = JoinAction::None;
|
||||
ssrc = updatedSsrc;
|
||||
}
|
||||
};
|
||||
struct SubChainPending {
|
||||
QVector<MTPbytes> blocks;
|
||||
int next = 0;
|
||||
};
|
||||
struct SubChainState {
|
||||
std::vector<SubChainPending> pending;
|
||||
mtpRequestId requestId = 0;
|
||||
bool inShortPoll = false;
|
||||
};
|
||||
|
||||
friend inline constexpr bool is_flag_type(SendUpdateType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo join,
|
||||
StartConferenceInfo conference,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
|
||||
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
|
||||
void broadcastPartCancel(not_null<LoadPartTask*> task);
|
||||
void mediaChannelDescriptionsStart(
|
||||
std::shared_ptr<MediaChannelDescriptionsTask> task);
|
||||
void mediaChannelDescriptionsCancel(
|
||||
not_null<MediaChannelDescriptionsTask*> task);
|
||||
void requestCurrentTimeStart(
|
||||
std::shared_ptr<RequestCurrentTimeTask> task);
|
||||
void requestCurrentTimeCancel(
|
||||
not_null<RequestCurrentTimeTask*> task);
|
||||
[[nodiscard]] int64 approximateServerTimeInMs() const;
|
||||
|
||||
[[nodiscard]] bool mediaChannelDescriptionsFill(
|
||||
not_null<MediaChannelDescriptionsTask*> task,
|
||||
Fn<bool(uint32)> resolved = nullptr);
|
||||
void checkMediaChannelDescriptions(Fn<bool(uint32)> resolved = nullptr);
|
||||
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);
|
||||
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
|
||||
void handleUpdate(const MTPDupdateGroupCall &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
|
||||
void applySubChainUpdate(
|
||||
int subchain,
|
||||
const QVector<MTPbytes> &blocks,
|
||||
int next);
|
||||
[[nodiscard]] auto lookupVideoCodecPreferences() const
|
||||
-> std::vector<tgcalls::VideoCodecName>;
|
||||
bool tryCreateController();
|
||||
void destroyController();
|
||||
bool tryCreateScreencast();
|
||||
void destroyScreencast();
|
||||
|
||||
void emitShareCameraError(Error error);
|
||||
void emitShareScreenError(Error error);
|
||||
|
||||
void setState(State state);
|
||||
void finish(FinishType type);
|
||||
void maybeSendMutedUpdate(MuteState previous);
|
||||
void sendSelfUpdate(SendUpdateType type);
|
||||
void updateInstanceMuteState();
|
||||
void updateInstanceVolumes();
|
||||
void updateInstanceVolume(
|
||||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant &now);
|
||||
void applyMeInCallLocally();
|
||||
void startRejoin();
|
||||
void rejoin();
|
||||
void leave();
|
||||
void rejoin(not_null<PeerData*> as);
|
||||
void setJoinAs(not_null<PeerData*> as);
|
||||
void saveDefaultJoinAs(not_null<PeerData*> as);
|
||||
void subscribeToReal(not_null<Data::GroupCall*> real);
|
||||
void setScheduledDate(TimeId date);
|
||||
void setMessagesEnabled(bool enabled);
|
||||
void rejoinPresentation();
|
||||
void leavePresentation();
|
||||
void checkNextJoinAction();
|
||||
void sendJoinRequest();
|
||||
void refreshLastBlockAndJoin();
|
||||
void requestSubchainBlocks(int subchain, int height);
|
||||
void sendOutboundBlock(QByteArray block);
|
||||
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setInstanceMode(InstanceMode mode);
|
||||
void setScreenInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setScreenInstanceMode(InstanceMode mode);
|
||||
void checkLastSpoke();
|
||||
void pushToTalkCancel();
|
||||
|
||||
void checkGlobalShortcutAvailability();
|
||||
void checkJoined();
|
||||
void checkFirstTimeJoined();
|
||||
void notifyAboutAllowedToSpeak();
|
||||
|
||||
void playConnectingSound();
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
void updateRequestedVideoChannels();
|
||||
void updateRequestedVideoChannelsDelayed();
|
||||
void fillActiveVideoEndpoints();
|
||||
|
||||
void editParticipant(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyParticipantLocally(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyQueuedSelfUpdates();
|
||||
void sendPendingSelfUpdates();
|
||||
void applySelfUpdate(const MTPDgroupCallParticipant &data);
|
||||
void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);
|
||||
|
||||
void captureMuteChanged(bool mute) override;
|
||||
rpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;
|
||||
|
||||
void setupMediaDevices();
|
||||
void setupOutgoingVideo();
|
||||
void initConferenceE2E();
|
||||
void setupConferenceCall();
|
||||
void trackParticipantsWithAccess();
|
||||
void setScreenEndpoint(std::string endpoint);
|
||||
void setCameraEndpoint(std::string endpoint);
|
||||
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
|
||||
void setVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
|
||||
void markEndpointActive(
|
||||
VideoEndpoint endpoint,
|
||||
bool active,
|
||||
bool paused);
|
||||
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
|
||||
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
|
||||
|
||||
void processConferenceStart(StartConferenceInfo conference);
|
||||
void inviteToConference(
|
||||
InviteRequest request,
|
||||
Fn<not_null<InviteResult*>()> resultAddress,
|
||||
Fn<void()> finishRequest);
|
||||
|
||||
[[nodiscard]] float64 singleSourceVolumeValue() const;
|
||||
[[nodiscard]] int activeVideoSendersCount() const;
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
std::shared_ptr<Data::GroupCall> _sharedCall;
|
||||
std::unique_ptr<TdE2E::Call> _e2e;
|
||||
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
|
||||
rpl::variable<QByteArray> _emojiHash;
|
||||
QByteArray _pendingOutboundBlock;
|
||||
std::shared_ptr<StartConferenceInfo> _startConferenceInfo;
|
||||
|
||||
not_null<PeerData*> _peer; // Can change in legacy group migration.
|
||||
rpl::event_stream<PeerData*> _peerStream;
|
||||
not_null<History*> _history; // Can change in legacy group migration.
|
||||
MTP::Sender _api;
|
||||
rpl::event_stream<not_null<Data::GroupCall*>> _realChanges;
|
||||
rpl::variable<State> _state = State::Creating;
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
rpl::event_stream<Error> _errors;
|
||||
std::vector<Fn<void()>> _rejoinedCallbacks;
|
||||
const std::unique_ptr<Group::Messages> _messages;
|
||||
bool _recordingStoppedByMe = false;
|
||||
bool _requestedVideoChannelsUpdateScheduled = false;
|
||||
|
||||
MTP::DcId _broadcastDcId = 0;
|
||||
base::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;
|
||||
base::flat_set<
|
||||
std::shared_ptr<MediaChannelDescriptionsTask>,
|
||||
base::pointer_comparator<
|
||||
MediaChannelDescriptionsTask>> _mediaChannelDescriptionses;
|
||||
base::flat_set<
|
||||
std::shared_ptr<RequestCurrentTimeTask>,
|
||||
base::pointer_comparator<
|
||||
RequestCurrentTimeTask>> _requestCurrentTimes;
|
||||
mtpRequestId _requestCurrentTimeRequestId = 0;
|
||||
|
||||
rpl::variable<not_null<PeerData*>> _joinAs;
|
||||
std::vector<not_null<PeerData*>> _possibleJoinAs;
|
||||
QString _joinHash;
|
||||
QString _conferenceLinkSlug;
|
||||
MsgId _conferenceJoinMessageId;
|
||||
int64 _serverTimeMs = 0;
|
||||
crl::time _serverTimeMsGotAt = 0;
|
||||
|
||||
QString _rtmpUrl;
|
||||
QString _rtmpKey;
|
||||
|
||||
rpl::variable<MuteState> _muted = MuteState::Muted;
|
||||
rpl::variable<bool> _canManage = false;
|
||||
rpl::variable<bool> _videoIsWorking = false;
|
||||
rpl::variable<bool> _emptyRtmp = false;
|
||||
rpl::variable<bool> _messagesEnabled = false;
|
||||
bool _initialMuteStateSent = false;
|
||||
bool _acceptFields = false;
|
||||
|
||||
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
|
||||
std::vector<MTPGroupCallParticipant> _queuedSelfUpdates;
|
||||
|
||||
CallId _id = 0;
|
||||
CallId _accessHash = 0;
|
||||
JoinState _joinState;
|
||||
JoinState _screenJoinState;
|
||||
std::string _cameraEndpoint;
|
||||
std::string _screenEndpoint;
|
||||
TimeId _scheduleDate = 0;
|
||||
base::flat_set<uint32> _mySsrcs;
|
||||
mtpRequestId _createRequestId = 0;
|
||||
mtpRequestId _selfUpdateRequestId = 0;
|
||||
|
||||
rpl::variable<InstanceState> _instanceState
|
||||
= InstanceState::Disconnected;
|
||||
bool _instanceTransitioning = false;
|
||||
InstanceMode _instanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;
|
||||
base::has_weak_ptr _instanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _cameraCapture;
|
||||
rpl::variable<Webrtc::VideoState> _cameraState;
|
||||
rpl::variable<bool> _isSharingCamera = false;
|
||||
base::flat_map<std::string, SinkPointer> _pendingVideoOutputs;
|
||||
|
||||
rpl::variable<InstanceState> _screenInstanceState
|
||||
= InstanceState::Disconnected;
|
||||
InstanceMode _screenInstanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _screenInstance;
|
||||
base::has_weak_ptr _screenInstanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _screenCapture;
|
||||
rpl::variable<Webrtc::VideoState> _screenState;
|
||||
rpl::variable<bool> _isSharingScreen = false;
|
||||
QString _screenDeviceId;
|
||||
bool _screenWithAudio = false;
|
||||
|
||||
base::flags<SendUpdateType> _pendingSelfUpdates;
|
||||
bool _requireARGB32 = true;
|
||||
|
||||
rpl::event_stream<LevelUpdate> _levelUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamActiveUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamPausedUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamShownUpdates;
|
||||
base::flat_map<
|
||||
VideoEndpoint,
|
||||
std::unique_ptr<VideoTrack>> _activeVideoTracks;
|
||||
base::flat_set<VideoEndpoint> _shownVideoTracks;
|
||||
rpl::variable<VideoEndpoint> _videoEndpointLarge;
|
||||
rpl::variable<bool> _videoEndpointPinned = false;
|
||||
crl::time _videoLargeTillTime = 0;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
rpl::event_stream<Group::RejoinEvent> _rejoinEvents;
|
||||
rpl::event_stream<> _allowedToSpeakNotifications;
|
||||
rpl::event_stream<> _titleChanged;
|
||||
base::Timer _lastSpokeCheckTimer;
|
||||
base::Timer _checkJoinedTimer;
|
||||
|
||||
crl::time _lastSendProgressUpdate = 0;
|
||||
|
||||
Fn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;
|
||||
Webrtc::DeviceResolver _playbackDeviceId;
|
||||
Webrtc::DeviceResolver _captureDeviceId;
|
||||
Webrtc::DeviceResolver _cameraDeviceId;
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
|
||||
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
|
||||
base::Timer _pushToTalkCancelTimer;
|
||||
base::Timer _connectingSoundTimer;
|
||||
bool _hadJoinedState = false;
|
||||
bool _listenersHidden = false;
|
||||
bool _rtmp = false;
|
||||
bool _reloadedStaleCall = false;
|
||||
int _singleSourceVolume = 0;
|
||||
|
||||
SubChainState _subchains[kSubChainsCount];
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
|
||||
const InviteResult &result);
|
||||
|
||||
} // namespace Calls
|
||||
506
Telegram/SourceFiles/calls/group/calls_group_common.cpp
Normal file
506
Telegram/SourceFiles/calls/group/calls_group_common.cpp
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/random.h"
|
||||
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
|
||||
#include "boxes/share_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "core/local_url_handlers.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "info/bot/starref/info_bot_starref_common.h"
|
||||
#include "tde2e/tde2e_api.h"
|
||||
#include "tde2e/tde2e_integration.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
|
||||
#ifdef Q_OS_MAC
|
||||
if (!Platform::IsMac10_15OrGreater()) {
|
||||
return { nullptr };
|
||||
}
|
||||
return Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
rpl::combine(
|
||||
tr::lng_group_call_mac_screencast_access(),
|
||||
tr::lng_group_call_mac_recording()
|
||||
) | rpl::map([](QString a, QString b) {
|
||||
auto result = tr::rich(a);
|
||||
result.append("\n\n").append(tr::rich(b));
|
||||
return result;
|
||||
}),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
box->addButton(tr::lng_group_call_mac_settings(), [=] {
|
||||
Platform::OpenDesktopCapturePrivacySettings();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
});
|
||||
#else // Q_OS_MAC
|
||||
return { nullptr };
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
|
||||
not_null<QWidget*> parent,
|
||||
const style::icon &icon,
|
||||
const style::margins &padding) {
|
||||
const auto logoSize = icon.size();
|
||||
const auto logoOuter = logoSize.grownBy(padding);
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto logo = result.data();
|
||||
logo->resize(logo->width(), logoOuter.height());
|
||||
logo->paintRequest() | rpl::on_next([=, &icon] {
|
||||
if (logo->width() < logoOuter.width()) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(logo);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto x = (logo->width() - logoOuter.width()) / 2;
|
||||
const auto outer = QRect(QPoint(x, 0), logoOuter);
|
||||
p.setBrush(st::windowBgActive);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(outer);
|
||||
icon.paintInCenter(p, outer);
|
||||
}, logo->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
|
||||
return MakeRoundActiveLogo(
|
||||
parent,
|
||||
st::confcallJoinLogo,
|
||||
st::confcallJoinLogoPadding);
|
||||
}
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join) {
|
||||
box->setStyle(st::confcallJoinBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
box->addRow(
|
||||
MakeJoinCallLogo(box),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_join_title(),
|
||||
st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
const auto wrapName = [&](not_null<PeerData*> peer) {
|
||||
return rpl::single(tr::bold(peer->shortName()));
|
||||
};
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
(maybeInviter
|
||||
? tr::lng_confcall_join_text_inviter(
|
||||
lt_user,
|
||||
wrapName(maybeInviter),
|
||||
tr::rich)
|
||||
: tr::lng_confcall_join_text(tr::rich)),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
const auto known = int(participants.size());
|
||||
if (known) {
|
||||
const auto sep = box->addRow(
|
||||
object_ptr<Ui::RpWidget>(box),
|
||||
st::boxRowPadding + st::confcallJoinSepPadding);
|
||||
sep->resize(sep->width(), st::normalFont->height);
|
||||
sep->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.2);
|
||||
p.fillRect(0, top, sep->width(), line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
auto peers = std::vector<not_null<PeerData*>>();
|
||||
for (const auto &participant : participants) {
|
||||
peers.push_back(participant.peer);
|
||||
if (peers.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
box->addRow(
|
||||
CreateUserpicsWithMoreBadge(
|
||||
box,
|
||||
rpl::single(peers),
|
||||
st::confcallJoinUserpics,
|
||||
known),
|
||||
st::boxRowPadding + st::confcallJoinUserpicsPadding);
|
||||
|
||||
const auto wrapByIndex = [&](int index) {
|
||||
Expects(index >= 0 && index < known);
|
||||
|
||||
return wrapName(participants[index].peer);
|
||||
};
|
||||
auto text = (known == 1)
|
||||
? tr::lng_confcall_already_joined_one(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
tr::rich)
|
||||
: (known == 2)
|
||||
? tr::lng_confcall_already_joined_two(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
tr::rich)
|
||||
: (known == 3)
|
||||
? tr::lng_confcall_already_joined_three(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
lt_third,
|
||||
wrapByIndex(2),
|
||||
tr::rich)
|
||||
: tr::lng_confcall_already_joined_many(
|
||||
lt_count,
|
||||
rpl::single(1. * (std::max(known, call->fullCount()) - 2)),
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
tr::rich);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
}
|
||||
box->addButton(tr::lng_confcall_join_button(), [=] {
|
||||
join([weak = base::make_weak(box)] {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {
|
||||
return {
|
||||
.box = &st::groupCallLinkBox,
|
||||
.menuToggle = &st::groupCallLinkMenu,
|
||||
.menu = &st::groupCallPopupMenuWithIcons,
|
||||
.close = &st::storiesStealthBoxClose,
|
||||
.centerLabel = &st::groupCallLinkCenteredText,
|
||||
.linkPreview = &st::groupCallLinkPreview,
|
||||
.contextRevoke = &st::mediaMenuIconRemove,
|
||||
.shareBox = std::make_shared<ShareBoxStyleOverrides>(
|
||||
DarkShareBoxStyle()),
|
||||
};
|
||||
}
|
||||
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args) {
|
||||
const auto st = args.st;
|
||||
const auto initial = args.initial;
|
||||
const auto link = call->conferenceInviteLink();
|
||||
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
bool resetting = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
box->setStyle(st.box
|
||||
? *st.box
|
||||
: initial
|
||||
? st::confcallLinkBoxInitial
|
||||
: st::confcallLinkBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
const auto close = box->addTopButton(
|
||||
st.close ? *st.close : st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
|
||||
if (!args.initial && call->canManage()) {
|
||||
const auto toggle = Ui::CreateChild<Ui::IconButton>(
|
||||
close->parentWidget(),
|
||||
st.menuToggle ? *st.menuToggle : st::boxTitleMenu);
|
||||
const auto handler = [=] {
|
||||
if (state->resetting) {
|
||||
return;
|
||||
}
|
||||
state->resetting = true;
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
const auto weak = base::make_weak(box);
|
||||
call->session().api().request(
|
||||
MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_reset_invite_hash),
|
||||
call->input(),
|
||||
MTPBool(), // join_muted
|
||||
MTPBool(), // messages_enabled
|
||||
MTPlong()) // send_paid_messages_stars
|
||||
).done([=](const MTPUpdates &result) {
|
||||
call->session().api().applyUpdates(result);
|
||||
ShowConferenceCallLinkBox(show, call, args);
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
show->showToast({
|
||||
.title = tr::lng_confcall_link_revoked_title(
|
||||
tr::now),
|
||||
.text = {
|
||||
tr::lng_confcall_link_revoked_text(tr::now),
|
||||
},
|
||||
});
|
||||
}).send();
|
||||
};
|
||||
toggle->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
toggle,
|
||||
st.menu ? *st.menu : st::popupMenuWithIcons);
|
||||
state->menu->addAction(
|
||||
tr::lng_confcall_link_revoke(tr::now),
|
||||
handler,
|
||||
(st.contextRevoke
|
||||
? st.contextRevoke
|
||||
: &st::menuIconRemove));
|
||||
state->menu->popup(QCursor::pos());
|
||||
});
|
||||
|
||||
close->geometryValue(
|
||||
) | rpl::on_next([=](QRect geometry) {
|
||||
toggle->moveToLeft(
|
||||
geometry.x() - toggle->width(),
|
||||
geometry.y());
|
||||
}, close->lifetime());
|
||||
}
|
||||
|
||||
box->addRow(
|
||||
Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_title(),
|
||||
st.box ? st.box->title : st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_about(),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText)),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
|
||||
const auto preview = box->addRow(
|
||||
Info::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
const auto copyCallback = [=] {
|
||||
QApplication::clipboard()->setText(link);
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
};
|
||||
const auto shareCallback = [=] {
|
||||
FastShareLink(
|
||||
show,
|
||||
link,
|
||||
st.shareBox ? *st.shareBox : ShareBoxStyleOverrides());
|
||||
};
|
||||
preview->setClickedCallback(copyCallback);
|
||||
const auto share = box->addButton(
|
||||
tr::lng_group_invite_share(),
|
||||
shareCallback,
|
||||
st::confcallLinkShareButton);
|
||||
const auto copy = box->addButton(
|
||||
tr::lng_group_invite_copy(),
|
||||
copyCallback,
|
||||
st::confcallLinkCopyButton);
|
||||
|
||||
rpl::combine(
|
||||
box->widthValue(),
|
||||
copy->widthValue(),
|
||||
share->widthValue()
|
||||
) | rpl::on_next([=] {
|
||||
const auto width = st::boxWideWidth;
|
||||
const auto padding = st::confcallLinkBox.buttonPadding;
|
||||
const auto available = width - 2 * padding.right();
|
||||
const auto buttonWidth = (available - padding.left()) / 2;
|
||||
copy->resizeToWidth(buttonWidth);
|
||||
share->resizeToWidth(buttonWidth);
|
||||
copy->moveToLeft(padding.right(), copy->y(), width);
|
||||
share->moveToRight(padding.right(), share->y(), width);
|
||||
}, box->lifetime());
|
||||
|
||||
if (!initial) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sep = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_or(),
|
||||
st::confcallLinkFooterOr);
|
||||
sep->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto text = sep->textMaxWidth();
|
||||
const auto white = (sep->width() - 2 * text) / 2;
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.4);
|
||||
p.fillRect(0, top, white, line, fg);
|
||||
p.fillRect(sep->width() - white, top, white, line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
const auto footer = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_join(
|
||||
lt_link,
|
||||
tr::lng_confcall_link_join_link(
|
||||
lt_arrow,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
|
||||
[](QString v) { return tr::link(v); }),
|
||||
tr::marked),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText));
|
||||
footer->setTryMakeSimilarLines(true);
|
||||
footer->setClickHandlerFilter([=](const auto &...) {
|
||||
if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.call = call,
|
||||
.linkSlug = std::move(slug),
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
copy->geometryValue() | rpl::on_next([=](QRect geometry) {
|
||||
const auto width = st::boxWideWidth
|
||||
- st::boxRowPadding.left()
|
||||
- st::boxRowPadding.right();
|
||||
footer->resizeToWidth(width);
|
||||
const auto top = geometry.y()
|
||||
+ geometry.height()
|
||||
+ st::confcallLinkFooterOrTop;
|
||||
sep->resizeToWidth(width / 2);
|
||||
sep->move(
|
||||
st::boxRowPadding.left() + (width - sep->width()) / 2,
|
||||
top);
|
||||
footer->moveToLeft(
|
||||
st::boxRowPadding.left(),
|
||||
top + sep->height() + st::confcallLinkFooterOrSkip);
|
||||
}, footer->lifetime());
|
||||
}));
|
||||
}
|
||||
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args) {
|
||||
const auto show = std::move(args.show);
|
||||
const auto finished = std::move(args.finished);
|
||||
const auto session = &show->session();
|
||||
const auto fail = [=](QString error) {
|
||||
show->showToast(error);
|
||||
if (const auto onstack = finished) {
|
||||
onstack(false);
|
||||
}
|
||||
};
|
||||
session->api().request(MTPphone_CreateConferenceCall(
|
||||
MTP_flags(0),
|
||||
MTP_int(base::RandomValue<int32>()),
|
||||
MTPint256(), // public_key
|
||||
MTPbytes(), // block
|
||||
MTPDataJSON() // params
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
auto call = session->data().sharedConferenceCallFind(result);
|
||||
if (!call) {
|
||||
fail(u"Call not found!"_q);
|
||||
return;
|
||||
}
|
||||
session->api().applyUpdates(result);
|
||||
|
||||
const auto link = call ? call->conferenceInviteLink() : QString();
|
||||
if (link.isEmpty()) {
|
||||
fail(u"Call link not found!"_q);
|
||||
return;
|
||||
}
|
||||
Calls::Group::ShowConferenceCallLinkBox(
|
||||
show,
|
||||
call,
|
||||
{ .initial = true });
|
||||
if (const auto onstack = finished) {
|
||||
finished(true);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
fail(error.type());
|
||||
}).send();
|
||||
}
|
||||
|
||||
QString ExtractConferenceSlug(const QString &link) {
|
||||
const auto local = Core::TryConvertUrlToLocal(link);
|
||||
const auto parts1 = QStringView(local).split('#');
|
||||
if (!parts1.isEmpty()) {
|
||||
const auto parts2 = parts1.front().split('&');
|
||||
if (!parts2.isEmpty()) {
|
||||
const auto parts3 = parts2.front().split(u"slug="_q);
|
||||
if (parts3.size() > 1) {
|
||||
return parts3.back().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
209
Telegram/SourceFiles/calls/group/calls_group_common.h
Normal file
209
Telegram/SourceFiles/calls/group/calls_group_common.h
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class UserData;
|
||||
struct ShareBoxStyleOverrides;
|
||||
|
||||
namespace style {
|
||||
struct Box;
|
||||
struct FlatLabel;
|
||||
struct IconButton;
|
||||
struct InputField;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Window;
|
||||
|
||||
struct InviteRequest {
|
||||
not_null<UserData*> user;
|
||||
bool video = false;
|
||||
};
|
||||
|
||||
struct InviteResult {
|
||||
std::vector<not_null<UserData*>> invited;
|
||||
std::vector<not_null<UserData*>> alreadyIn;
|
||||
std::vector<not_null<UserData*>> privacyRestricted;
|
||||
std::vector<not_null<UserData*>> kicked;
|
||||
std::vector<not_null<UserData*>> failed;
|
||||
};
|
||||
|
||||
struct StartConferenceInfo {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
std::shared_ptr<Data::GroupCall> call;
|
||||
QString linkSlug;
|
||||
MsgId joinMessageId;
|
||||
std::vector<InviteRequest> invite;
|
||||
bool sharingLink = false;
|
||||
bool migrating = false;
|
||||
bool muted = false;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;
|
||||
QString videoCaptureScreenId;
|
||||
};
|
||||
|
||||
struct ConferencePanelMigration {
|
||||
std::shared_ptr<Window> window;
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
constexpr auto kDefaultVolume = 10000;
|
||||
constexpr auto kMaxVolume = 20000;
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
|
||||
struct MuteRequest {
|
||||
not_null<PeerData*> peer;
|
||||
bool mute = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct VolumeRequest {
|
||||
not_null<PeerData*> peer;
|
||||
int volume = kDefaultVolume;
|
||||
bool finalized = true;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct ParticipantState {
|
||||
not_null<PeerData*> peer;
|
||||
std::optional<int> volume;
|
||||
bool mutedByMe = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct RejoinEvent {
|
||||
not_null<PeerData*> wasJoinAs;
|
||||
not_null<PeerData*> nowJoinAs;
|
||||
};
|
||||
|
||||
struct RtmpInfo {
|
||||
QString url;
|
||||
QString key;
|
||||
};
|
||||
|
||||
struct JoinInfo {
|
||||
not_null<PeerData*> peer;
|
||||
not_null<PeerData*> joinAs;
|
||||
std::vector<not_null<PeerData*>> possibleJoinAs;
|
||||
QString joinHash;
|
||||
RtmpInfo rtmpInfo;
|
||||
TimeId scheduleDate = 0;
|
||||
bool rtmp = false;
|
||||
};
|
||||
|
||||
enum class PanelMode {
|
||||
Default,
|
||||
Wide,
|
||||
VideoStream,
|
||||
};
|
||||
|
||||
enum class VideoQuality {
|
||||
Thumbnail,
|
||||
Medium,
|
||||
Full,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
NoCamera,
|
||||
CameraFailed,
|
||||
ScreenFailed,
|
||||
MutedNoCamera,
|
||||
MutedNoScreen,
|
||||
DisabledNoCamera,
|
||||
DisabledNoScreen,
|
||||
};
|
||||
|
||||
enum class StickedTooltip {
|
||||
Camera = 0x01,
|
||||
Microphone = 0x02,
|
||||
};
|
||||
constexpr inline bool is_flag_type(StickedTooltip) {
|
||||
return true;
|
||||
}
|
||||
using StickedTooltips = base::flags<StickedTooltip>;
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
|
||||
not_null<QWidget*> parent,
|
||||
const style::icon &icon,
|
||||
const style::margins &padding);
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
|
||||
not_null<QWidget*> parent);
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join);
|
||||
|
||||
struct ConferenceCallLinkStyleOverrides {
|
||||
const style::Box *box = nullptr;
|
||||
const style::IconButton *menuToggle = nullptr;
|
||||
const style::PopupMenu *menu = nullptr;
|
||||
const style::IconButton *close = nullptr;
|
||||
const style::FlatLabel *centerLabel = nullptr;
|
||||
const style::InputField *linkPreview = nullptr;
|
||||
const style::icon *contextRevoke = nullptr;
|
||||
std::shared_ptr<ShareBoxStyleOverrides> shareBox;
|
||||
};
|
||||
[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();
|
||||
|
||||
struct ConferenceCallLinkArgs {
|
||||
ConferenceCallLinkStyleOverrides st;
|
||||
bool initial = false;
|
||||
};
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args);
|
||||
|
||||
struct ConferenceFactoryArgs {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
Fn<void(bool)> finished;
|
||||
bool joining = false;
|
||||
StartConferenceInfo info;
|
||||
};
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args);
|
||||
|
||||
[[nodiscard]] QString ExtractConferenceSlug(const QString &link);
|
||||
|
||||
} // namespace Calls::Group
|
||||
1213
Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp
Normal file
1213
Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
105
Telegram/SourceFiles/calls/group/calls_group_invite_controller.h
Normal file
105
Telegram/SourceFiles/calls/group/calls_group_invite_controller.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/add_participants_box.h"
|
||||
|
||||
namespace Calls {
|
||||
class Call;
|
||||
class GroupCall;
|
||||
struct InviteRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class InviteController final : public ParticipantsBoxController {
|
||||
public:
|
||||
InviteController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn);
|
||||
|
||||
void prepare() override;
|
||||
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
|
||||
void itemDeselectedHook(not_null<PeerData*> peer) override;
|
||||
|
||||
[[nodiscard]] auto peersWithRows() const
|
||||
-> not_null<const base::flat_set<not_null<UserData*>>*>;
|
||||
[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;
|
||||
|
||||
[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<PeerData*> participant) const override;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
const base::flat_set<not_null<UserData*>> _alreadyIn;
|
||||
mutable base::flat_set<not_null<UserData*>> _inGroup;
|
||||
rpl::event_stream<not_null<UserData*>> _rowAdded;
|
||||
|
||||
};
|
||||
|
||||
class InviteContactsController final : public AddParticipantsBoxController {
|
||||
public:
|
||||
InviteContactsController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
|
||||
rpl::producer<not_null<UserData*>> discoveredInGroup);
|
||||
|
||||
private:
|
||||
void prepareViewHook() override;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<UserData*> user) override;
|
||||
|
||||
bool needsInviteLinkButton() override {
|
||||
return false;
|
||||
}
|
||||
|
||||
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
|
||||
rpl::producer<not_null<UserData*>> _discoveredInGroup;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast,
|
||||
Fn<void()> shareConferenceLink = nullptr);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<Call*> call,
|
||||
Fn<void(std::vector<InviteRequest>)> inviteUsers,
|
||||
Fn<void()> shareLink);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
MsgId inviteMsgId,
|
||||
std::vector<not_null<UserData*>> prioritize);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(
|
||||
not_null<::Window::SessionController*> window,
|
||||
Fn<void()> created = nullptr,
|
||||
MsgId discardedInviteMsgId = 0,
|
||||
std::vector<not_null<UserData*>> prioritize = {});
|
||||
|
||||
} // namespace Calls::Group
|
||||
2225
Telegram/SourceFiles/calls/group/calls_group_members.cpp
Normal file
2225
Telegram/SourceFiles/calls/group/calls_group_members.cpp
Normal file
File diff suppressed because it is too large
Load Diff
131
Telegram/SourceFiles/calls/group/calls_group_members.h
Normal file
131
Telegram/SourceFiles/calls/group/calls_group_members.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class ScrollArea;
|
||||
class VerticalLayout;
|
||||
class SettingsButton;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct FingerprintBadgeState;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport;
|
||||
class MembersRow;
|
||||
struct VolumeRequest;
|
||||
struct MuteRequest;
|
||||
enum class PanelMode;
|
||||
|
||||
class Members final
|
||||
: public Ui::RpWidget
|
||||
, private PeerListContentDelegate {
|
||||
public:
|
||||
Members(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<GroupCall*> call,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend);
|
||||
~Members();
|
||||
|
||||
[[nodiscard]] not_null<Viewport*> viewport() const;
|
||||
[[nodiscard]] int desiredHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] auto toggleMuteRequests() const
|
||||
-> rpl::producer<Group::MuteRequest>;
|
||||
[[nodiscard]] auto changeVolumeRequests() const
|
||||
-> rpl::producer<Group::VolumeRequest>;
|
||||
[[nodiscard]] auto kickParticipantRequests() const
|
||||
-> rpl::producer<not_null<PeerData*>>;
|
||||
[[nodiscard]] rpl::producer<> addMembersRequests() const {
|
||||
return _addMemberRequests.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> shareLinkRequests() const {
|
||||
return _shareLinkRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(
|
||||
not_null<PeerData*> peer) const;
|
||||
|
||||
void setMode(PanelMode mode);
|
||||
[[nodiscard]] QRect getInnerGeometry() const;
|
||||
|
||||
private:
|
||||
class Controller;
|
||||
struct VideoTile;
|
||||
using ListWidget = PeerListContent;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
// PeerListContentDelegate interface.
|
||||
void peerListSetTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetHideEmpty(bool hide) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
void peerListAddSelectedRowInBunch(
|
||||
not_null<PeerListRow*> row) override;
|
||||
void peerListFinishSelectedRowsBunch() override;
|
||||
void peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) override;
|
||||
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
|
||||
|
||||
void setupAddMember(not_null<GroupCall*> call);
|
||||
void resizeToList();
|
||||
void setupList();
|
||||
void setupFingerprint();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void trackViewportGeometry();
|
||||
void updateControlsGeometry();
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
rpl::variable<PanelMode> _mode = PanelMode();
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<Controller> _listController;
|
||||
not_null<Ui::VerticalLayout*> _layout;
|
||||
Ui::RpWidget *_fingerprint = nullptr;
|
||||
rpl::event_stream<> _fingerprintRepaints;
|
||||
const FingerprintBadgeState *_fingerprintState = nullptr;
|
||||
const not_null<Ui::RpWidget*> _videoWrap;
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
|
||||
rpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;
|
||||
RpWidget *_topSkip = nullptr;
|
||||
RpWidget *_bottomSkip = nullptr;
|
||||
ListWidget *_list = nullptr;
|
||||
rpl::event_stream<> _addMemberRequests;
|
||||
rpl::event_stream<> _shareLinkRequests;
|
||||
|
||||
mutable std::unique_ptr<MembersRow> _rtmpFakeRow;
|
||||
|
||||
rpl::variable<bool> _canInviteByLink;
|
||||
rpl::variable<bool> _canAddMembers;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
822
Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
Normal file
822
Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
Normal file
@@ -0,0 +1,822 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/paint/arcs.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kArcsStrokeRatio = 0.8;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
Group::kDefaultVolume * 0.1f / Group::kMaxVolume,
|
||||
Group::kDefaultVolume * 0.9f / Group::kMaxVolume };
|
||||
|
||||
auto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::groupCallRowBlobMinRadius,
|
||||
.maxRadius = (float)st::groupCallRowBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
[[nodiscard]] QString StatusPercentString(float volume) {
|
||||
return QString::number(int(base::SafeRound(volume * 200))) + '%';
|
||||
}
|
||||
|
||||
[[nodiscard]] int StatusPercentWidth(const QString &percent) {
|
||||
return st::normalFont->width(percent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MembersRow::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel);
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSoundingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
|
||||
QImage userpicCache;
|
||||
InMemoryKey userpicKey;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
struct MembersRow::StatusIcon {
|
||||
StatusIcon(bool shown, float volume);
|
||||
|
||||
const style::icon &speaker;
|
||||
Ui::Paint::ArcsAnimation arcs;
|
||||
Ui::Animations::Simple arcsAnimation;
|
||||
Ui::Animations::Simple shownAnimation;
|
||||
QString percent;
|
||||
int percentWidth = 0;
|
||||
int arcsWidth = 0;
|
||||
int wasArcsWidth = 0;
|
||||
bool shown = true;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
MembersRow::BlobsAnimation::BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
userpicCache = QImage();
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
MembersRow::StatusIcon::StatusIcon(bool shown, float volume)
|
||||
: speaker(st::groupCallStatusSpeakerIcon)
|
||||
, arcs(
|
||||
st::groupCallStatusSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
volume,
|
||||
Ui::Paint::ArcsAnimation::Direction::Right)
|
||||
, percent(StatusPercentString(volume))
|
||||
, percentWidth(StatusPercentWidth(percent))
|
||||
, shown(shown) {
|
||||
}
|
||||
|
||||
MembersRow::MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer)
|
||||
: PeerListRow(participantPeer)
|
||||
, _delegate(delegate) {
|
||||
refreshStatus();
|
||||
_about.setText(st::defaultTextStyle, participantPeer->about());
|
||||
}
|
||||
|
||||
MembersRow::~MembersRow() = default;
|
||||
|
||||
void MembersRow::setSkipLevelUpdate(bool value) {
|
||||
_skipLevelUpdate = value;
|
||||
}
|
||||
|
||||
void MembersRow::updateStateInvited(bool calling) {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(calling ? State::Calling : State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateStateWithAccess() {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(State::WithAccess);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateState(const Data::GroupCallParticipant &participant) {
|
||||
setVolume(participant.volume);
|
||||
if (!participant.muted
|
||||
|| (participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {
|
||||
setState(State::Active);
|
||||
setSounding((participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
setSpeaking((participant.speaking && participant.ssrc != 0)
|
||||
|| (participant.additionalSpeaking
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else if (participant.canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else {
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = participant.raisedHandRating;
|
||||
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
|
||||
}
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::setSpeaking(bool speaking) {
|
||||
if (_speaking == speaking) {
|
||||
return;
|
||||
}
|
||||
_speaking = speaking;
|
||||
_speakingAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
_speaking ? 0. : 1.,
|
||||
_speaking ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
|
||||
if (!_speaking
|
||||
|| _mutedByMe
|
||||
|| (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand)) {
|
||||
if (_statusIcon) {
|
||||
_statusIcon = nullptr;
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
} else if (!_statusIcon) {
|
||||
_statusIcon = std::make_unique<StatusIcon>(
|
||||
(_volume != Group::kDefaultVolume),
|
||||
(float)_volume / Group::kMaxVolume);
|
||||
_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);
|
||||
_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();
|
||||
_statusIcon->arcs.startUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
if (!_statusIcon->arcsAnimation.animating()) {
|
||||
_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;
|
||||
}
|
||||
auto callback = [=](float64 value) {
|
||||
_statusIcon->arcs.update(crl::now());
|
||||
_statusIcon->arcsWidth = anim::interpolate(
|
||||
_statusIcon->wasArcsWidth,
|
||||
_statusIcon->arcs.finishedWidth(),
|
||||
value);
|
||||
_delegate->rowUpdateRow(this);
|
||||
};
|
||||
_statusIcon->arcsAnimation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}, _statusIcon->lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setSounding(bool sounding) {
|
||||
if (_sounding == sounding) {
|
||||
return;
|
||||
}
|
||||
_sounding = sounding;
|
||||
if (!_sounding) {
|
||||
_blobsAnimation = nullptr;
|
||||
} else if (!_blobsAnimation) {
|
||||
_blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
RowBlobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
_blobsAnimation->lastTime = crl::now();
|
||||
updateLevel(GroupCall::kSpeakLevelThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::clearRaisedHandStatus() {
|
||||
if (!_raisedHandStatus) {
|
||||
return;
|
||||
}
|
||||
_raisedHandStatus = false;
|
||||
refreshStatus();
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::setState(State state) {
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
const auto wasActive = (_state == State::Active);
|
||||
const auto wasMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto wasRaisedHand = (_state == State::RaisedHand);
|
||||
_state = state;
|
||||
const auto nowActive = (_state == State::Active);
|
||||
const auto nowMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto nowRaisedHand = (_state == State::RaisedHand);
|
||||
if (!wasRaisedHand && nowRaisedHand) {
|
||||
_raisedHandStatus = true;
|
||||
_delegate->rowScheduleRaisedHandStatusRemove(this);
|
||||
}
|
||||
if (nowActive != wasActive) {
|
||||
_activeAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowActive ? 0. : 1.,
|
||||
nowActive ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
if (nowMuted != wasMuted) {
|
||||
_mutedAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowMuted ? 0. : 1.,
|
||||
nowMuted ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setVolume(int volume) {
|
||||
_volume = volume;
|
||||
if (_statusIcon) {
|
||||
const auto floatVolume = (float)volume / Group::kMaxVolume;
|
||||
_statusIcon->arcs.setValue(floatVolume);
|
||||
_statusIcon->percent = StatusPercentString(floatVolume);
|
||||
_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);
|
||||
|
||||
const auto shown = (volume != Group::kDefaultVolume);
|
||||
if (_statusIcon->shown != shown) {
|
||||
_statusIcon->shown = shown;
|
||||
_statusIcon->shownAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::updateLevel(float level) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto spoke = (level >= GroupCall::kSpeakLevelThreshold)
|
||||
? crl::now()
|
||||
: crl::time();
|
||||
if (spoke && _speaking) {
|
||||
_speakingLastTime = spoke;
|
||||
}
|
||||
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spoke) {
|
||||
_blobsAnimation->lastSoundingUpdateTime = spoke;
|
||||
}
|
||||
_blobsAnimation->blobs.setLevel(level);
|
||||
}
|
||||
|
||||
void MembersRow::updateBlobAnimation(crl::time now) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
|
||||
+ Data::GroupCall::kSoundStatusKeptFor;
|
||||
const auto soundingStartsFinishing = soundingFinishesAt
|
||||
- kBlobsEnterDuration;
|
||||
const auto soundingFinishes = (soundingStartsFinishing < now);
|
||||
if (soundingFinishes) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
|
||||
0.,
|
||||
1.);
|
||||
} else if (_blobsAnimation->enter < 1.) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(_blobsAnimation->enter
|
||||
+ ((now - _blobsAnimation->lastTime)
|
||||
/ float64(kBlobsEnterDuration))),
|
||||
0.,
|
||||
1.);
|
||||
}
|
||||
_blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime);
|
||||
_blobsAnimation->lastTime = now;
|
||||
}
|
||||
|
||||
void MembersRow::ensureUserpicCache(
|
||||
Ui::PeerUserpicView &view,
|
||||
int size) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto user = peer();
|
||||
const auto key = user->userpicUniqueKey(view);
|
||||
const auto full = QSize(size, size)
|
||||
* kWideScale
|
||||
* style::DevicePixelRatio();
|
||||
auto &cache = _blobsAnimation->userpicCache;
|
||||
if (cache.isNull()) {
|
||||
cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
} else if (_blobsAnimation->userpicKey == key
|
||||
&& cache.size() == full) {
|
||||
return;
|
||||
}
|
||||
_blobsAnimation->userpicKey = key;
|
||||
cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
if (!_blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
auto size = sizew;
|
||||
const auto shift = QPointF(x + size / 2., y + size / 2.);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.translate(shift);
|
||||
const auto brush = _mutedByMe
|
||||
? st::groupCallMemberMutedIcon->b
|
||||
: anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
_blobsAnimation->blobs.paint(p, brush);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
void MembersRow::paintScaledUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
auto size = sizew;
|
||||
if (!_blobsAnimation) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
const auto enter = _blobsAnimation->enter;
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
const auto scaleUserpic = minScale
|
||||
+ (1. - minScale) * _blobsAnimation->blobs.currentLevel();
|
||||
const auto scale = scaleUserpic * enter + 1. * (1. - enter);
|
||||
if (scale == 1.) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
ensureUserpicCache(userpic, size);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
auto target = QRect(
|
||||
x + (1 - kWideScale) / 2 * size,
|
||||
y + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(
|
||||
target.marginsAdded(margins),
|
||||
_blobsAnimation->userpicCache);
|
||||
}
|
||||
|
||||
void MembersRow::paintMuteIcon(
|
||||
QPainter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style) {
|
||||
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
|
||||
}
|
||||
|
||||
QString MembersRow::generateName() {
|
||||
const auto result = peer()->name();
|
||||
if (result.isEmpty()) {
|
||||
DEBUG_LOG(("UnknownParticipant: %1, Loaded: %2, Name Version: %3"
|
||||
).arg(peerToUser(peer()->id).bare
|
||||
).arg(peer()->isLoaded() ? "TRUE" : "FALSE"
|
||||
).arg(peer()->nameVersion()));
|
||||
}
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
QString MembersRow::generateShortName() {
|
||||
const auto result = peer()->shortName();
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
auto MembersRow::generatePaintUserpicCallback(bool forceRound)
|
||||
-> PaintRoundImageCallback {
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) {
|
||||
const auto outer = outerWidth;
|
||||
paintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default);
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected) {
|
||||
paintBlobs(p, x, y, sizew, sizeh, mode);
|
||||
paintScaledUserpic(
|
||||
p,
|
||||
ensureUserpicView(),
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
sizew,
|
||||
sizeh,
|
||||
mode);
|
||||
}
|
||||
|
||||
int MembersRow::statusIconWidth(bool skipIcon) const {
|
||||
if (!_statusIcon || !_speaking) {
|
||||
return 0;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (_statusIcon->speaker.width() + _statusIcon->arcsWidth);
|
||||
const auto full = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
return int(base::SafeRound(shown * full));
|
||||
}
|
||||
|
||||
int MembersRow::statusIconHeight() const {
|
||||
return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon) {
|
||||
if (!_statusIcon) {
|
||||
return;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.setFont(font);
|
||||
const auto color = (_speaking
|
||||
? st.statusFgActive
|
||||
: (selected ? st.statusFgOver : st.statusFg))->c;
|
||||
p.setPen(color);
|
||||
|
||||
const auto speakerRect = QRect(
|
||||
QPoint(x, y + (font->height - statusIconHeight()) / 2),
|
||||
_statusIcon->speaker.size());
|
||||
const auto arcPosition = speakerRect.topLeft()
|
||||
+ QPoint(
|
||||
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
|
||||
speakerRect.height() / 2);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (speakerRect.width() + _statusIcon->arcsWidth);
|
||||
const auto fullWidth = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
|
||||
p.save();
|
||||
if (shown < 1.) {
|
||||
const auto centerx = speakerRect.x() + fullWidth / 2;
|
||||
const auto centery = speakerRect.y() + speakerRect.height() / 2;
|
||||
p.translate(centerx, centery);
|
||||
p.scale(shown, shown);
|
||||
p.translate(-centerx, -centery);
|
||||
}
|
||||
if (!skipIcon) {
|
||||
_statusIcon->speaker.paint(
|
||||
p,
|
||||
speakerRect.topLeft(),
|
||||
speakerRect.width(),
|
||||
color);
|
||||
p.translate(arcPosition);
|
||||
_statusIcon->arcs.paint(p, color);
|
||||
p.translate(-arcPosition);
|
||||
}
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st.statusFgActive);
|
||||
p.drawTextLeft(
|
||||
x + iconWidth,
|
||||
y,
|
||||
fullWidth,
|
||||
_statusIcon->percent);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void MembersRow::setAbout(const QString &about) {
|
||||
if (_about.toString() == about) {
|
||||
return;
|
||||
}
|
||||
_about.setText(st::defaultTextStyle, about);
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
paintComplexStatusText(
|
||||
p,
|
||||
st,
|
||||
x,
|
||||
y,
|
||||
availableWidth,
|
||||
outerWidth,
|
||||
selected,
|
||||
MembersRowStyle::Default);
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style) {
|
||||
const auto skip = (style == MembersRowStyle::Default)
|
||||
? _delegate->rowPaintStatusIcon(
|
||||
p,
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
this,
|
||||
computeIconState(MembersRowStyle::Narrow))
|
||||
: 0;
|
||||
const auto narrowMode = (skip > 0);
|
||||
x += skip;
|
||||
availableWidth -= skip;
|
||||
const auto &font = st::normalFont;
|
||||
const auto useAbout = !_about.isEmpty()
|
||||
&& (_state != State::WithAccess)
|
||||
&& (_state != State::Invited)
|
||||
&& (_state != State::Calling)
|
||||
&& (style != MembersRowStyle::Video)
|
||||
&& ((_state == State::RaisedHand && !_raisedHandStatus)
|
||||
|| (_state != State::RaisedHand && !_speaking));
|
||||
if (!useAbout
|
||||
&& _state != State::Invited
|
||||
&& _state != State::Calling
|
||||
&& _state != State::WithAccess
|
||||
&& !_mutedByMe) {
|
||||
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
|
||||
|
||||
const auto translatedWidth = statusIconWidth(narrowMode);
|
||||
p.translate(translatedWidth, 0);
|
||||
const auto guard = gsl::finally([&] {
|
||||
p.translate(-translatedWidth, 0);
|
||||
});
|
||||
|
||||
const auto &style = (!narrowMode
|
||||
|| (_state == State::RaisedHand && _raisedHandStatus))
|
||||
? st
|
||||
: st::groupCallNarrowMembersListItem;
|
||||
PeerListRow::paintStatusText(
|
||||
p,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
availableWidth - translatedWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
return;
|
||||
}
|
||||
p.setPen((style == MembersRowStyle::Video)
|
||||
? st::groupCallVideoSubTextFg
|
||||
: _mutedByMe
|
||||
? st::groupCallMemberMutedIcon
|
||||
: st::groupCallMemberNotJoinedStatus);
|
||||
if (!_mutedByMe && useAbout) {
|
||||
return _about.draw(p, {
|
||||
.position = QPoint(x, y),
|
||||
.outerWidth = outerWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
} else {
|
||||
p.setFont(font);
|
||||
p.drawTextLeft(
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
(_mutedByMe
|
||||
? tr::lng_group_call_muted_by_me_status(tr::now)
|
||||
: _delegate->rowIsMe(peer())
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: (_state == State::WithAccess)
|
||||
? tr::lng_group_call_blockchain_only_status(tr::now)
|
||||
: (_state == State::Calling)
|
||||
? tr::lng_group_call_calling_status(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
}
|
||||
|
||||
QSize MembersRow::rightActionSize() const {
|
||||
return _delegate->rowIsNarrow() ? QSize() : QSize(
|
||||
st::groupCallActiveButton.width,
|
||||
st::groupCallActiveButton.height);
|
||||
}
|
||||
|
||||
bool MembersRow::rightActionDisabled() const {
|
||||
return _delegate->rowIsMe(peer())
|
||||
|| (_state == State::Invited)
|
||||
|| (_state == State::Calling)
|
||||
|| !_delegate->rowCanMuteMembers();
|
||||
}
|
||||
|
||||
QMargins MembersRow::rightActionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
0,
|
||||
st::groupCallMemberButtonSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void MembersRow::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = rightActionSize();
|
||||
const auto iconRect = style::rtlrect(
|
||||
x,
|
||||
y,
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
if (_state == State::Invited
|
||||
|| _state == State::Calling
|
||||
|| _state == State::WithAccess) {
|
||||
_actionRipple = nullptr;
|
||||
}
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
x + st::groupCallActiveButton.rippleAreaPosition.x(),
|
||||
y + st::groupCallActiveButton.rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
paintMuteIcon(p, iconRect);
|
||||
}
|
||||
|
||||
MembersRowDelegate::IconState MembersRow::computeIconState(
|
||||
MembersRowStyle style) const {
|
||||
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
|
||||
const auto active = _activeAnimation.value(
|
||||
(_state == State::Active) ? 1. : 0.);
|
||||
const auto muted = _mutedAnimation.value(
|
||||
(_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.);
|
||||
return {
|
||||
.speaking = speaking,
|
||||
.active = active,
|
||||
.muted = muted,
|
||||
.mutedByMe = _mutedByMe,
|
||||
.raisedHand = (_state == State::RaisedHand),
|
||||
.invited = (_state == State::Invited),
|
||||
.calling = (_state == State::Calling),
|
||||
.style = style,
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::showContextMenu() {
|
||||
return _delegate->rowShowContextMenu(this);
|
||||
}
|
||||
|
||||
void MembersRow::refreshStatus() {
|
||||
setCustomStatus(
|
||||
(_speaking
|
||||
? tr::lng_group_call_active(tr::now)
|
||||
: _raisedHandStatus
|
||||
? tr::lng_group_call_raised_hand_status(tr::now)
|
||||
: tr::lng_group_call_inactive(tr::now)),
|
||||
_speaking);
|
||||
}
|
||||
|
||||
void MembersRow::rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
|
||||
st::groupCallActiveButton.rippleAreaSize,
|
||||
st::groupCallActiveButton.rippleAreaSize));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::groupCallActiveButton.ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition);
|
||||
}
|
||||
|
||||
void MembersRow::refreshName(const style::PeerListItem &st) {
|
||||
PeerListRow::refreshName(st);
|
||||
//_narrowName = Ui::Text::String();
|
||||
}
|
||||
|
||||
void MembersRow::rightActionStopLastRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
230
Telegram/SourceFiles/calls/group/calls_group_members_row.h
Normal file
230
Telegram/SourceFiles/calls/group/calls_group_members_row.h
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
class PeerData;
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
struct GroupCallParticipant;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
struct PeerUserpicView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class MembersRowStyle : uchar {
|
||||
Default,
|
||||
Narrow,
|
||||
Video,
|
||||
};
|
||||
|
||||
class MembersRow;
|
||||
class MembersRowDelegate {
|
||||
public:
|
||||
struct IconState {
|
||||
float64 speaking = 0.;
|
||||
float64 active = 0.;
|
||||
float64 muted = 0.;
|
||||
bool mutedByMe = false;
|
||||
bool raisedHand = false;
|
||||
bool invited = false;
|
||||
bool calling = false;
|
||||
MembersRowStyle style = MembersRowStyle::Default;
|
||||
};
|
||||
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
|
||||
virtual bool rowCanMuteMembers() = 0;
|
||||
virtual void rowUpdateRow(not_null<MembersRow*> row) = 0;
|
||||
virtual void rowScheduleRaisedHandStatusRemove(
|
||||
not_null<MembersRow*> row) = 0;
|
||||
virtual void rowPaintIcon(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const IconState &state) = 0;
|
||||
virtual int rowPaintStatusIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<MembersRow*> row,
|
||||
const IconState &state) = 0;
|
||||
virtual bool rowIsNarrow() = 0;
|
||||
virtual void rowShowContextMenu(not_null<PeerListRow*> row) = 0;
|
||||
};
|
||||
|
||||
class MembersRow final : public PeerListRow {
|
||||
public:
|
||||
MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer);
|
||||
~MembersRow();
|
||||
|
||||
enum class State {
|
||||
Active,
|
||||
Inactive,
|
||||
Muted,
|
||||
RaisedHand,
|
||||
Invited,
|
||||
Calling,
|
||||
WithAccess,
|
||||
};
|
||||
|
||||
void setAbout(const QString &about);
|
||||
void setSkipLevelUpdate(bool value);
|
||||
void updateState(const Data::GroupCallParticipant &participant);
|
||||
void updateStateInvited(bool calling);
|
||||
void updateStateWithAccess();
|
||||
void updateLevel(float level);
|
||||
void updateBlobAnimation(crl::time now);
|
||||
void clearRaisedHandStatus();
|
||||
[[nodiscard]] State state() const {
|
||||
return _state;
|
||||
}
|
||||
[[nodiscard]] bool sounding() const {
|
||||
return _sounding;
|
||||
}
|
||||
[[nodiscard]] bool speaking() const {
|
||||
return _speaking;
|
||||
}
|
||||
[[nodiscard]] bool mutedByMe() const {
|
||||
return _mutedByMe;
|
||||
}
|
||||
[[nodiscard]] crl::time speakingLastTime() const {
|
||||
return _speakingLastTime;
|
||||
}
|
||||
[[nodiscard]] int volume() const {
|
||||
return _volume;
|
||||
}
|
||||
[[nodiscard]] uint64 raisedHandRating() const {
|
||||
return _raisedHandRating;
|
||||
}
|
||||
|
||||
void refreshName(const style::PeerListItem &st) override;
|
||||
|
||||
void rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void rightActionStopLastRipple() override;
|
||||
QSize rightActionSize() const override;
|
||||
bool rightActionDisabled() const override;
|
||||
QMargins rightActionMargins() const override;
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
QString generateName() override;
|
||||
QString generateShortName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(
|
||||
bool forceRound) override;
|
||||
void paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected = false);
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
void paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style);
|
||||
void paintMuteIcon(
|
||||
QPainter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style = MembersRowStyle::Default);
|
||||
[[nodiscard]] MembersRowDelegate::IconState computeIconState(
|
||||
MembersRowStyle style = MembersRowStyle::Default) const;
|
||||
|
||||
void showContextMenu();
|
||||
|
||||
private:
|
||||
struct BlobsAnimation;
|
||||
struct StatusIcon;
|
||||
|
||||
int statusIconWidth(bool skipIcon) const;
|
||||
int statusIconHeight() const;
|
||||
void paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon);
|
||||
|
||||
void refreshStatus() override;
|
||||
void setSounding(bool sounding);
|
||||
void setSpeaking(bool speaking);
|
||||
void setState(State state);
|
||||
void setVolume(int volume);
|
||||
|
||||
void ensureUserpicCache(
|
||||
Ui::PeerUserpicView &view,
|
||||
int size);
|
||||
void paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh, PanelMode mode);
|
||||
void paintScaledUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode);
|
||||
|
||||
const not_null<MembersRowDelegate*> _delegate;
|
||||
State _state = State::Inactive;
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
std::unique_ptr<BlobsAnimation> _blobsAnimation;
|
||||
std::unique_ptr<StatusIcon> _statusIcon;
|
||||
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
|
||||
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
|
||||
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
|
||||
Ui::Text::String _about;
|
||||
crl::time _speakingLastTime = 0;
|
||||
uint64 _raisedHandRating = 0;
|
||||
int _volume = Group::kDefaultVolume;
|
||||
bool _sounding : 1 = false;
|
||||
bool _speaking : 1 = false;
|
||||
bool _raisedHandStatus : 1 = false;
|
||||
bool _skipLevelUpdate : 1 = false;
|
||||
bool _mutedByMe : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
645
Telegram/SourceFiles/calls/group/calls_group_menu.cpp
Normal file
645
Telegram/SourceFiles/calls/group/calls_group_menu.cpp
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "calls/group/ui/calls_group_recording_box.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
|
||||
#include "ui/widgets/dropdown_menu.h"
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
class JoinAsAction final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
JoinAsAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void prepare();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const not_null<PeerData*> _peer;
|
||||
Ui::PeerUserpicView _userpicView;
|
||||
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::String _name;
|
||||
int _textWidth = 0;
|
||||
int _nameWidth = 0;
|
||||
const int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
class RecordingAction final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
RecordingAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<TimeId> startAtValues,
|
||||
Fn<void()> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void prepare(rpl::producer<QString> text);
|
||||
void refreshElapsedText();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
TimeId _startAt = 0;
|
||||
crl::time _startedAt = 0;
|
||||
base::Timer _refreshTimer;
|
||||
|
||||
Ui::Text::String _text;
|
||||
int _textWidth = 0;
|
||||
QString _elapsedText;
|
||||
const int _smallHeight = 0;
|
||||
const int _bigHeight = 0;
|
||||
|
||||
};
|
||||
|
||||
TextParseOptions MenuTextOptions = {
|
||||
TextParseLinks, // flags
|
||||
0, // maxw
|
||||
0, // maxh
|
||||
Qt::LayoutDirectionAuto, // dir
|
||||
};
|
||||
|
||||
JoinAsAction::JoinAsAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback)
|
||||
: ItemBase(parent, st)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _peer(peer)
|
||||
, _height(st::groupCallJoinAsPadding.top()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ st::groupCallJoinAsPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback(std::move(callback));
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
prepare();
|
||||
}
|
||||
|
||||
void JoinAsAction::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
const auto height = contentHeight();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
const auto &padding = st::groupCallJoinAsPadding;
|
||||
_peer->paintUserpic(
|
||||
p,
|
||||
_userpicView,
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
st::groupCallJoinAsPhotoSize);
|
||||
const auto textLeft = padding.left()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ padding.left();
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
textLeft,
|
||||
st::groupCallJoinAsTextTop,
|
||||
_textWidth,
|
||||
width());
|
||||
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
textLeft,
|
||||
st::groupCallJoinAsNameTop,
|
||||
_nameWidth,
|
||||
width());
|
||||
}
|
||||
|
||||
void JoinAsAction::prepare() {
|
||||
rpl::combine(
|
||||
tr::lng_group_call_display_as_header(),
|
||||
Info::Profile::NameValue(_peer)
|
||||
) | rpl::on_next([=](QString text, QString name) {
|
||||
const auto &padding = st::groupCallJoinAsPadding;
|
||||
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
|
||||
_name.setMarkedText(_st.itemStyle, { name }, MenuTextOptions);
|
||||
const auto textWidth = _text.maxWidth();
|
||||
const auto nameWidth = _name.maxWidth();
|
||||
const auto textLeft = padding.left()
|
||||
+ st::groupCallJoinAsPhotoSize
|
||||
+ padding.left();
|
||||
const auto w = std::clamp(
|
||||
(textLeft
|
||||
+ std::max(textWidth, nameWidth)
|
||||
+ padding.right()),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
setMinWidth(w);
|
||||
_textWidth = w - textLeft - padding.right();
|
||||
_nameWidth = w - textLeft - padding.right();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
bool JoinAsAction::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> JoinAsAction::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint JoinAsAction::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage JoinAsAction::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int JoinAsAction::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void JoinAsAction::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
RecordingAction::RecordingAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<TimeId> startAtValues,
|
||||
Fn<void()> callback)
|
||||
: ItemBase(parent, st)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _refreshTimer([=] { refreshElapsedText(); })
|
||||
, _smallHeight(st.itemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st.itemPadding.bottom())
|
||||
, _bigHeight(st::groupCallRecordingTimerPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st::groupCallRecordingTimerFont->height
|
||||
+ st::groupCallRecordingTimerPadding.bottom()) {
|
||||
std::move(
|
||||
startAtValues
|
||||
) | rpl::on_next([=](TimeId startAt) {
|
||||
_startAt = startAt;
|
||||
_startedAt = crl::now();
|
||||
_refreshTimer.cancel();
|
||||
refreshElapsedText();
|
||||
resize(width(), contentHeight());
|
||||
}, lifetime());
|
||||
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback(std::move(callback));
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
prepare(std::move(text));
|
||||
}
|
||||
|
||||
void RecordingAction::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
const auto height = contentHeight();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
const auto smallTop = st::groupCallRecordingTimerPadding.top();
|
||||
const auto textTop = _startAt ? smallTop : _st.itemPadding.top();
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
textTop,
|
||||
_textWidth,
|
||||
width());
|
||||
if (_startAt) {
|
||||
p.setFont(st::groupCallRecordingTimerFont);
|
||||
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
|
||||
p.drawTextLeft(
|
||||
_st.itemPadding.left(),
|
||||
smallTop + _st.itemStyle.font->height,
|
||||
width(),
|
||||
_elapsedText);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordingAction::refreshElapsedText() {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto elapsed = std::max(now - _startAt, 0);
|
||||
const auto text = !_startAt
|
||||
? QString()
|
||||
: (elapsed >= 3600)
|
||||
? QString("%1:%2:%3"
|
||||
).arg(elapsed / 3600
|
||||
).arg((elapsed % 3600) / 60, 2, 10, QChar('0')
|
||||
).arg(elapsed % 60, 2, 10, QChar('0'))
|
||||
: QString("%1:%2"
|
||||
).arg(elapsed / 60
|
||||
).arg(elapsed % 60, 2, 10, QChar('0'));
|
||||
if (_elapsedText != text) {
|
||||
_elapsedText = text;
|
||||
update();
|
||||
}
|
||||
|
||||
const auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500);
|
||||
_refreshTimer.callOnce(nextCall);
|
||||
}
|
||||
|
||||
void RecordingAction::prepare(rpl::producer<QString> text) {
|
||||
refreshElapsedText();
|
||||
|
||||
const auto &padding = _st.itemPadding;
|
||||
const auto textWidth1 = _st.itemStyle.font->width(
|
||||
tr::lng_group_call_recording_start(tr::now));
|
||||
const auto textWidth2 = _st.itemStyle.font->width(
|
||||
tr::lng_group_call_recording_stop(tr::now));
|
||||
const auto maxWidth = st::groupCallRecordingTimerFont->width("23:59:59");
|
||||
const auto w = std::clamp(
|
||||
(padding.left()
|
||||
+ std::max({ textWidth1, textWidth2, maxWidth })
|
||||
+ padding.right()),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
setMinWidth(w);
|
||||
|
||||
std::move(text) | rpl::on_next([=](QString text) {
|
||||
const auto &padding = _st.itemPadding;
|
||||
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
|
||||
_textWidth = w - padding.left() - padding.right();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
bool RecordingAction::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> RecordingAction::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint RecordingAction::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage RecordingAction::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int RecordingAction::contentHeight() const {
|
||||
return _startAt ? _bigHeight : _smallHeight;
|
||||
}
|
||||
|
||||
void RecordingAction::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeJoinAsAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> callback) {
|
||||
return base::make_unique_q<JoinAsAction>(
|
||||
menu,
|
||||
menu->st(),
|
||||
peer,
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
rpl::producer<TimeId> startDate,
|
||||
Fn<void()> callback) {
|
||||
using namespace rpl::mappers;
|
||||
return base::make_unique_q<RecordingAction>(
|
||||
menu,
|
||||
menu->st(),
|
||||
rpl::conditional(
|
||||
rpl::duplicate(startDate) | rpl::map(!!_1),
|
||||
tr::lng_group_call_recording_stop(),
|
||||
tr::lng_group_call_recording_start()),
|
||||
rpl::duplicate(startDate),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LeaveBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call,
|
||||
bool discardChecked,
|
||||
BoxContext context) {
|
||||
const auto conference = call->conference();
|
||||
const auto livestream = call->peer()->isBroadcast();
|
||||
const auto scheduled = (call->scheduleDate() != 0);
|
||||
if (!scheduled) {
|
||||
box->setTitle(conference
|
||||
? tr::lng_group_call_leave_title_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_title_channel()
|
||||
: tr::lng_group_call_leave_title());
|
||||
}
|
||||
const auto inCall = (context == BoxContext::GroupCallPanel);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
(scheduled
|
||||
? (livestream
|
||||
? tr::lng_group_call_close_sure_channel()
|
||||
: tr::lng_group_call_close_sure())
|
||||
: (conference
|
||||
? tr::lng_group_call_leave_sure_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_sure_channel()
|
||||
: tr::lng_group_call_leave_sure())),
|
||||
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
|
||||
scheduled ? st::boxPadding : st::boxRowPadding);
|
||||
const auto discard = call->canManage()
|
||||
? box->addRow(object_ptr<Ui::Checkbox>(
|
||||
box.get(),
|
||||
(scheduled
|
||||
? (livestream
|
||||
? tr::lng_group_call_also_cancel_channel()
|
||||
: tr::lng_group_call_also_cancel())
|
||||
: (livestream
|
||||
? tr::lng_group_call_also_end_channel()
|
||||
: tr::lng_group_call_also_end())),
|
||||
discardChecked,
|
||||
(inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),
|
||||
(inCall ? st::groupCallCheck : st::defaultCheck)),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxRowPadding.bottom()))
|
||||
: nullptr;
|
||||
const auto weak = base::make_weak(call);
|
||||
auto label = scheduled
|
||||
? tr::lng_group_call_close()
|
||||
: tr::lng_group_call_leave();
|
||||
box->addButton(std::move(label), [=] {
|
||||
const auto discardCall = (discard && discard->checked());
|
||||
box->closeBox();
|
||||
|
||||
if (!weak) {
|
||||
return;
|
||||
} else if (discardCall) {
|
||||
call->discard();
|
||||
} else {
|
||||
call->hangup();
|
||||
}
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
object_ptr<Ui::GenericBox> ConfirmBox(Ui::ConfirmBoxArgs &&args) {
|
||||
auto copy = std::move(args);
|
||||
copy.labelStyle = &st::groupCallBoxLabel;
|
||||
return Ui::MakeConfirmBox(std::move(copy));
|
||||
}
|
||||
|
||||
void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
|
||||
const auto weak = base::make_weak(call);
|
||||
const auto resolveReal = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
if (const auto real = strong->lookupReal()) {
|
||||
return real;
|
||||
}
|
||||
}
|
||||
return (Data::GroupCall*)nullptr;
|
||||
};
|
||||
const auto real = resolveReal();
|
||||
if (!real) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto conference = call->conference();
|
||||
const auto addEditJoinAs = call->showChooseJoinAs();
|
||||
const auto addEditTitle = !conference && call->canManage();
|
||||
const auto addEditRecording = !conference
|
||||
&& call->canManage()
|
||||
&& !real->scheduleDate();
|
||||
const auto addScreenCast = call->videoIsWorking()
|
||||
&& !real->scheduleDate();
|
||||
if (addEditJoinAs) {
|
||||
menu->addAction(MakeJoinAsAction(
|
||||
menu->menu(),
|
||||
call->joinAs(),
|
||||
chooseJoinAs));
|
||||
menu->addSeparator();
|
||||
}
|
||||
if (addEditTitle) {
|
||||
const auto livestream = call->peer()->isBroadcast();
|
||||
const auto text = (livestream
|
||||
? tr::lng_group_call_edit_title_channel
|
||||
: tr::lng_group_call_edit_title)(tr::now);
|
||||
menu->addAction(text, [=] {
|
||||
const auto done = [=](const QString &title) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->changeTitle(title);
|
||||
}
|
||||
};
|
||||
if (const auto real = resolveReal()) {
|
||||
showBox(Box(
|
||||
EditGroupCallTitleBox,
|
||||
peer->name(),
|
||||
real->title(),
|
||||
livestream,
|
||||
done));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (addEditRecording) {
|
||||
const auto handler = [=] {
|
||||
const auto real = resolveReal();
|
||||
if (!real) {
|
||||
return;
|
||||
}
|
||||
const auto type = std::make_shared<RecordingType>();
|
||||
const auto recordStartDate = real->recordStartDate();
|
||||
const auto done = [=](QString title) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->toggleRecording(
|
||||
!recordStartDate,
|
||||
title,
|
||||
(*type) != RecordingType::AudioOnly,
|
||||
(*type) == RecordingType::VideoPortrait);
|
||||
}
|
||||
};
|
||||
if (recordStartDate) {
|
||||
showBox(Box(
|
||||
StopGroupCallRecordingBox,
|
||||
done));
|
||||
} else {
|
||||
const auto typeDone = [=](RecordingType newType) {
|
||||
*type = newType;
|
||||
showBox(Box(
|
||||
AddTitleGroupCallRecordingBox,
|
||||
real->title(),
|
||||
done));
|
||||
};
|
||||
showBox(Box(StartGroupCallRecordingBox, typeDone));
|
||||
}
|
||||
};
|
||||
menu->addAction(MakeRecordingAction(
|
||||
menu->menu(),
|
||||
real->recordStartDateValue(),
|
||||
handler));
|
||||
}
|
||||
if (addScreenCast) {
|
||||
const auto sharing = call->isSharingScreen();
|
||||
const auto toggle = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
if (sharing) {
|
||||
strong->toggleScreenSharing(std::nullopt);
|
||||
} else {
|
||||
chooseShareScreenSource();
|
||||
}
|
||||
}
|
||||
};
|
||||
menu->addAction(
|
||||
(call->isSharingScreen()
|
||||
? tr::lng_group_call_screen_share_stop(tr::now)
|
||||
: tr::lng_group_call_screen_share_start(tr::now)),
|
||||
toggle);
|
||||
}
|
||||
menu->addAction(tr::lng_group_call_settings(tr::now), [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
showBox(Box(SettingsBox, strong));
|
||||
}
|
||||
});
|
||||
const auto finish = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
showBox(Box(
|
||||
LeaveBox,
|
||||
strong,
|
||||
true,
|
||||
BoxContext::GroupCallPanel));
|
||||
}
|
||||
};
|
||||
const auto livestream = real->peer()->isBroadcast();
|
||||
menu->addAction(MakeAttentionAction(
|
||||
menu->menu(),
|
||||
(!call->canManage()
|
||||
? tr::lng_group_call_leave
|
||||
: real->scheduleDate()
|
||||
? (livestream
|
||||
? tr::lng_group_call_cancel_channel
|
||||
: tr::lng_group_call_cancel)
|
||||
: (livestream
|
||||
? tr::lng_group_call_end_channel
|
||||
: tr::lng_group_call_end))(tr::now),
|
||||
finish));
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
const QString &text,
|
||||
Fn<void()> callback) {
|
||||
return base::make_unique_q<Ui::Menu::Action>(
|
||||
menu,
|
||||
st::groupCallFinishMenu,
|
||||
Ui::Menu::CreateAction(
|
||||
menu,
|
||||
text,
|
||||
std::move(callback)),
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
63
Telegram/SourceFiles/calls/group/calls_group_menu.h
Normal file
63
Telegram/SourceFiles/calls/group/calls_group_menu.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class DropdownMenu;
|
||||
class GenericBox;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
class ItemBase;
|
||||
class Menu;
|
||||
} // namespace Ui::Menu
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class BoxContext {
|
||||
GroupCallPanel,
|
||||
MainWindow,
|
||||
};
|
||||
|
||||
void LeaveBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call,
|
||||
bool discardChecked,
|
||||
BoxContext context);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> ConfirmBox(
|
||||
Ui::ConfirmBoxArgs &&args);
|
||||
|
||||
void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox);
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
|
||||
not_null<Ui::Menu::Menu*> menu,
|
||||
const QString &text,
|
||||
Fn<void()> callback);
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_message_encryption.h"
|
||||
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
//[[nodiscard]] MTPJSONValue String(const QByteArray &value) {
|
||||
// return MTP_jsonString(MTP_bytes(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Int(int value) {
|
||||
// return MTP_jsonNumber(MTP_double(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONObjectValue Value(
|
||||
// const QByteArray &name,
|
||||
// const MTPJSONValue &value) {
|
||||
// return MTP_jsonObjectValue(MTP_bytes(name), value);
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Object(
|
||||
// const QByteArray &cons,
|
||||
// QVector<MTPJSONObjectValue> &&values) {
|
||||
// values.insert(values.begin(), Value("_", String(cons)));
|
||||
// return MTP_jsonObject(MTP_vector<MTPJSONObjectValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Array(QVector<MTPJSONValue> &&values) {
|
||||
// return MTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//template <typename MTPD>
|
||||
//[[nodiscard]] MTPJSONValue SimpleEntity(
|
||||
// const QByteArray &name,
|
||||
// const MTPD &data) {
|
||||
// return Object(name, {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Entity(const MTPMessageEntity &entity) {
|
||||
// return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
// return SimpleEntity("messageEntityBold", data);
|
||||
// }, [](const MTPDmessageEntityItalic &data) {
|
||||
// return SimpleEntity("messageEntityItalic", data);
|
||||
// }, [](const MTPDmessageEntityUnderline &data) {
|
||||
// return SimpleEntity("messageEntityUnderline", data);
|
||||
// }, [](const MTPDmessageEntityStrike &data) {
|
||||
// return SimpleEntity("messageEntityStrike", data);
|
||||
// }, [](const MTPDmessageEntitySpoiler &data) {
|
||||
// return SimpleEntity("messageEntitySpoiler", data);
|
||||
// }, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
// return Object("messageEntityCustomEmoji", {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// Value(
|
||||
// "document_id",
|
||||
// String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
// });
|
||||
// }, [](const auto &data) {
|
||||
// return MTP_jsonNull();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QVector<MTPJSONValue> Entities(
|
||||
// const QVector<MTPMessageEntity> &list) {
|
||||
// auto result = QVector<MTPJSONValue>();
|
||||
// result.reserve(list.size());
|
||||
// for (const auto &entity : list) {
|
||||
// if (const auto e = Entity(entity); e.type() != mtpc_jsonNull) {
|
||||
// result.push_back(e);
|
||||
// }
|
||||
// }
|
||||
// return result;
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QByteArray Serialize(const MTPJSONValue &value) {
|
||||
// auto counter = ::tl::details::LengthCounter();
|
||||
// value.write(counter);
|
||||
// auto buffer = mtpBuffer();
|
||||
// buffer.reserve(counter.length);
|
||||
// value.write(buffer);
|
||||
// return QByteArray(
|
||||
// reinterpret_cast<const char*>(buffer.constData()),
|
||||
// buffer.size() * sizeof(buffer.front()));
|
||||
//}
|
||||
|
||||
[[nodiscard]] QJsonValue String(const QByteArray &value) {
|
||||
return QJsonValue(QString::fromUtf8(value));
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Int(int value) {
|
||||
return QJsonValue(double(value));
|
||||
}
|
||||
|
||||
struct JsonObjectValue {
|
||||
const char *name = nullptr;
|
||||
QJsonValue value;
|
||||
};
|
||||
|
||||
[[nodiscard]] JsonObjectValue Value(
|
||||
const char *name,
|
||||
const QJsonValue &value) {
|
||||
return JsonObjectValue{ name, value };
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Object(
|
||||
const char *cons,
|
||||
QVector<JsonObjectValue> &&values) {
|
||||
auto result = QJsonObject();
|
||||
result.insert("_", cons);
|
||||
for (const auto &value : values) {
|
||||
result.insert(value.name, value.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Array(QVector<QJsonValue> &&values) {
|
||||
auto result = QJsonArray();
|
||||
for (const auto &value : values) {
|
||||
result.push_back(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename MTPD>
|
||||
[[nodiscard]] QJsonValue SimpleEntity(
|
||||
const char *name,
|
||||
const MTPD &data) {
|
||||
return Object(name, {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Entity(const MTPMessageEntity &entity) {
|
||||
return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
return SimpleEntity("messageEntityBold", data);
|
||||
}, [](const MTPDmessageEntityItalic &data) {
|
||||
return SimpleEntity("messageEntityItalic", data);
|
||||
}, [](const MTPDmessageEntityUnderline &data) {
|
||||
return SimpleEntity("messageEntityUnderline", data);
|
||||
}, [](const MTPDmessageEntityStrike &data) {
|
||||
return SimpleEntity("messageEntityStrike", data);
|
||||
}, [](const MTPDmessageEntitySpoiler &data) {
|
||||
return SimpleEntity("messageEntitySpoiler", data);
|
||||
}, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
return Object("messageEntityCustomEmoji", {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
Value(
|
||||
"document_id",
|
||||
String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
});
|
||||
}, [](const auto &data) {
|
||||
return QJsonValue(QJsonValue::Null);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<QJsonValue> Entities(
|
||||
const QVector<MTPMessageEntity> &list) {
|
||||
auto result = QVector<QJsonValue>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entity : list) {
|
||||
if (const auto e = Entity(entity); !e.isNull()) {
|
||||
result.push_back(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray Serialize(const QJsonValue &value) {
|
||||
return QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<QJsonValue> GetValue(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto i = object.find(name);
|
||||
return (i != object.end()) ? *i : std::optional<QJsonValue>();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<int> GetInt(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
return int(base::SafeRound(maybeValue->toDouble()));
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toInt(&ok);
|
||||
return ok ? result : std::optional<int>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<uint64> GetLong(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
const auto value = maybeValue->toDouble();
|
||||
return (value >= 0.)
|
||||
? uint64(base::SafeRound(value))
|
||||
: std::optional<uint64>();
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toLongLong(&ok);
|
||||
return ok ? uint64(result) : std::optional<uint64>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetString(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto maybeValue = GetValue(object, name);
|
||||
return (maybeValue && maybeValue->isString())
|
||||
? maybeValue->toString()
|
||||
: std::optional<QString>();
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetCons(const QJsonObject &object) {
|
||||
return GetString(object, "_");
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Unsupported(
|
||||
const QJsonObject &object,
|
||||
const QString &cons = QString()) {
|
||||
const auto maybeMinLayer = GetInt(object, "_min_layer");
|
||||
const auto layer = int(MTP::details::kCurrentLayer);
|
||||
if (maybeMinLayer.value_or(layer) > layer) {
|
||||
LOG(("E2E Error: _min_layer too large: %1 > %2").arg(*maybeMinLayer).arg(layer));
|
||||
return true;
|
||||
} else if (!cons.isEmpty() && GetCons(object) != cons) {
|
||||
LOG(("E2E Error: Expected %1 here.").arg(cons));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MTPMessageEntity> GetEntity(
|
||||
const QString &text,
|
||||
const QJsonObject &object) {
|
||||
const auto cons = GetCons(object).value_or(QString());
|
||||
const auto offset = GetInt(object, "offset").value_or(-1);
|
||||
const auto length = GetInt(object, "length").value_or(0);
|
||||
if (Unsupported(object)
|
||||
|| (offset < 0)
|
||||
|| (length <= 0)
|
||||
|| (offset >= text.size())
|
||||
|| (length > text.size())
|
||||
|| (offset + length > text.size())) {
|
||||
return {};
|
||||
}
|
||||
const auto simple = [&](const auto &make) {
|
||||
return make(MTP_int(offset), MTP_int(length));
|
||||
};
|
||||
if (cons == "messageEntityBold") {
|
||||
return simple(MTP_messageEntityBold);
|
||||
} else if (cons == "messageEntityItalic") {
|
||||
return simple(MTP_messageEntityItalic);
|
||||
} else if (cons == "messageEntityUnderline") {
|
||||
return simple(MTP_messageEntityUnderline);
|
||||
} else if (cons == "messageEntityStrike") {
|
||||
return simple(MTP_messageEntityStrike);
|
||||
} else if (cons == "messageEntitySpoiler") {
|
||||
return simple(MTP_messageEntitySpoiler);
|
||||
} else if (cons == "messageEntityCustomEmoji") {
|
||||
const auto maybeDocumentId = GetLong(object, "document_id");
|
||||
if (const auto documentId = maybeDocumentId.value_or(0)) {
|
||||
return MTP_messageEntityCustomEmoji(
|
||||
MTP_int(offset),
|
||||
MTP_int(length),
|
||||
MTP_long(documentId));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<MTPMessageEntity> GetEntities(
|
||||
const QString &text,
|
||||
const QJsonArray &list) {
|
||||
auto result = QVector<MTPMessageEntity>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entry : list) {
|
||||
if (const auto entity = GetEntity(text, entry.toObject())) {
|
||||
result.push_back(*entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QByteArray SerializeMessage(const PreparedMessage &data) {
|
||||
return Serialize(Object("groupCallMessage", {
|
||||
Value(
|
||||
"random_id",
|
||||
String(QByteArray::number(int64(data.randomId)))),
|
||||
Value(
|
||||
"message",
|
||||
Object("textWithEntities", {
|
||||
Value("text", String(data.message.data().vtext().v)),
|
||||
Value(
|
||||
"entities",
|
||||
Array(Entities(data.message.data().ventities().v))),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data) {
|
||||
auto error = QJsonParseError();
|
||||
auto document = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError
|
||||
|| !document.isObject()) {
|
||||
LOG(("E2E Error: Bad json in Calls::Group::DeserializeMessage."));
|
||||
return {};
|
||||
}
|
||||
const auto groupCallMessage = document.object();
|
||||
if (Unsupported(groupCallMessage, "groupCallMessage")) {
|
||||
return {};
|
||||
}
|
||||
const auto randomId = GetLong(groupCallMessage, "random_id").value_or(0);
|
||||
if (!randomId) {
|
||||
return {};
|
||||
}
|
||||
const auto message = groupCallMessage["message"].toObject();
|
||||
if (Unsupported(message, "textWithEntities")) {
|
||||
return {};
|
||||
}
|
||||
const auto maybeText = GetString(message, "text");
|
||||
if (!maybeText) {
|
||||
return {};
|
||||
}
|
||||
const auto &text = *maybeText;
|
||||
const auto maybeEntities = GetValue(message, "entities");
|
||||
if (!maybeEntities || !maybeEntities->isArray()) {
|
||||
return {};
|
||||
}
|
||||
const auto entities = GetEntities(text, maybeEntities->toArray());
|
||||
return PreparedMessage{
|
||||
.randomId = randomId,
|
||||
.message = MTP_textWithEntities(
|
||||
MTP_string(text),
|
||||
MTP_vector<MTPMessageEntity>(entities)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct PreparedMessage {
|
||||
uint64 randomId = 0;
|
||||
MTPTextWithEntities message;
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray SerializeMessage(const PreparedMessage &data);
|
||||
[[nodiscard]] std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data);
|
||||
|
||||
} // namespace Calls::Group
|
||||
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_message_field.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "history/view/reactions/history_view_reactions_strip.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorLimit = 99;
|
||||
|
||||
using Chosen = HistoryView::Reactions::ChosenReaction;
|
||||
|
||||
} // namespace
|
||||
|
||||
class ReactionPanel final {
|
||||
public:
|
||||
ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry);
|
||||
~ReactionPanel();
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> chosen() const;
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void raise();
|
||||
void hideIfCollapsed();
|
||||
void collapse();
|
||||
|
||||
private:
|
||||
struct Hiding;
|
||||
|
||||
void create();
|
||||
void updateShowState();
|
||||
void fadeOutSelector();
|
||||
void startAnimation();
|
||||
|
||||
const not_null<QWidget*> _outer;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
std::unique_ptr<Ui::RpWidget> _parent;
|
||||
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
|
||||
std::vector<std::unique_ptr<Hiding>> _hiding;
|
||||
rpl::event_stream<Chosen> _chosen;
|
||||
Ui::Animations::Simple _showing;
|
||||
rpl::variable<float64> _shownValue;
|
||||
rpl::variable<QRect> _fieldGeometry;
|
||||
rpl::variable<bool> _expanded;
|
||||
rpl::variable<bool> _shown = false;
|
||||
|
||||
};
|
||||
|
||||
struct ReactionPanel::Hiding {
|
||||
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
|
||||
}
|
||||
|
||||
Ui::RpWidget widget;
|
||||
Ui::Animations::Simple animation;
|
||||
QImage frame;
|
||||
};
|
||||
|
||||
ReactionPanel::ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry)
|
||||
: _outer(outer)
|
||||
, _show(std::move(show))
|
||||
, _fieldGeometry(std::move(fieldGeometry)) {
|
||||
}
|
||||
|
||||
ReactionPanel::~ReactionPanel() = default;
|
||||
|
||||
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
|
||||
return _chosen.events();
|
||||
}
|
||||
|
||||
void ReactionPanel::show() {
|
||||
if (_shown.current()) {
|
||||
return;
|
||||
}
|
||||
create();
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
const auto duration = st::defaultPanelAnimation.heightDuration
|
||||
* st::defaultPopupMenu.showDuration;
|
||||
_shown = true;
|
||||
_showing.start([=] { updateShowState(); }, 0., 1., duration);
|
||||
updateShowState();
|
||||
_parent->show();
|
||||
}
|
||||
|
||||
void ReactionPanel::hide() {
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
_selector->beforeDestroy();
|
||||
if (!anim::Disabled()) {
|
||||
fadeOutSelector();
|
||||
}
|
||||
_shown = false;
|
||||
_expanded = false;
|
||||
_showing.stop();
|
||||
_selector = nullptr;
|
||||
_parent = nullptr;
|
||||
}
|
||||
|
||||
void ReactionPanel::raise() {
|
||||
if (_parent) {
|
||||
_parent->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::hideIfCollapsed() {
|
||||
if (!_expanded.current()) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::collapse() {
|
||||
if (_expanded.current()) {
|
||||
hide();
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::create() {
|
||||
auto reactions = Data::LookupPossibleReactions(&_show->session());
|
||||
if (reactions.recent.empty()) {
|
||||
return;
|
||||
}
|
||||
_parent = std::make_unique<Ui::RpWidget>(_outer);
|
||||
_parent->show();
|
||||
|
||||
_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(event->pos())) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _parent->lifetime());
|
||||
|
||||
_selector = std::make_unique<HistoryView::Reactions::Selector>(
|
||||
_parent.get(),
|
||||
st::storiesReactionsPan,
|
||||
_show,
|
||||
std::move(reactions),
|
||||
TextWithEntities(),
|
||||
[=](bool fast) { hide(); },
|
||||
nullptr, // iconFactory
|
||||
nullptr, // paused
|
||||
true);
|
||||
|
||||
_selector->chosen(
|
||||
) | rpl::on_next([=](Chosen reaction) {
|
||||
if (reaction.id.custom() && !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
_chosen.fire(std::move(reaction));
|
||||
hide();
|
||||
}
|
||||
}, _selector->lifetime());
|
||||
|
||||
const auto desiredWidth = st::storiesReactionsWidth;
|
||||
const auto maxWidth = desiredWidth * 2;
|
||||
const auto width = _selector->countWidth(desiredWidth, maxWidth);
|
||||
const auto margins = _selector->marginsForShadow();
|
||||
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
|
||||
width);
|
||||
const auto full = margins.left() + width + margins.right();
|
||||
|
||||
_shownValue = 0.;
|
||||
rpl::combine(
|
||||
_fieldGeometry.value(),
|
||||
_shownValue.value(),
|
||||
_expanded.value()
|
||||
) | rpl::on_next([=](QRect field, float64 shown, bool expanded) {
|
||||
const auto width = margins.left()
|
||||
+ _selector->countAppearedWidth(shown)
|
||||
+ margins.right();
|
||||
const auto available = field.y();
|
||||
const auto min = st::storiesReactionsBottomSkip
|
||||
+ st::reactStripHeight;
|
||||
const auto max = min
|
||||
+ margins.top()
|
||||
+ categoriesTop
|
||||
+ st::storiesReactionsAddedTop;
|
||||
const auto height = expanded ? std::min(available, max) : min;
|
||||
const auto top = field.y() - height;
|
||||
const auto shift = (width / 2);
|
||||
const auto right = (field.x() + field.width() / 2 + shift);
|
||||
_parent->setGeometry(QRect((right - width), top, full, height));
|
||||
const auto innerTop = height
|
||||
- st::storiesReactionsBottomSkip
|
||||
- st::reactStripHeight;
|
||||
const auto maxAdded = innerTop - margins.top() - categoriesTop;
|
||||
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
|
||||
_selector->setSpecialExpandTopSkip(added);
|
||||
_selector->initGeometry(innerTop);
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->willExpand(
|
||||
) | rpl::on_next([=] {
|
||||
_expanded = true;
|
||||
|
||||
const auto raw = _parent.get();
|
||||
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(
|
||||
_parent->mapFromGlobal(event->globalPos()))) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->escapes() | rpl::on_next([=] {
|
||||
collapse();
|
||||
}, _selector->lifetime());
|
||||
}
|
||||
|
||||
void ReactionPanel::fadeOutSelector() {
|
||||
const auto geometry = Ui::MapFrom(
|
||||
_outer,
|
||||
_parent.get(),
|
||||
_selector->geometry());
|
||||
_hiding.push_back(std::make_unique<Hiding>(_outer));
|
||||
const auto raw = _hiding.back().get();
|
||||
raw->frame = Ui::GrabWidgetToImage(_selector.get());
|
||||
raw->widget.setGeometry(geometry);
|
||||
raw->widget.show();
|
||||
raw->widget.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
if (const auto opacity = raw->animation.value(0.)) {
|
||||
auto p = QPainter(&raw->widget);
|
||||
p.setOpacity(opacity);
|
||||
p.drawImage(0, 0, raw->frame);
|
||||
}
|
||||
}, raw->widget.lifetime());
|
||||
Ui::PostponeCall(&raw->widget, [=] {
|
||||
raw->animation.start([=] {
|
||||
if (raw->animation.animating()) {
|
||||
raw->widget.update();
|
||||
} else {
|
||||
const auto i = ranges::find(
|
||||
_hiding,
|
||||
raw,
|
||||
&std::unique_ptr<Hiding>::get);
|
||||
if (i != end(_hiding)) {
|
||||
_hiding.erase(i);
|
||||
}
|
||||
}
|
||||
}, 1., 0., st::slideWrapDuration);
|
||||
});
|
||||
}
|
||||
|
||||
void ReactionPanel::updateShowState() {
|
||||
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
|
||||
const auto opacity = 1.;
|
||||
const auto appearing = _showing.animating();
|
||||
const auto toggling = false;
|
||||
_shownValue = progress;
|
||||
_selector->updateShowState(progress, opacity, appearing, toggling);
|
||||
}
|
||||
|
||||
MessageField::MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer)
|
||||
: _parent(parent)
|
||||
, _show(std::move(show))
|
||||
, _wrap(std::make_unique<Ui::RpWidget>(_parent))
|
||||
, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {
|
||||
createControls(peer);
|
||||
}
|
||||
|
||||
MessageField::~MessageField() = default;
|
||||
|
||||
void MessageField::createControls(PeerData *peer) {
|
||||
setupBackground();
|
||||
|
||||
const auto &st = st::storiesComposeControls;
|
||||
_field = Ui::CreateChild<Ui::InputField>(
|
||||
_wrap.get(),
|
||||
st.field,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_message_ph());
|
||||
_field->setMaxLength(_limit + kErrorLimit);
|
||||
_field->setMinHeight(
|
||||
st::historySendSize.height() - 2 * st::historySendPadding);
|
||||
_field->setMaxHeight(st::historyComposeFieldMaxHeight);
|
||||
_field->setDocumentMargin(4.);
|
||||
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
|
||||
|
||||
_reactionPanel = std::make_unique<ReactionPanel>(
|
||||
_parent,
|
||||
_show,
|
||||
_wrap->geometryValue());
|
||||
_fieldFocused = _field->focusedChanges();
|
||||
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
|
||||
return field->getLastText().trimmed().isEmpty();
|
||||
});
|
||||
rpl::combine(
|
||||
_fieldFocused.value(),
|
||||
_fieldEmpty.value()
|
||||
) | rpl::on_next([=](bool focused, bool empty) {
|
||||
if (!focused) {
|
||||
_reactionPanel->hideIfCollapsed();
|
||||
} else if (empty) {
|
||||
_reactionPanel->show();
|
||||
} else {
|
||||
_reactionPanel->hide();
|
||||
}
|
||||
}, _field->lifetime());
|
||||
|
||||
_reactionPanel->chosen(
|
||||
) | rpl::on_next([=](Chosen reaction) {
|
||||
if (const auto customId = reaction.id.custom()) {
|
||||
const auto document = _show->session().data().document(customId);
|
||||
if (const auto sticker = document->sticker()) {
|
||||
if (const auto alt = sticker->alt; !alt.isEmpty()) {
|
||||
const auto length = int(alt.size());
|
||||
const auto data = Data::SerializeCustomEmojiId(customId);
|
||||
const auto tag = Ui::InputField::CustomEmojiLink(data);
|
||||
_submitted.fire({ alt, { { 0, length, tag } } });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_submitted.fire({ reaction.id.emoji() });
|
||||
}
|
||||
_reactionPanel->hide();
|
||||
}, _field->lifetime());
|
||||
|
||||
const auto show = _show;
|
||||
const auto allow = [=](not_null<DocumentData*> emoji) {
|
||||
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
InitMessageFieldHandlers({
|
||||
.session = &show->session(),
|
||||
.show = show,
|
||||
.field = _field,
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(ChatHelpers::PauseReason::Layer);
|
||||
},
|
||||
.allowPremiumEmoji = allow,
|
||||
.fieldStyle = &st.files.caption,
|
||||
.allowMarkdownTags = {
|
||||
Ui::InputField::kTagBold,
|
||||
Ui::InputField::kTagItalic,
|
||||
Ui::InputField::kTagUnderline,
|
||||
Ui::InputField::kTagStrikeOut,
|
||||
Ui::InputField::kTagSpoiler,
|
||||
},
|
||||
});
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
_parent,
|
||||
_field,
|
||||
&_show->session(),
|
||||
{
|
||||
.suggestCustomEmoji = true,
|
||||
.allowCustomWithoutPremium = allow,
|
||||
.st = &st.suggestions,
|
||||
});
|
||||
|
||||
_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);
|
||||
_send->show();
|
||||
|
||||
using Selector = ChatHelpers::TabbedSelector;
|
||||
_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(
|
||||
_parent,
|
||||
ChatHelpers::TabbedPanelDescriptor{
|
||||
.ownedSelector = object_ptr<Selector>(
|
||||
nullptr,
|
||||
ChatHelpers::TabbedSelectorDescriptor{
|
||||
.show = _show,
|
||||
.st = st.tabbed,
|
||||
.level = ChatHelpers::PauseReason::Layer,
|
||||
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
|
||||
.features = {
|
||||
.stickersSettings = false,
|
||||
.openStickerSets = false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const auto panel = _emojiPanel.get();
|
||||
panel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
panel->hide();
|
||||
panel->selector()->setCurrentPeer(peer);
|
||||
panel->selector()->emojiChosen(
|
||||
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
|
||||
Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
|
||||
}, lifetime());
|
||||
panel->selector()->customEmojiChosen(
|
||||
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
||||
const auto info = data.document->sticker();
|
||||
if (info
|
||||
&& info->setType == Data::StickersType::Emoji
|
||||
&& !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
Data::InsertCustomEmoji(_field, data.document);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);
|
||||
_emojiToggle->show();
|
||||
|
||||
_emojiToggle->installEventFilter(panel);
|
||||
_emojiToggle->addClickHandler([=] {
|
||||
panel->toggleAnimated();
|
||||
});
|
||||
|
||||
_width.value(
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_1 > 0
|
||||
) | rpl::on_next([=](int newWidth) {
|
||||
const auto fieldWidth = newWidth
|
||||
- st::historySendPadding
|
||||
- _emojiToggle->width()
|
||||
- _send->width();
|
||||
_field->resizeToWidth(fieldWidth);
|
||||
_field->moveToLeft(
|
||||
st::historySendPadding,
|
||||
st::historySendPadding,
|
||||
newWidth);
|
||||
updateWrapSize(newWidth);
|
||||
}, _lifetime);
|
||||
|
||||
rpl::combine(
|
||||
_width.value(),
|
||||
_field->heightValue()
|
||||
) | rpl::on_next([=](int width, int height) {
|
||||
if (width <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto minHeight = st::historySendSize.height()
|
||||
- 2 * st::historySendPadding;
|
||||
_send->moveToRight(0, height - minHeight, width);
|
||||
_emojiToggle->moveToRight(_send->width(), height - minHeight, width);
|
||||
updateWrapSize();
|
||||
}, _lifetime);
|
||||
|
||||
_field->cancelled() | rpl::on_next([=] {
|
||||
_closeRequests.fire({});
|
||||
}, _lifetime);
|
||||
|
||||
const auto updateLimitPosition = [=](QSize parent, QSize label) {
|
||||
const auto skip = st::historySendPadding;
|
||||
return QPoint(parent.width() - label.width() - skip, skip);
|
||||
};
|
||||
Ui::AddLengthLimitLabel(_field, _limit, {
|
||||
.customParent = _wrap.get(),
|
||||
.customUpdatePosition = updateLimitPosition,
|
||||
});
|
||||
|
||||
rpl::merge(
|
||||
_field->submits() | rpl::to_empty,
|
||||
_send->clicks() | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
auto text = _field->getTextWithTags();
|
||||
if (text.text.size() <= _limit) {
|
||||
_submitted.fire(std::move(text));
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::updateEmojiPanelGeometry() {
|
||||
const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
|
||||
const auto local = _parent->mapFromGlobal(global);
|
||||
_emojiPanel->moveBottomRight(
|
||||
local.y(),
|
||||
local.x() + _emojiToggle->width() * 3);
|
||||
}
|
||||
|
||||
void MessageField::setupBackground() {
|
||||
_wrap->paintRequest() | rpl::on_next([=] {
|
||||
const auto radius = st::historySendSize.height() / 2.;
|
||||
auto p = QPainter(_wrap.get());
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::storiesComposeBg);
|
||||
p.drawRoundedRect(_wrap->rect(), radius, radius);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::resizeToWidth(int newWidth) {
|
||||
_width = newWidth;
|
||||
if (_wrap->isHidden()) {
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
}
|
||||
updateEmojiPanelGeometry();
|
||||
}
|
||||
|
||||
void MessageField::move(int x, int y) {
|
||||
_wrap->move(x, y);
|
||||
if (_cache) {
|
||||
_cache->move(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::toggle(bool shown) {
|
||||
if (_shown == shown) {
|
||||
return;
|
||||
} else if (shown) {
|
||||
Assert(_width.current() > 0);
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
} else if (Ui::InFocusChain(_field)) {
|
||||
_parent->setFocus();
|
||||
}
|
||||
_shown = shown;
|
||||
if (!anim::Disabled()) {
|
||||
if (!_cache) {
|
||||
auto image = Ui::GrabWidgetToImage(_wrap.get());
|
||||
_cache = std::make_unique<Ui::RpWidget>(_parent);
|
||||
const auto raw = _cache.get();
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto scale = raw->height() / float64(_wrap->height());
|
||||
const auto target = _wrap->rect();
|
||||
const auto center = target.center();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
p.drawImage(target, image);
|
||||
}, raw->lifetime());
|
||||
raw->show();
|
||||
raw->move(_wrap->pos());
|
||||
raw->resize(_wrap->width(), 0);
|
||||
|
||||
_wrap->hide();
|
||||
}
|
||||
_shownAnimation.start(
|
||||
[=] { shownAnimationCallback(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration,
|
||||
anim::easeOutCirc);
|
||||
}
|
||||
shownAnimationCallback();
|
||||
}
|
||||
|
||||
void MessageField::raise() {
|
||||
_wrap->raise();
|
||||
if (_cache) {
|
||||
_cache->raise();
|
||||
}
|
||||
if (_reactionPanel) {
|
||||
_reactionPanel->raise();
|
||||
}
|
||||
if (_emojiPanel) {
|
||||
_emojiPanel->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::updateWrapSize(int widthOverride) {
|
||||
const auto width = widthOverride ? widthOverride : _wrap->width();
|
||||
const auto height = _field->height() + 2 * st::historySendPadding;
|
||||
_wrap->resize(width, height);
|
||||
updateHeight();
|
||||
}
|
||||
|
||||
void MessageField::updateHeight() {
|
||||
_height = int(base::SafeRound(
|
||||
_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));
|
||||
}
|
||||
|
||||
void MessageField::shownAnimationCallback() {
|
||||
updateHeight();
|
||||
if (_shownAnimation.animating()) {
|
||||
Assert(_cache != nullptr);
|
||||
_cache->resize(_cache->width(), _height.current());
|
||||
_cache->update();
|
||||
} else if (_shown) {
|
||||
_cache = nullptr;
|
||||
_wrap->show();
|
||||
_field->setFocusFast();
|
||||
} else {
|
||||
_closed.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
int MessageField::height() const {
|
||||
return _height.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> MessageField::heightValue() const {
|
||||
return _height.value();
|
||||
}
|
||||
|
||||
rpl::producer<TextWithTags> MessageField::submitted() const {
|
||||
return _submitted.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closeRequests() const {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closed() const {
|
||||
return _closed.events();
|
||||
}
|
||||
|
||||
rpl::lifetime &MessageField::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class ReactionPanel;
|
||||
|
||||
class MessageField final {
|
||||
public:
|
||||
MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer);
|
||||
~MessageField();
|
||||
|
||||
void resizeToWidth(int newWidth);
|
||||
void move(int x, int y);
|
||||
void toggle(bool shown);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<TextWithTags> submitted() const;
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> closed() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void createControls(PeerData *peer);
|
||||
void setupBackground();
|
||||
void shownAnimationCallback();
|
||||
void updateEmojiPanelGeometry();
|
||||
void updateWrapSize(int widthOverride = 0);
|
||||
void updateHeight();
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const std::unique_ptr<Ui::RpWidget> _wrap;
|
||||
|
||||
int _limit = 0;
|
||||
Ui::InputField *_field = nullptr;
|
||||
Ui::SendButton *_send = nullptr;
|
||||
Ui::EmojiButton *_emojiToggle = nullptr;
|
||||
std::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
std::unique_ptr<ReactionPanel> _reactionPanel;
|
||||
rpl::variable<bool> _fieldFocused;
|
||||
rpl::variable<bool> _fieldEmpty = true;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
rpl::variable<int> _height;
|
||||
|
||||
bool _shown = false;
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
std::unique_ptr<Ui::RpWidget> _cache;
|
||||
|
||||
rpl::event_stream<TextWithTags> _submitted;
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::event_stream<> _closed;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
731
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
731
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
@@ -0,0 +1,731 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_blocked_peers.h"
|
||||
#include "api/api_chat_participants.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "calls/group/ui/calls_group_stars_coloring.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_message_encryption.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxShownVideoStreamMessages = 100;
|
||||
constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] StarsTop ParseStarsTop(
|
||||
not_null<Data::Session*> owner,
|
||||
const MTPphone_GroupCallStars &stars) {
|
||||
const auto &data = stars.data();
|
||||
const auto &list = data.vtop_donors().v;
|
||||
auto result = StarsTop{ .total = int(data.vtotal_stars().v) };
|
||||
result.topDonors.reserve(list.size());
|
||||
for (const auto &entry : list) {
|
||||
const auto &fields = entry.data();
|
||||
result.topDonors.push_back({
|
||||
.peer = (fields.vpeer_id()
|
||||
? owner->peer(peerFromMTP(*fields.vpeer_id())).get()
|
||||
: nullptr),
|
||||
.stars = int(fields.vstars().v),
|
||||
.my = fields.is_my(),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId PinFinishDate(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId date,
|
||||
int stars) {
|
||||
if (!date || !stars) {
|
||||
return 0;
|
||||
}
|
||||
const auto &colorings = peer->session().appConfig().groupCallColorings();
|
||||
return date + Ui::StarsColoringForCount(colorings, stars).secondsPin;
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId PinFinishDate(const Message &message) {
|
||||
return PinFinishDate(message.peer, message.date, message.stars);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
|
||||
: _call(call)
|
||||
, _session(&call->peer()->session())
|
||||
, _api(api)
|
||||
, _destroyTimer([=] { checkDestroying(); })
|
||||
, _ttl(_session->appConfig().groupCallMessageTTL())
|
||||
, _starsStatsTimer([=] { requestStarsStats(); }) {
|
||||
Ui::PostponeCall(_call, [=] {
|
||||
_call->real(
|
||||
) | rpl::on_next([=](not_null<Data::GroupCall*> call) {
|
||||
_real = call;
|
||||
if (ready()) {
|
||||
sendPending();
|
||||
} else {
|
||||
Unexpected("Not ready call.");
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
requestStarsStats();
|
||||
});
|
||||
}
|
||||
|
||||
Messages::~Messages() {
|
||||
if (_paid.sending > 0) {
|
||||
finishPaidSending({
|
||||
.count = int(_paid.sending),
|
||||
.valid = true,
|
||||
.shownPeer = _paid.sendingShownPeer,
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::requestStarsStats() {
|
||||
if (!_call->videoStream()) {
|
||||
return;
|
||||
}
|
||||
_starsStatsTimer.cancel();
|
||||
_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(
|
||||
_call->inputCall()
|
||||
)).done([=](const MTPphone_GroupCallStars &result) {
|
||||
const auto &data = result.data();
|
||||
|
||||
const auto owner = &_session->data();
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
|
||||
_paid.top = ParseStarsTop(owner, result);
|
||||
_paidChanges.fire({});
|
||||
|
||||
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
[[maybe_unused]] const auto &type = error.type();
|
||||
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
|
||||
}).send();
|
||||
|
||||
}
|
||||
|
||||
bool Messages::ready() const {
|
||||
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
|
||||
}
|
||||
|
||||
void Messages::send(TextWithTags text, int stars) {
|
||||
if (text.empty() && !stars) {
|
||||
return;
|
||||
} else if (!ready()) {
|
||||
_pending.push_back({ std::move(text), stars });
|
||||
return;
|
||||
}
|
||||
|
||||
auto prepared = TextWithEntities{
|
||||
text.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(text.tags)
|
||||
};
|
||||
auto serialized = MTPTextWithEntities(MTP_textWithEntities(
|
||||
MTP_string(prepared.text),
|
||||
Api::EntitiesToMTP(
|
||||
&_real->session(),
|
||||
prepared.entities,
|
||||
Api::ConvertOption::SkipLocal)));
|
||||
|
||||
const auto localId = _call->peer()->owner().nextLocalMessageId();
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
_sendingIdByRandomId.emplace(randomId, localId);
|
||||
|
||||
const auto from = _call->messagesFrom();
|
||||
const auto creator = _real->creator();
|
||||
const auto skip = skipMessage(prepared, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(localId);
|
||||
} else {
|
||||
_messages.push_back({
|
||||
.id = localId,
|
||||
.peer = from,
|
||||
.text = std::move(prepared),
|
||||
.stars = stars,
|
||||
.admin = (from == _call->peer()) || (creator && from->isSelf()),
|
||||
.mine = true,
|
||||
});
|
||||
}
|
||||
if (!_call->conference()) {
|
||||
using Flag = MTPphone_SendGroupCallMessage::Flag;
|
||||
_api->request(MTPphone_SendGroupCallMessage(
|
||||
MTP_flags(Flag::f_send_as
|
||||
| (stars ? Flag::f_allow_paid_stars : Flag())),
|
||||
_call->inputCall(),
|
||||
MTP_long(randomId),
|
||||
serialized,
|
||||
MTP_long(stars),
|
||||
from->input()
|
||||
)).done([=](
|
||||
const MTPUpdates &result,
|
||||
const MTP::Response &response) {
|
||||
_session->api().applyUpdates(result, randomId);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
} else {
|
||||
const auto bytes = SerializeMessage({ randomId, serialized });
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(from->id).bare;
|
||||
const auto encrypt = _call->e2eEncryptDecrypt();
|
||||
const auto encrypted = encrypt(v, int64_t(userId), true, 0);
|
||||
|
||||
_api->request(MTPphone_SendGroupCallEncryptedMessage(
|
||||
_call->inputCall(),
|
||||
MTP_bytes(bytes::make_span(encrypted))
|
||||
)).done([=](const MTPBool &, const MTP::Response &response) {
|
||||
sent(randomId, response);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
}
|
||||
|
||||
addStars(from, stars, true);
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::setApplyingInitial(bool value) {
|
||||
_applyingInitial = value;
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
const auto &fields = data.vmessage().data();
|
||||
received(
|
||||
fields.vid().v,
|
||||
fields.vfrom_id(),
|
||||
fields.vmessage(),
|
||||
fields.vdate().v,
|
||||
fields.vpaid_message_stars().value_or_empty(),
|
||||
fields.is_from_admin());
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
const auto fromId = data.vfrom_id();
|
||||
const auto &bytes = data.vencrypted_message().v;
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(peerFromMTP(fromId)).bare;
|
||||
const auto decrypt = _call->e2eEncryptDecrypt();
|
||||
const auto decrypted = decrypt(v, int64_t(userId), false, 0);
|
||||
|
||||
const auto deserialized = DeserializeMessage(QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(decrypted.data()),
|
||||
decrypted.size()));
|
||||
if (!deserialized) {
|
||||
LOG(("API Error: Can't parse decrypted message"));
|
||||
return;
|
||||
}
|
||||
const auto realId = ++_conferenceIdAutoIncrement;
|
||||
const auto randomId = deserialized->randomId;
|
||||
if (!_conferenceIdByRandomId.emplace(randomId, realId).second) {
|
||||
// Already received.
|
||||
return;
|
||||
}
|
||||
received(
|
||||
realId,
|
||||
fromId,
|
||||
deserialized->message,
|
||||
base::unixtime::now(), // date
|
||||
0, // stars
|
||||
false,
|
||||
true); // checkCustomEmoji
|
||||
}
|
||||
|
||||
void Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {
|
||||
const auto was = _messages.size();
|
||||
for (const auto &id : data.vmessages().v) {
|
||||
const auto i = ranges::find(_messages, id.v, &Message::id);
|
||||
if (i != end(_messages)) {
|
||||
_messages.erase(i);
|
||||
}
|
||||
}
|
||||
if (_messages.size() < was) {
|
||||
pushChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::sent(const MTPDupdateMessageID &data) {
|
||||
sent(data.vrandom_id().v, data.vid().v);
|
||||
}
|
||||
|
||||
void Messages::sent(uint64 randomId, const MTP::Response &response) {
|
||||
const auto realId = ++_conferenceIdAutoIncrement;
|
||||
_conferenceIdByRandomId.emplace(randomId, realId);
|
||||
sent(randomId, realId);
|
||||
|
||||
const auto i = ranges::find(_messages, realId, &Message::id);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::sent(uint64 randomId, MsgId realId) {
|
||||
const auto i = _sendingIdByRandomId.find(randomId);
|
||||
if (i == end(_sendingIdByRandomId)) {
|
||||
return;
|
||||
}
|
||||
const auto localId = i->second;
|
||||
_sendingIdByRandomId.erase(i);
|
||||
|
||||
const auto j = ranges::find(_messages, localId, &Message::id);
|
||||
if (j == end(_messages)) {
|
||||
_skippedIds.emplace(realId);
|
||||
return;
|
||||
}
|
||||
j->id = realId;
|
||||
crl::on_main(this, [=] {
|
||||
const auto i = ranges::find(_messages, realId, &Message::id);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = base::unixtime::now();
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
});
|
||||
_idUpdates.fire({ .localId = localId, .realId = realId });
|
||||
}
|
||||
|
||||
void Messages::received(
|
||||
MsgId id,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji) {
|
||||
const auto peer = _call->peer();
|
||||
const auto i = ranges::find(_messages, id, &Message::id);
|
||||
if (i != end(_messages)) {
|
||||
const auto fromId = peerFromMTP(from);
|
||||
const auto me1 = peer->session().userPeerId();
|
||||
const auto me2 = _call->messagesFrom()->id;
|
||||
if (((fromId == me1) || (fromId == me2)) && !i->date) {
|
||||
i->date = date;
|
||||
i->pinFinishDate = PinFinishDate(*i);
|
||||
checkDestroying(true);
|
||||
}
|
||||
return;
|
||||
} else if (_skippedIds.contains(id)) {
|
||||
return;
|
||||
}
|
||||
auto allowedEntityTypes = std::vector<EntityType>{
|
||||
EntityType::Code,
|
||||
EntityType::Bold,
|
||||
EntityType::Semibold,
|
||||
EntityType::Spoiler,
|
||||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::CustomEmoji,
|
||||
};
|
||||
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
|
||||
allowedEntityTypes.pop_back();
|
||||
}
|
||||
const auto author = peer->owner().peer(peerFromMTP(from));
|
||||
|
||||
auto text = Ui::Text::Filtered(
|
||||
Api::ParseTextWithEntities(&author->session(), message),
|
||||
allowedEntityTypes);
|
||||
const auto mine = author->isSelf()
|
||||
|| (author->isChannel() && author->asChannel()->amCreator());
|
||||
const auto skip = skipMessage(text, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(id);
|
||||
} else {
|
||||
// Should check by sendAsPeers() list instead, but it may not be
|
||||
// loaded here yet.
|
||||
_messages.push_back({
|
||||
.id = id,
|
||||
.date = date,
|
||||
.pinFinishDate = PinFinishDate(author, date, stars),
|
||||
.peer = author,
|
||||
.text = std::move(text),
|
||||
.stars = stars,
|
||||
.admin = fromAdmin,
|
||||
.mine = mine,
|
||||
});
|
||||
ranges::sort(_messages, ranges::less(), &Message::id);
|
||||
}
|
||||
if (!_applyingInitial) {
|
||||
addStars(author, stars, mine);
|
||||
}
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool Messages::skipMessage(const TextWithEntities &text, int stars) const {
|
||||
const auto real = _call->lookupReal();
|
||||
return text.empty()
|
||||
&& real
|
||||
&& (stars < real->messagesMinPrice());
|
||||
}
|
||||
|
||||
void Messages::checkDestroying(bool afterChanges) {
|
||||
auto next = TimeId();
|
||||
const auto now = base::unixtime::now();
|
||||
const auto initial = int(_messages.size());
|
||||
if (_call->videoStream()) {
|
||||
if (initial > kMaxShownVideoStreamMessages) {
|
||||
const auto remove = initial - kMaxShownVideoStreamMessages;
|
||||
auto i = begin(_messages);
|
||||
for (auto k = 0; k != remove; ++k) {
|
||||
if (i->date && i->pinFinishDate <= now) {
|
||||
i = _messages.erase(i);
|
||||
} else if (!next || next > i->pinFinishDate - now) {
|
||||
next = i->pinFinishDate - now;
|
||||
++i;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else for (auto i = begin(_messages); i != end(_messages);) {
|
||||
const auto date = i->date;
|
||||
//const auto ttl = i->stars
|
||||
// ? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)
|
||||
// : _ttl;
|
||||
const auto ttl = _ttl;
|
||||
if (!date) {
|
||||
if (i->id < 0) {
|
||||
++i;
|
||||
} else {
|
||||
i = _messages.erase(i);
|
||||
}
|
||||
} else if (date + ttl <= now) {
|
||||
i = _messages.erase(i);
|
||||
} else if (!next || next > date + ttl - now) {
|
||||
next = date + ttl - now;
|
||||
++i;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
_destroyTimer.cancel();
|
||||
} else {
|
||||
const auto delay = next * crl::time(1000);
|
||||
if (!_destroyTimer.isActive()
|
||||
|| (_destroyTimer.remainingTime() > delay)) {
|
||||
_destroyTimer.callOnce(delay);
|
||||
}
|
||||
}
|
||||
if (afterChanges || (_messages.size() < initial)) {
|
||||
pushChanges();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<Message>> Messages::listValue() const {
|
||||
return _changes.events_starting_with_copy(_messages);
|
||||
}
|
||||
|
||||
rpl::producer<MessageIdUpdate> Messages::idUpdates() const {
|
||||
return _idUpdates.events();
|
||||
}
|
||||
|
||||
void Messages::sendPending() {
|
||||
Expects(_real != nullptr);
|
||||
|
||||
for (auto &pending : base::take(_pending)) {
|
||||
send(std::move(pending.text), pending.stars);
|
||||
}
|
||||
if (_paidSendingPending) {
|
||||
reactionsPaidSend();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::pushChanges() {
|
||||
if (_changesScheduled) {
|
||||
return;
|
||||
}
|
||||
_changesScheduled = true;
|
||||
Ui::PostponeCall(this, [=] {
|
||||
_changesScheduled = false;
|
||||
_changes.fire_copy(_messages);
|
||||
});
|
||||
}
|
||||
|
||||
void Messages::failed(uint64 randomId, const MTP::Response &response) {
|
||||
const auto i = _sendingIdByRandomId.find(randomId);
|
||||
if (i == end(_sendingIdByRandomId)) {
|
||||
return;
|
||||
}
|
||||
const auto localId = i->second;
|
||||
_sendingIdByRandomId.erase(i);
|
||||
|
||||
const auto j = ranges::find(_messages, localId, &Message::id);
|
||||
if (j != end(_messages) && !j->date) {
|
||||
j->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
j->stars = 0;
|
||||
j->failed = true;
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
int Messages::reactionsPaidScheduled() const {
|
||||
return _paid.scheduled;
|
||||
}
|
||||
|
||||
PeerId Messages::reactionsLocalShownPeer() const {
|
||||
const auto minePaidShownPeer = [&] {
|
||||
for (const auto &entry : _paid.top.topDonors) {
|
||||
if (entry.my) {
|
||||
return entry.peer ? entry.peer->id : PeerId();
|
||||
}
|
||||
}
|
||||
return _call->messagesFrom()->id;
|
||||
//const auto api = &_session->api();
|
||||
//return api->globalPrivacy().paidReactionShownPeerCurrent();
|
||||
};
|
||||
return _paid.scheduledFlag
|
||||
? _paid.scheduledShownPeer
|
||||
: _paid.sendingFlag
|
||||
? _paid.sendingShownPeer
|
||||
: minePaidShownPeer();
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidAdd(int count) {
|
||||
Expects(count >= 0);
|
||||
|
||||
_paid.scheduled += count;
|
||||
_paid.scheduledFlag = 1;
|
||||
_paid.scheduledShownPeer = _call->messagesFrom()->id;
|
||||
if (count > 0) {
|
||||
_session->credits().lock(CreditsAmount(count));
|
||||
}
|
||||
_call->peer()->owner().reactions().schedulePaid(_call);
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidScheduledCancel() {
|
||||
if (!_paid.scheduledFlag) {
|
||||
return;
|
||||
}
|
||||
if (const auto amount = int(_paid.scheduled)) {
|
||||
_session->credits().unlock(
|
||||
CreditsAmount(amount));
|
||||
}
|
||||
_paid.scheduled = 0;
|
||||
_paid.scheduledFlag = 0;
|
||||
_paid.scheduledShownPeer = 0;
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
|
||||
Data::PaidReactionSend Messages::startPaidReactionSending() {
|
||||
_paidSendingPending = false;
|
||||
if (!_paid.scheduledFlag || !_paid.scheduled) {
|
||||
return {};
|
||||
} else if (_paid.sendingFlag || !ready()) {
|
||||
_paidSendingPending = true;
|
||||
return {};
|
||||
}
|
||||
_paid.sending = _paid.scheduled;
|
||||
_paid.sendingFlag = _paid.scheduledFlag;
|
||||
_paid.sendingShownPeer = _paid.scheduledShownPeer;
|
||||
_paid.scheduled = 0;
|
||||
_paid.scheduledFlag = 0;
|
||||
_paid.scheduledShownPeer = 0;
|
||||
return {
|
||||
.count = int(_paid.sending),
|
||||
.valid = true,
|
||||
.shownPeer = _paid.sendingShownPeer,
|
||||
};
|
||||
}
|
||||
|
||||
void Messages::finishPaidSending(
|
||||
Data::PaidReactionSend send,
|
||||
bool success) {
|
||||
Expects(send.count == _paid.sending);
|
||||
Expects(send.valid == (_paid.sendingFlag == 1));
|
||||
Expects(send.shownPeer == _paid.sendingShownPeer);
|
||||
|
||||
_paid.sending = 0;
|
||||
_paid.sendingFlag = 0;
|
||||
_paid.sendingShownPeer = 0;
|
||||
if (const auto amount = send.count) {
|
||||
if (success) {
|
||||
const auto from = _session->data().peer(*send.shownPeer);
|
||||
_session->credits().withdrawLocked(CreditsAmount(amount));
|
||||
|
||||
auto &donors = _paid.top.topDonors;
|
||||
const auto i = ranges::find(donors, true, &StarsDonor::my);
|
||||
if (i != end(donors)) {
|
||||
i->peer = from;
|
||||
i->stars += amount;
|
||||
} else {
|
||||
donors.push_back({
|
||||
.peer = from,
|
||||
.stars = amount,
|
||||
.my = true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_session->credits().unlock(CreditsAmount(amount));
|
||||
_paidChanges.fire({});
|
||||
}
|
||||
}
|
||||
if (_paidSendingPending) {
|
||||
reactionsPaidSend();
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::reactionsPaidSend() {
|
||||
const auto send = startPaidReactionSending();
|
||||
if (!send.valid || !send.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto localId = _call->peer()->owner().nextLocalMessageId();
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
_sendingIdByRandomId.emplace(randomId, localId);
|
||||
|
||||
const auto from = _session->data().peer(*send.shownPeer);
|
||||
const auto stars = int(send.count);
|
||||
const auto skip = skipMessage({}, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(localId);
|
||||
} else {
|
||||
_messages.push_back({
|
||||
.id = localId,
|
||||
.peer = from,
|
||||
.stars = stars,
|
||||
.admin = (from == _call->peer()),
|
||||
.mine = true,
|
||||
});
|
||||
}
|
||||
using Flag = MTPphone_SendGroupCallMessage::Flag;
|
||||
_api->request(MTPphone_SendGroupCallMessage(
|
||||
MTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),
|
||||
_call->inputCall(),
|
||||
MTP_long(randomId),
|
||||
MTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),
|
||||
MTP_long(stars),
|
||||
from->input()
|
||||
)).done([=](
|
||||
const MTPUpdates &result,
|
||||
const MTP::Response &response) {
|
||||
finishPaidSending(send, true);
|
||||
_session->api().applyUpdates(result, randomId);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
finishPaidSending(send, false);
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
|
||||
addStars(from, stars, true);
|
||||
if (!skip) {
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::undoScheduledPaidOnDestroy() {
|
||||
_call->peer()->owner().reactions().undoScheduledPaid(_call);
|
||||
}
|
||||
|
||||
Messages::PaidLocalState Messages::starsLocalState() const {
|
||||
const auto &donors = _paid.top.topDonors;
|
||||
const auto i = ranges::find(donors, true, &StarsDonor::my);
|
||||
const auto local = int(_paid.scheduled);
|
||||
const auto my = (i != end(donors) ? i->stars : 0) + local;
|
||||
const auto total = _paid.top.total + local;
|
||||
return { .total = total, .my = my };
|
||||
}
|
||||
|
||||
void Messages::deleteConfirmed(MessageDeleteRequest request) {
|
||||
const auto eraseFrom = [&](auto iterator) {
|
||||
if (iterator != end(_messages)) {
|
||||
_messages.erase(iterator, end(_messages));
|
||||
pushChanges();
|
||||
}
|
||||
};
|
||||
const auto peer = _call->peer();
|
||||
if (const auto from = request.deleteAllFrom) {
|
||||
using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;
|
||||
_api->request(MTPphone_DeleteGroupCallParticipantMessages(
|
||||
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
|
||||
_call->inputCall(),
|
||||
from->input()
|
||||
)).send();
|
||||
eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));
|
||||
} else {
|
||||
using Flag = MTPphone_DeleteGroupCallMessages::Flag;
|
||||
_api->request(MTPphone_DeleteGroupCallMessages(
|
||||
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
|
||||
_call->inputCall(),
|
||||
MTP_vector<MTPint>(1, MTP_int(request.id.bare))
|
||||
)).send();
|
||||
eraseFrom(ranges::remove(_messages, request.id, &Message::id));
|
||||
}
|
||||
if (const auto ban = request.ban) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
ban->session().api().chatParticipants().kick(
|
||||
channel,
|
||||
ban,
|
||||
ChatRestrictionsInfo());
|
||||
} else {
|
||||
ban->session().api().blockedPeers().block(ban);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {
|
||||
if (stars <= 0) {
|
||||
return;
|
||||
}
|
||||
_paid.top.total += stars;
|
||||
const auto i = ranges::find(
|
||||
_paid.top.topDonors,
|
||||
from.get(),
|
||||
&StarsDonor::peer);
|
||||
if (i != end(_paid.top.topDonors)) {
|
||||
i->stars += stars;
|
||||
} else {
|
||||
_paid.top.topDonors.push_back({
|
||||
.peer = from,
|
||||
.stars = stars,
|
||||
.my = mine,
|
||||
});
|
||||
}
|
||||
ranges::stable_sort(
|
||||
_paid.top.topDonors,
|
||||
ranges::greater(),
|
||||
&StarsDonor::stars);
|
||||
_paidChanges.fire({ .peer = from, .stars = stars });
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
199
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
199
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
struct PaidReactionSend;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace MTP {
|
||||
class Sender;
|
||||
struct Response;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message {
|
||||
MsgId id = 0;
|
||||
TimeId date = 0;
|
||||
TimeId pinFinishDate = 0;
|
||||
not_null<PeerData*> peer;
|
||||
TextWithEntities text;
|
||||
int stars = 0;
|
||||
bool failed = false;
|
||||
bool admin = false;
|
||||
bool mine = false;
|
||||
};
|
||||
|
||||
struct MessageIdUpdate {
|
||||
MsgId localId = 0;
|
||||
MsgId realId = 0;
|
||||
};
|
||||
|
||||
struct MessageDeleteRequest {
|
||||
MsgId id = 0;
|
||||
PeerData *deleteAllFrom = nullptr;
|
||||
PeerData *ban = nullptr;
|
||||
bool reportSpam = false;
|
||||
};
|
||||
|
||||
struct StarsDonor {
|
||||
PeerData *peer = nullptr;
|
||||
int stars = 0;
|
||||
bool my = false;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StarsDonor &,
|
||||
const StarsDonor &) = default;
|
||||
};
|
||||
|
||||
struct StarsTop {
|
||||
std::vector<StarsDonor> topDonors;
|
||||
int total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const StarsTop &,
|
||||
const StarsTop &) = default;
|
||||
};
|
||||
|
||||
class Messages final : public base::has_weak_ptr {
|
||||
public:
|
||||
Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);
|
||||
~Messages();
|
||||
|
||||
void send(TextWithTags text, int stars);
|
||||
|
||||
void setApplyingInitial(bool value);
|
||||
void received(const MTPDupdateGroupCallMessage &data);
|
||||
void received(const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
void deleted(const MTPDupdateDeleteGroupCallMessages &data);
|
||||
void sent(const MTPDupdateMessageID &data);
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;
|
||||
[[nodiscard]] rpl::producer<MessageIdUpdate> idUpdates() const;
|
||||
|
||||
[[nodiscard]] int reactionsPaidScheduled() const;
|
||||
[[nodiscard]] PeerId reactionsLocalShownPeer() const;
|
||||
void reactionsPaidAdd(int count);
|
||||
void reactionsPaidScheduledCancel();
|
||||
void reactionsPaidSend();
|
||||
void undoScheduledPaidOnDestroy();
|
||||
|
||||
struct PaidLocalState {
|
||||
int total = 0;
|
||||
int my = 0;
|
||||
};
|
||||
[[nodiscard]] PaidLocalState starsLocalState() const;
|
||||
[[nodiscard]] rpl::producer<StarsDonor> starsValueChanges() const {
|
||||
return _paidChanges.events();
|
||||
}
|
||||
[[nodiscard]] const StarsTop &starsTop() const {
|
||||
return _paid.top;
|
||||
}
|
||||
|
||||
void requestHiddenShow() {
|
||||
_hiddenShowRequests.fire({});
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> hiddenShowRequested() const {
|
||||
return _hiddenShowRequests.events();
|
||||
}
|
||||
|
||||
void deleteConfirmed(MessageDeleteRequest request);
|
||||
|
||||
private:
|
||||
struct Pending {
|
||||
TextWithTags text;
|
||||
int stars = 0;
|
||||
};
|
||||
struct Paid {
|
||||
StarsTop top;
|
||||
PeerId scheduledShownPeer = 0;
|
||||
PeerId sendingShownPeer = 0;
|
||||
uint32 scheduled : 30 = 0;
|
||||
uint32 scheduledFlag : 1 = 0;
|
||||
uint32 scheduledPrivacySet : 1 = 0;
|
||||
uint32 sending : 30 = 0;
|
||||
uint32 sendingFlag : 1 = 0;
|
||||
uint32 sendingPrivacySet : 1 = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool ready() const;
|
||||
void sendPending();
|
||||
void pushChanges();
|
||||
void checkDestroying(bool afterChanges = false);
|
||||
|
||||
void received(
|
||||
MsgId id,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji = false);
|
||||
void sent(uint64 randomId, const MTP::Response &response);
|
||||
void sent(uint64 randomId, MsgId realId);
|
||||
void failed(uint64 randomId, const MTP::Response &response);
|
||||
|
||||
[[nodiscard]] bool skipMessage(
|
||||
const TextWithEntities &text,
|
||||
int stars) const;
|
||||
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
|
||||
void finishPaidSending(
|
||||
Data::PaidReactionSend send,
|
||||
bool success);
|
||||
void addStars(not_null<PeerData*> from, int stars, bool mine);
|
||||
void requestStarsStats();
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<MTP::Sender*> _api;
|
||||
|
||||
MsgId _conferenceIdAutoIncrement = 0;
|
||||
base::flat_map<uint64, MsgId> _conferenceIdByRandomId;
|
||||
|
||||
base::flat_map<uint64, MsgId> _sendingIdByRandomId;
|
||||
|
||||
Data::GroupCall *_real = nullptr;
|
||||
|
||||
std::vector<Pending> _pending;
|
||||
|
||||
base::Timer _destroyTimer;
|
||||
std::vector<Message> _messages;
|
||||
base::flat_set<MsgId> _skippedIds;
|
||||
rpl::event_stream<std::vector<Message>> _changes;
|
||||
rpl::event_stream<MessageIdUpdate> _idUpdates;
|
||||
bool _applyingInitial = false;
|
||||
|
||||
mtpRequestId _starsTopRequestId = 0;
|
||||
Paid _paid;
|
||||
rpl::event_stream<StarsDonor> _paidChanges;
|
||||
bool _paidSendingPending = false;
|
||||
|
||||
TimeId _ttl = 0;
|
||||
bool _changesScheduled = false;
|
||||
|
||||
rpl::event_stream<> _hiddenShowRequests;
|
||||
base::Timer _starsStatsTimer;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
1913
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
1913
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
File diff suppressed because it is too large
Load Diff
202
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
202
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
#include "ui/round_rect.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class ElasticScroll;
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class PopupMenu;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
struct StarsColoring;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message;
|
||||
struct MessageIdUpdate;
|
||||
struct MessageDeleteRequest;
|
||||
|
||||
enum class MessagesMode {
|
||||
GroupCall,
|
||||
VideoStream,
|
||||
};
|
||||
|
||||
class MessagesUi final {
|
||||
public:
|
||||
MessagesUi(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
MessagesMode mode,
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,
|
||||
rpl::producer<MessageIdUpdate> idUpdates,
|
||||
rpl::producer<bool> canManageValue,
|
||||
rpl::producer<bool> shown);
|
||||
~MessagesUi();
|
||||
|
||||
void move(int left, int bottom, int width, int availableHeight);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] rpl::producer<> hiddenShowRequested() const;
|
||||
[[nodiscard]] rpl::producer<MessageDeleteRequest> deleteRequests() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct MessageView;
|
||||
struct PinnedView;
|
||||
struct PayedBg {
|
||||
explicit PayedBg(const Ui::StarsColoring &coloring);
|
||||
|
||||
style::owned_color light;
|
||||
style::owned_color dark;
|
||||
Ui::RoundRect pinnedLight;
|
||||
Ui::RoundRect pinnedDark;
|
||||
Ui::RoundRect messageLight;
|
||||
Ui::RoundRect priceDark;
|
||||
Ui::RoundRect badgeDark;
|
||||
};
|
||||
|
||||
void setupBadges();
|
||||
void setupList(
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown);
|
||||
void showList(const std::vector<Message> &list);
|
||||
void handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates);
|
||||
void toggleMessage(MessageView &entry, bool shown);
|
||||
void setContentFailed(MessageView &entry);
|
||||
void setContent(MessageView &entry);
|
||||
void setContent(PinnedView &entry);
|
||||
void updateMessageSize(MessageView &entry);
|
||||
bool updateMessageHeight(MessageView &entry);
|
||||
void updatePinnedSize(PinnedView &entry);
|
||||
bool updatePinnedWidth(PinnedView &entry);
|
||||
void animateMessageSent(MessageView &entry);
|
||||
void repaintMessage(MsgId id);
|
||||
void highlightMessage(MsgId id);
|
||||
void startHighlight(MsgId id);
|
||||
void recountHeights(std::vector<MessageView>::iterator i, int top);
|
||||
void appendMessage(const Message &data);
|
||||
void checkReactionContent(
|
||||
MessageView &entry,
|
||||
const TextWithEntities &text);
|
||||
void startReactionAnimation(MessageView &entry);
|
||||
void updateReactionPosition(MessageView &entry);
|
||||
void removeReaction(not_null<Ui::RpWidget*> widget);
|
||||
void setupMessagesWidget();
|
||||
|
||||
void togglePinned(PinnedView &entry, bool shown);
|
||||
void repaintPinned(MsgId id);
|
||||
void recountWidths(std::vector<PinnedView>::iterator i, int left);
|
||||
void appendPinned(const Message &data, TimeId now);
|
||||
void setupPinnedWidget();
|
||||
|
||||
void applyGeometry();
|
||||
void applyGeometryToPinned();
|
||||
void updateGeometries();
|
||||
[[nodiscard]] int countPinnedScrollSkip(const PinnedView &entry) const;
|
||||
void setPinnedScrollSkip(int skip);
|
||||
|
||||
void updateTopFade();
|
||||
void updateBottomFade();
|
||||
void updateLeftFade();
|
||||
void updateRightFade();
|
||||
|
||||
void receiveSomeMouseEvents();
|
||||
void receiveAllMouseEvents();
|
||||
void handleClick(const MessageView &entry, QPoint point);
|
||||
void showContextMenu(const MessageView &entry, QPoint globalPoint);
|
||||
|
||||
[[nodiscard]] int donorPlace(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] TextWithEntities nameText(
|
||||
not_null<PeerData*> peer,
|
||||
int place);
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const MessagesMode _mode;
|
||||
std::unique_ptr<Ui::ElasticScroll> _scroll;
|
||||
Ui::Animations::Simple _scrollToAnimation;
|
||||
Ui::RpWidget *_messages = nullptr;
|
||||
QImage _canvas;
|
||||
|
||||
std::unique_ptr<Ui::ElasticScroll> _pinnedScroll;
|
||||
Ui::RpWidget *_pinned = nullptr;
|
||||
QImage _pinnedCanvas;
|
||||
int _pinnedScrollSkip = 0;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
rpl::variable<bool> _canManage;
|
||||
rpl::event_stream<> _hiddenShowRequested;
|
||||
rpl::event_stream<MessageDeleteRequest> _deleteRequests;
|
||||
std::optional<std::vector<Message>> _hidden;
|
||||
std::vector<MessageView> _views;
|
||||
style::complex_color _messageBg;
|
||||
Ui::RoundRect _messageBgRect;
|
||||
MsgId _delayedHighlightId = 0;
|
||||
MsgId _highlightId = 0;
|
||||
Ui::Animations::Simple _highlightAnimation;
|
||||
|
||||
std::vector<PinnedView> _pinnedViews;
|
||||
base::flat_map<uint64, std::unique_ptr<PayedBg>> _bgs;
|
||||
|
||||
QPoint _reactionBasePosition;
|
||||
rpl::lifetime _effectsLifetime;
|
||||
|
||||
Ui::Text::String _liveBadge;
|
||||
Ui::Text::String _adminBadge;
|
||||
|
||||
Ui::Text::CustomEmojiHelper _crownHelper;
|
||||
base::flat_map<int, QString> _crownEmojiDataCache;
|
||||
rpl::variable<std::vector<not_null<PeerData*>>> _topDonors;
|
||||
//Ui::Animations::Simple _topFadeAnimation;
|
||||
//Ui::Animations::Simple _bottomFadeAnimation;
|
||||
//Ui::Animations::Simple _leftFadeAnimation;
|
||||
//Ui::Animations::Simple _rightFadeAnimation;
|
||||
int _fadeHeight = 0;
|
||||
int _fadeWidth = 0;
|
||||
bool _topFadeShown = false;
|
||||
bool _bottomFadeShown = false;
|
||||
bool _leftFadeShown = false;
|
||||
bool _rightFadeShown = false;
|
||||
bool _streamMode = false;
|
||||
|
||||
int _left = 0;
|
||||
int _bottom = 0;
|
||||
int _width = 0;
|
||||
int _availableHeight = 0;
|
||||
|
||||
MsgId _revealedSpoilerId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
2944
Telegram/SourceFiles/calls/group/calls_group_panel.cpp
Normal file
2944
Telegram/SourceFiles/calls/group/calls_group_panel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
281
Telegram/SourceFiles/calls/group/calls_group_panel.h
Normal file
281
Telegram/SourceFiles/calls/group/calls_group_panel.h
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Image;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class AbstractButton;
|
||||
class ImportantTooltip;
|
||||
class DropdownMenu;
|
||||
class CallButton;
|
||||
class CallMuteButton;
|
||||
class IconButton;
|
||||
class FlatLabel;
|
||||
class RpWidget;
|
||||
class RpWindow;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
class ScrollArea;
|
||||
class GenericBox;
|
||||
class GroupCallScheduledLeft;
|
||||
struct CallButtonColors;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
class Instance;
|
||||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
struct CallBodyLayout;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
struct InviteRequest;
|
||||
struct ConferencePanelMigration;
|
||||
class Window;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Toasts;
|
||||
class Members;
|
||||
class Viewport;
|
||||
enum class PanelMode;
|
||||
enum class StickedTooltip;
|
||||
class MicLevelTester;
|
||||
class MessageField;
|
||||
class MessagesUi;
|
||||
|
||||
class Panel final
|
||||
: public base::has_weak_ptr
|
||||
, private Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
explicit Panel(not_null<GroupCall*> call);
|
||||
Panel(not_null<GroupCall*> call, ConferencePanelMigration info);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
[[nodiscard]] not_null<GroupCall*> call() const;
|
||||
[[nodiscard]] bool isVisible() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
void migrationShowShareLink();
|
||||
void migrationInviteUsers(std::vector<InviteRequest> users);
|
||||
|
||||
void minimize();
|
||||
void toggleFullScreen();
|
||||
void toggleFullScreen(bool fullscreen);
|
||||
void close();
|
||||
void showAndActivate();
|
||||
void closeBeforeDestroy();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
|
||||
[[nodiscard]] not_null<Window*> callWindow() const;
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
using State = GroupCall::State;
|
||||
struct ControlsBackgroundNarrow;
|
||||
|
||||
enum class NiceTooltipType {
|
||||
Normal,
|
||||
Sticked,
|
||||
};
|
||||
enum class StickedTooltipHide {
|
||||
Unavailable,
|
||||
Activated,
|
||||
Discarded,
|
||||
};
|
||||
|
||||
[[nodiscard]] PanelMode mode() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
|
||||
void initWindow();
|
||||
void initWidget();
|
||||
void initControls();
|
||||
void initShareAction();
|
||||
void initLayout(ConferencePanelMigration info);
|
||||
void initGeometry(ConferencePanelMigration info);
|
||||
void setupScheduledLabels(rpl::producer<TimeId> date);
|
||||
void setupMembers();
|
||||
void setupVideo(not_null<Viewport*> viewport);
|
||||
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
|
||||
[[nodiscard]] rpl::producer<QString> titleText();
|
||||
|
||||
bool handleClose();
|
||||
void startScheduledNow();
|
||||
void trackControls(bool track, bool force = false);
|
||||
void raiseControls();
|
||||
void enlargeVideo();
|
||||
|
||||
void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime);
|
||||
void trackControlOver(not_null<Ui::RpWidget*> control, bool over);
|
||||
void showNiceTooltip(
|
||||
not_null<Ui::RpWidget*> control,
|
||||
NiceTooltipType type = NiceTooltipType::Normal);
|
||||
void showStickedTooltip();
|
||||
void hideStickedTooltip(StickedTooltipHide hide);
|
||||
void hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide);
|
||||
void hideNiceTooltip();
|
||||
|
||||
bool updateMode();
|
||||
void updateControlsGeometry();
|
||||
void updateButtonsGeometry();
|
||||
void updateTooltipGeometry();
|
||||
void updateButtonsStyles();
|
||||
void updateMembersGeometry();
|
||||
void refreshControlsBackground();
|
||||
void refreshTitleBackground();
|
||||
void setupControlsBackgroundWide();
|
||||
void setupControlsBackgroundNarrow();
|
||||
void showControls();
|
||||
void createMessageButton();
|
||||
void refreshLeftButton();
|
||||
void refreshVideoButtons(
|
||||
std::optional<bool> overrideWideMode = std::nullopt);
|
||||
void refreshTopButton();
|
||||
void createPinOnTop();
|
||||
void setupEmptyRtmp();
|
||||
void toggleWideControls(bool shown);
|
||||
void updateWideControlsVisibility();
|
||||
[[nodiscard]] bool videoButtonInNarrowMode() const;
|
||||
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
|
||||
void toggleMessageTyping();
|
||||
[[nodiscard]] rpl::producer<Ui::CallButtonColors> toggleableOverrides(
|
||||
rpl::producer<bool> active);
|
||||
|
||||
void endCall();
|
||||
|
||||
void showMainMenu();
|
||||
void chooseJoinAs();
|
||||
void chooseShareScreenSource();
|
||||
void screenSharingPrivacyRequest();
|
||||
void addMembers();
|
||||
void kickParticipant(not_null<PeerData*> participantPeer);
|
||||
void kickParticipantSure(not_null<PeerData*> participantPeer);
|
||||
[[nodiscard]] QRect computeTitleRect() const;
|
||||
void refreshTitle();
|
||||
void refreshTitleGeometry();
|
||||
void refreshTitleColors();
|
||||
void setupRealCallViewers();
|
||||
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
||||
|
||||
void migrate(not_null<ChannelData*> channel);
|
||||
void subscribeToPeerChanges();
|
||||
|
||||
QWidget *chooseSourceParent() override;
|
||||
QString chooseSourceActiveDeviceId() override;
|
||||
bool chooseSourceActiveWithAudio() override;
|
||||
bool chooseSourceWithAudioSupported() override;
|
||||
rpl::lifetime &chooseSourceInstanceLifetime() override;
|
||||
void chooseSourceAccepted(
|
||||
const QString &deviceId,
|
||||
bool withAudio) override;
|
||||
void chooseSourceStop() override;
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
not_null<PeerData*> _peer;
|
||||
|
||||
std::shared_ptr<Window> _window;
|
||||
rpl::variable<PanelMode> _mode;
|
||||
rpl::variable<bool> _fullScreenOrMaximized = false;
|
||||
bool _unpinnedMaximized = false;
|
||||
bool _rtmpFull = false;
|
||||
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
object_ptr<Ui::RpWidget> _titleBackground = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _title = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _titleSeparator = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _viewers = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
|
||||
object_ptr<Ui::AbstractButton> _recordingMark = { nullptr };
|
||||
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
|
||||
object_ptr<Ui::IconButton> _pinOnTop = { nullptr };
|
||||
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
|
||||
rpl::variable<bool> _wideMenuShown = false;
|
||||
rpl::variable<bool> _messageTyping = false;
|
||||
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
|
||||
object_ptr<Members> _members = { nullptr };
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::lifetime _trackControlsOverStateLifetime;
|
||||
rpl::lifetime _trackControlsMenuLifetime;
|
||||
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _countdown = { nullptr };
|
||||
std::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;
|
||||
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _emptyRtmp = { nullptr };
|
||||
ChooseJoinAsProcess _joinAsProcess;
|
||||
std::optional<QRect> _lastSmallGeometry;
|
||||
std::optional<QRect> _lastLargeGeometry;
|
||||
bool _lastLargeMaximized = false;
|
||||
bool _showWideControls = false;
|
||||
bool _trackControls = false;
|
||||
bool _wideControlsShown = false;
|
||||
Ui::Animations::Simple _wideControlsAnimation;
|
||||
|
||||
object_ptr<Ui::RpWidget> _controlsBackgroundWide = { nullptr };
|
||||
std::unique_ptr<ControlsBackgroundNarrow> _controlsBackgroundNarrow;
|
||||
object_ptr<Ui::CallButton> _settings = { nullptr };
|
||||
object_ptr<Ui::CallButton> _wideMenu = { nullptr };
|
||||
object_ptr<Ui::CallButton> _callShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _video = { nullptr };
|
||||
object_ptr<Ui::CallButton> _screenShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _message = { nullptr };
|
||||
std::unique_ptr<Ui::CallMuteButton> _mute;
|
||||
object_ptr<Ui::CallButton> _hangup;
|
||||
object_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };
|
||||
QPointer<Ui::IconButton> _stickedTooltipClose;
|
||||
QPointer<Ui::RpWidget> _niceTooltipControl;
|
||||
StickedTooltips _stickedTooltipsShown;
|
||||
Fn<void()> _callShareLinkCallback;
|
||||
|
||||
std::shared_ptr<ChatHelpers::Show> _cachedShow;
|
||||
std::unique_ptr<MessageField> _messageField;
|
||||
std::unique_ptr<MessagesUi> _messages;
|
||||
|
||||
const std::unique_ptr<Toasts> _toasts;
|
||||
|
||||
std::unique_ptr<MicLevelTester> _micLevelTester;
|
||||
|
||||
style::complex_color _controlsBackgroundColor;
|
||||
base::Timer _hideControlsTimer;
|
||||
rpl::lifetime _hideControlsTimerLifetime;
|
||||
|
||||
rpl::lifetime _peerLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
356
Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp
Normal file
356
Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_rtmp.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QStyle>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPasswordCharAmount = 24;
|
||||
|
||||
void StartWithBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void()> done,
|
||||
Fn<void()> revoke,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
{
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box->verticalLayout(),
|
||||
{
|
||||
.name = u"rtmp"_q,
|
||||
.sizeOverride = st::normalBoxLottieSize,
|
||||
},
|
||||
{});
|
||||
box->verticalLayout()->add(std::move(icon.widget), {}, style::al_top);
|
||||
box->setShowFinishedCallback([animate = icon.animate] {
|
||||
animate(anim::repeat::loop);
|
||||
});
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
}
|
||||
|
||||
StartRtmpProcess::FillRtmpRows(
|
||||
box->verticalLayout(),
|
||||
true,
|
||||
std::move(show),
|
||||
std::move(data),
|
||||
&st::boxLabel,
|
||||
&st::groupCallRtmpShowButton,
|
||||
&st::defaultSubsectionTitle,
|
||||
&st::attentionBoxButton,
|
||||
&st::defaultPopupMenu);
|
||||
|
||||
box->setTitle(tr::lng_group_call_rtmp_title());
|
||||
|
||||
Ui::AddDividerText(
|
||||
box->verticalLayout(),
|
||||
tr::lng_group_call_rtmp_info());
|
||||
|
||||
box->addButton(tr::lng_group_call_rtmp_start(), done);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
box->setWidth(st::boxWideWidth);
|
||||
{
|
||||
const auto top = box->addTopButton(st::infoTopBarMenu);
|
||||
top->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
top,
|
||||
st::popupMenuWithIcons);
|
||||
state->menu->addAction(
|
||||
tr::lng_group_invite_context_revoke(tr::now),
|
||||
revoke,
|
||||
&st::menuIconRemove);
|
||||
state->menu->setForcedOrigin(
|
||||
Ui::PanelAnimation::Origin::TopRight);
|
||||
top->setForceRippled(true);
|
||||
const auto raw = state->menu.get();
|
||||
raw->setDestroyedCallback([=] {
|
||||
if ((state->menu == raw) && top) {
|
||||
top->setForceRippled(false);
|
||||
}
|
||||
});
|
||||
state->menu->popup(top->mapToGlobal(top->rect().center()));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StartRtmpProcess::~StartRtmpProcess() {
|
||||
close();
|
||||
}
|
||||
|
||||
void StartRtmpProcess::start(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done) {
|
||||
Expects(done != nullptr);
|
||||
|
||||
const auto session = &peer->session();
|
||||
if (_request) {
|
||||
if (_request->peer == peer) {
|
||||
_request->show = std::move(show);
|
||||
_request->done = std::move(done);
|
||||
return;
|
||||
}
|
||||
session->api().request(_request->id).cancel();
|
||||
_request = nullptr;
|
||||
}
|
||||
_request = std::make_unique<RtmpRequest>(
|
||||
RtmpRequest{
|
||||
.peer = peer,
|
||||
.show = std::move(show),
|
||||
.done = std::move(done),
|
||||
});
|
||||
session->account().sessionChanges(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
|
||||
requestUrl(false);
|
||||
}
|
||||
|
||||
void StartRtmpProcess::close() {
|
||||
if (_request) {
|
||||
_request->peer->session().api().request(_request->id).cancel();
|
||||
if (const auto strong = _request->box.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
_request = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void StartRtmpProcess::requestUrl(bool revoke) {
|
||||
const auto session = &_request->peer->session();
|
||||
_request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl(
|
||||
MTP_flags(0),
|
||||
_request->peer->input(),
|
||||
MTP_bool(revoke)
|
||||
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
|
||||
auto data = result.match([&](
|
||||
const MTPDphone_groupCallStreamRtmpUrl &data) {
|
||||
return RtmpInfo{ .url = qs(data.vurl()), .key = qs(data.vkey()) };
|
||||
});
|
||||
processUrl(std::move(data));
|
||||
}).fail([=] {
|
||||
_request->show->showToast(Lang::Hard::ServerError());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void StartRtmpProcess::processUrl(RtmpInfo data) {
|
||||
if (!_request->box) {
|
||||
createBox();
|
||||
}
|
||||
_request->data = std::move(data);
|
||||
}
|
||||
|
||||
void StartRtmpProcess::finish(JoinInfo info) {
|
||||
info.rtmpInfo = _request->data.current();
|
||||
_request->done(std::move(info));
|
||||
}
|
||||
|
||||
void StartRtmpProcess::createBox() {
|
||||
auto done = [=] {
|
||||
const auto peer = _request->peer;
|
||||
const auto joinAs = (peer->isChat() && peer->asChat()->amCreator())
|
||||
? peer
|
||||
: (peer->isChannel() && peer->asChannel()->amCreator())
|
||||
? peer
|
||||
: peer->session().user();
|
||||
finish({ .peer = peer, .joinAs = joinAs, .rtmp = true });
|
||||
};
|
||||
auto revoke = [=] {
|
||||
const auto guard = base::make_weak(&_request->guard);
|
||||
_request->show->showBox(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_revoke_sure(),
|
||||
.confirmed = crl::guard(guard, [=](Fn<void()> &&close) {
|
||||
requestUrl(true);
|
||||
close();
|
||||
}),
|
||||
.confirmText = tr::lng_group_invite_context_revoke(),
|
||||
}));
|
||||
};
|
||||
auto object = Box(
|
||||
StartWithBox,
|
||||
std::move(done),
|
||||
std::move(revoke),
|
||||
_request->show,
|
||||
_request->data.value());
|
||||
object->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
_request = nullptr;
|
||||
}, _request->lifetime);
|
||||
_request->box = base::make_weak(object.data());
|
||||
_request->show->showBox(std::move(object));
|
||||
}
|
||||
|
||||
void StartRtmpProcess::FillRtmpRows(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
bool divider,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data,
|
||||
const style::FlatLabel *labelStyle,
|
||||
const style::IconButton *showButtonStyle,
|
||||
const style::FlatLabel *subsectionTitleStyle,
|
||||
const style::RoundButton *attentionButtonStyle,
|
||||
const style::PopupMenu *popupMenuStyle) {
|
||||
struct State {
|
||||
rpl::variable<QString> key;
|
||||
rpl::variable<QString> url;
|
||||
bool warned = false;
|
||||
};
|
||||
|
||||
const auto &rowPadding = st::boxRowPadding;
|
||||
|
||||
const auto state = container->lifetime().make_state<State>();
|
||||
state->key = rpl::duplicate(
|
||||
data
|
||||
) | rpl::map([=](const auto &d) { return d.key; });
|
||||
state->url = std::move(
|
||||
data
|
||||
) | rpl::map([=](const auto &d) { return d.url; });
|
||||
|
||||
const auto showToast = [=](const QString &text) {
|
||||
show->showToast(text);
|
||||
};
|
||||
const auto addButton = [&](
|
||||
bool key,
|
||||
rpl::producer<QString> &&text) {
|
||||
auto wrap = object_ptr<Ui::RpWidget>(container);
|
||||
auto button = Ui::CreateChild<Ui::RoundButton>(
|
||||
wrap.data(),
|
||||
rpl::duplicate(text),
|
||||
st::groupCallRtmpCopyButton);
|
||||
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
button->setClickedCallback(key
|
||||
? Fn<void()>([=] {
|
||||
QGuiApplication::clipboard()->setText(state->key.current());
|
||||
showToast(tr::lng_group_call_rtmp_key_copied(tr::now));
|
||||
})
|
||||
: Fn<void()>([=] {
|
||||
QGuiApplication::clipboard()->setText(state->url.current());
|
||||
showToast(tr::lng_group_call_rtmp_url_copied(tr::now));
|
||||
}));
|
||||
Ui::AddSkip(container, st::groupCallRtmpCopyButtonTopSkip);
|
||||
const auto weak = container->add(std::move(wrap), rowPadding);
|
||||
Ui::AddSkip(container, st::groupCallRtmpCopyButtonBottomSkip);
|
||||
button->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
weak->resize(weak->width(), height);
|
||||
}, container->lifetime());
|
||||
return weak;
|
||||
};
|
||||
|
||||
const auto addLabel = [&](v::text::data &&text) {
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
v::text::take_marked(std::move(text)),
|
||||
*labelStyle,
|
||||
*popupMenuStyle),
|
||||
st::boxRowPadding + QMargins(0, 0, showButtonStyle->width, 0));
|
||||
label->setSelectable(true);
|
||||
label->setBreakEverywhere(true);
|
||||
return label;
|
||||
};
|
||||
|
||||
// Server URL.
|
||||
Ui::AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_group_call_rtmp_url_subtitle(),
|
||||
st::groupCallRtmpSubsectionTitleAddPadding,
|
||||
subsectionTitleStyle);
|
||||
|
||||
auto urlLabelContent = state->url.value();
|
||||
addLabel(std::move(urlLabelContent));
|
||||
Ui::AddSkip(container, st::groupCallRtmpUrlSkip);
|
||||
addButton(false, tr::lng_group_call_rtmp_url_copy());
|
||||
//
|
||||
|
||||
if (divider) {
|
||||
Ui::AddDivider(container);
|
||||
}
|
||||
|
||||
// Stream Key.
|
||||
Ui::AddSkip(container, st::groupCallRtmpKeySubsectionTitleSkip);
|
||||
|
||||
Ui::AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_group_call_rtmp_key_subtitle(),
|
||||
st::groupCallRtmpSubsectionTitleAddPadding,
|
||||
subsectionTitleStyle);
|
||||
|
||||
auto keyLabelContent = state->key.value(
|
||||
) | rpl::map([](const QString &key) {
|
||||
const auto size = int(key.size());
|
||||
auto result = TextWithEntities{ key };
|
||||
if (size > 0) {
|
||||
result.entities.push_back({ EntityType::Spoiler, 0, size });
|
||||
}
|
||||
return result;
|
||||
}) | rpl::after_next([=] {
|
||||
container->resizeToWidth(container->widthNoMargins());
|
||||
});
|
||||
const auto streamKeyLabel = addLabel(std::move(keyLabelContent));
|
||||
streamKeyLabel->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton button) {
|
||||
if (button == Qt::LeftButton) {
|
||||
show->showBox(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_key_warning(
|
||||
tr::rich),
|
||||
.confirmed = [=](Fn<void()> &&close) {
|
||||
handler->onClick({});
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_from_request_understand(),
|
||||
.cancelText = tr::lng_cancel(),
|
||||
.confirmStyle = attentionButtonStyle,
|
||||
.labelStyle = labelStyle,
|
||||
}));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
addButton(true, tr::lng_group_call_rtmp_key_copy());
|
||||
//
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
75
Telegram/SourceFiles/calls/group/calls_group_rtmp.h
Normal file
75
Telegram/SourceFiles/calls/group/calls_group_rtmp.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
struct RoundButton;
|
||||
struct IconButton;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct JoinInfo;
|
||||
|
||||
class StartRtmpProcess final {
|
||||
public:
|
||||
StartRtmpProcess() = default;
|
||||
~StartRtmpProcess();
|
||||
|
||||
void start(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
Fn<void(JoinInfo)> done);
|
||||
void close();
|
||||
|
||||
static void FillRtmpRows(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
bool divider,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
rpl::producer<RtmpInfo> &&data,
|
||||
const style::FlatLabel *labelStyle,
|
||||
const style::IconButton *showButtonStyle,
|
||||
const style::FlatLabel *subsectionTitleStyle,
|
||||
const style::RoundButton *attentionButtonStyle,
|
||||
const style::PopupMenu *popupMenuStyle);
|
||||
|
||||
private:
|
||||
void requestUrl(bool revoke);
|
||||
void processUrl(RtmpInfo data);
|
||||
void createBox();
|
||||
void finish(JoinInfo info);
|
||||
|
||||
struct RtmpRequest {
|
||||
not_null<PeerData*> peer;
|
||||
rpl::variable<RtmpInfo> data;
|
||||
std::shared_ptr<Ui::Show> show;
|
||||
Fn<void(JoinInfo)> done;
|
||||
base::has_weak_ptr guard;
|
||||
base::weak_qptr<Ui::BoxContent> box;
|
||||
rpl::lifetime lifetime;
|
||||
mtpRequestId id = 0;
|
||||
};
|
||||
std::unique_ptr<RtmpRequest> _request;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
988
Telegram/SourceFiles/calls/group/calls_group_settings.cpp
Normal file
988
Telegram/SourceFiles/calls/group/calls_group_settings.cpp
Normal file
@@ -0,0 +1,988 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h" // LeaveBox.
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_volume_item.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "ui/widgets/level_meter.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "history/view/history_view_schedule_box.h"
|
||||
#include "history/history_item_helpers.h" // GetErrorForSending.
|
||||
#include "history/history.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/global_shortcuts.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_user.h"
|
||||
#include "calls/group/calls_group_rtmp.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "webrtc/webrtc_audio_input_tester.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
#include "settings/settings_calls.h"
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDelaysCount = 201;
|
||||
constexpr auto kMicrophoneTooltipAfterLoudCount = 3;
|
||||
constexpr auto kDropLoudAfterQuietCount = 5;
|
||||
constexpr auto kMicrophoneTooltipLevelThreshold = 0.2;
|
||||
constexpr auto kMicrophoneTooltipCheckInterval = crl::time(500);
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
constexpr auto kCheckAccessibilityInterval = crl::time(500);
|
||||
#endif // Q_OS_MAC
|
||||
|
||||
void SaveCallJoinMuted(
|
||||
not_null<PeerData*> peer,
|
||||
CallId callId,
|
||||
bool joinMuted) {
|
||||
const auto call = peer->groupCall();
|
||||
if (!call
|
||||
|| call->id() != callId
|
||||
|| peer->isUser()
|
||||
|| !peer->canManageGroupCall()
|
||||
|| !call->canChangeJoinMuted()
|
||||
|| call->joinMuted() == joinMuted) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setJoinMutedLocally(joinMuted);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_join_muted),
|
||||
call->input(),
|
||||
MTP_bool(joinMuted),
|
||||
MTPBool(), // messages_enabled
|
||||
MTPlong() // send_paid_messages_stars
|
||||
)).send();
|
||||
}
|
||||
|
||||
void SaveCallMessagesEnabled(
|
||||
not_null<PeerData*> peer,
|
||||
CallId callId,
|
||||
bool messagesEnabled) {
|
||||
const auto call = peer->groupCall();
|
||||
if (!call
|
||||
|| call->id() != callId
|
||||
|| !peer->canManageGroupCall()
|
||||
|| !call->canChangeMessagesEnabled()
|
||||
|| call->messagesEnabled() == messagesEnabled) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setMessagesEnabledLocally(messagesEnabled);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_messages_enabled),
|
||||
call->input(),
|
||||
MTPBool(), // join_muted
|
||||
MTP_bool(messagesEnabled),
|
||||
MTPlong() // send_paid_messages_stars
|
||||
)).send();
|
||||
}
|
||||
|
||||
[[nodiscard]] crl::time DelayByIndex(int index) {
|
||||
return index * crl::time(10);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString FormatDelay(crl::time delay) {
|
||||
return (delay < crl::time(1000))
|
||||
? tr::lng_group_call_ptt_delay_ms(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
QString::number(delay))
|
||||
: tr::lng_group_call_ptt_delay_s(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
QString::number(delay / 1000., 'f', 2));
|
||||
}
|
||||
|
||||
object_ptr<ShareBox> ShareInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &linkSpeaker,
|
||||
const QString &linkListener,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
const auto sending = std::make_shared<bool>();
|
||||
const auto box = std::make_shared<base::weak_qptr<ShareBox>>();
|
||||
|
||||
auto bottom = linkSpeaker.isEmpty()
|
||||
? nullptr
|
||||
: object_ptr<Ui::PaddingWrap<Ui::Checkbox>>(
|
||||
nullptr,
|
||||
object_ptr<Ui::Checkbox>(
|
||||
nullptr,
|
||||
tr::lng_group_call_share_speaker(tr::now),
|
||||
true,
|
||||
st::groupCallCheckbox),
|
||||
st::groupCallShareMutedMargin);
|
||||
const auto speakerCheckbox = bottom ? bottom->entity() : nullptr;
|
||||
const auto currentLink = [=] {
|
||||
return (!speakerCheckbox || !speakerCheckbox->checked())
|
||||
? linkListener
|
||||
: linkSpeaker;
|
||||
};
|
||||
auto copyCallback = [=] {
|
||||
QGuiApplication::clipboard()->setText(currentLink());
|
||||
show->showToast(tr::lng_group_invite_copied(tr::now));
|
||||
};
|
||||
auto countMessagesCallback = [=](const TextWithTags &comment) {
|
||||
return 1;
|
||||
};
|
||||
auto submitCallback = [=](
|
||||
std::vector<not_null<Data::Thread*>> &&result,
|
||||
Fn<bool()> checkPaid,
|
||||
TextWithTags &&comment,
|
||||
Api::SendOptions options,
|
||||
Data::ForwardOptions) {
|
||||
if (*sending || result.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = GetErrorForSending(
|
||||
result,
|
||||
{ .text = &comment });
|
||||
if (error.error) {
|
||||
if (const auto weak = *box) {
|
||||
weak->getDelegate()->show(
|
||||
MakeSendErrorBox(error, result.size() > 1));
|
||||
}
|
||||
return;
|
||||
} else if (!checkPaid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
*sending = true;
|
||||
const auto link = currentLink();
|
||||
if (!comment.text.isEmpty()) {
|
||||
comment.text = link + "\n" + comment.text;
|
||||
const auto add = link.size() + 1;
|
||||
for (auto &tag : comment.tags) {
|
||||
tag.offset += add;
|
||||
}
|
||||
} else {
|
||||
comment.text = link;
|
||||
}
|
||||
auto &api = peer->session().api();
|
||||
for (const auto thread : result) {
|
||||
auto message = Api::MessageToSend(
|
||||
Api::SendAction(thread, options));
|
||||
message.textWithTags = comment;
|
||||
message.action.clearDraft = false;
|
||||
api.sendMessage(std::move(message));
|
||||
}
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
show->showToast(tr::lng_share_done(tr::now));
|
||||
};
|
||||
auto filterCallback = [](not_null<Data::Thread*> thread) {
|
||||
if (const auto user = thread->peer()->asUser()) {
|
||||
if (user->canSendIgnoreMoneyRestrictions()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Data::CanSend(thread, ChatRestriction::SendOther);
|
||||
};
|
||||
|
||||
const auto st = ::Settings::DarkCreditsEntryBoxStyle();
|
||||
auto result = Box<ShareBox>(ShareBox::Descriptor{
|
||||
.session = &peer->session(),
|
||||
.copyCallback = std::move(copyCallback),
|
||||
.countMessagesCallback = std::move(countMessagesCallback),
|
||||
.submitCallback = std::move(submitCallback),
|
||||
.filterCallback = std::move(filterCallback),
|
||||
.bottomWidget = std::move(bottom),
|
||||
.copyLinkText = rpl::conditional(
|
||||
(speakerCheckbox
|
||||
? speakerCheckbox->checkedValue()
|
||||
: rpl::single(false)),
|
||||
tr::lng_group_call_copy_speaker_link(),
|
||||
tr::lng_group_call_copy_listener_link()),
|
||||
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
|
||||
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
|
||||
});
|
||||
*box = result.data();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call) {
|
||||
using namespace Settings;
|
||||
|
||||
const auto weakCall = base::make_weak(call);
|
||||
const auto weakBox = base::make_weak(box);
|
||||
|
||||
struct State {
|
||||
std::unique_ptr<Webrtc::DeviceResolver> deviceId;
|
||||
std::unique_ptr<Webrtc::AudioInputTester> micTester;
|
||||
Ui::LevelMeter *micTestLevel = nullptr;
|
||||
float micLevel = 0.;
|
||||
Ui::Animations::Simple micLevelAnimation;
|
||||
base::Timer levelUpdateTimer;
|
||||
bool generatingLink = false;
|
||||
};
|
||||
const auto peer = call->peer();
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
const auto real = call->conference()
|
||||
? call->lookupReal()
|
||||
: peer->groupCall();
|
||||
const auto rtmp = call->rtmp();
|
||||
const auto id = call->id();
|
||||
const auto goodReal = (real && real->id() == id);
|
||||
|
||||
const auto layout = box->verticalLayout();
|
||||
const auto &settings = Core::App().settings();
|
||||
|
||||
const auto joinMuted = !call->conference()
|
||||
&& goodReal
|
||||
&& real->joinMuted();
|
||||
const auto messagesEnabled = goodReal && real->messagesEnabled();
|
||||
const auto canChangeJoinMuted = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeJoinMuted();
|
||||
const auto canChangeMessagesEnabled = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeMessagesEnabled();
|
||||
const auto addCheck = canChangeJoinMuted && peer->canManageGroupCall();
|
||||
const auto addMessages = canChangeMessagesEnabled
|
||||
&& (call->conference() || peer->canManageGroupCall());
|
||||
|
||||
const auto addDivider = [&] {
|
||||
layout->add(object_ptr<Ui::BoxContentDivider>(
|
||||
layout,
|
||||
st::boxDividerHeight,
|
||||
st::groupCallDividerBar));
|
||||
};
|
||||
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
const auto muteJoined = addCheck
|
||||
? layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_new_muted(),
|
||||
st::groupCallSettingsButton))->toggleOn(rpl::single(joinMuted))
|
||||
: nullptr;
|
||||
const auto enableMessages = addMessages
|
||||
? layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_enable_messages(),
|
||||
st::groupCallSettingsButton))->toggleOn(
|
||||
rpl::single(messagesEnabled))
|
||||
: nullptr;
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
|
||||
auto playbackIdWithFallback = Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callPlaybackDeviceIdValue(),
|
||||
Core::App().settings().playbackDeviceIdValue());
|
||||
AddButtonWithLabel(
|
||||
layout,
|
||||
tr::lng_group_call_speakers(),
|
||||
PlaybackDeviceNameValue(rpl::duplicate(playbackIdWithFallback)),
|
||||
st::groupCallSettingsButton
|
||||
)->addClickHandler([=] {
|
||||
box->getDelegate()->show(ChoosePlaybackDeviceBox(
|
||||
rpl::duplicate(playbackIdWithFallback),
|
||||
crl::guard(box, [=](const QString &id) {
|
||||
Core::App().settings().setCallPlaybackDeviceId(id);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}),
|
||||
&st::groupCallCheckbox,
|
||||
&st::groupCallRadio));
|
||||
});
|
||||
|
||||
if (!rtmp) {
|
||||
auto captureIdWithFallback = Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue());
|
||||
AddButtonWithLabel(
|
||||
layout,
|
||||
tr::lng_group_call_microphone(),
|
||||
CaptureDeviceNameValue(rpl::duplicate(captureIdWithFallback)),
|
||||
st::groupCallSettingsButton
|
||||
)->addClickHandler([=] {
|
||||
box->getDelegate()->show(ChooseCaptureDeviceBox(
|
||||
rpl::duplicate(captureIdWithFallback),
|
||||
crl::guard(box, [=](const QString &id) {
|
||||
Core::App().settings().setCallCaptureDeviceId(id);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}),
|
||||
&st::groupCallCheckbox,
|
||||
&st::groupCallRadio));
|
||||
});
|
||||
|
||||
state->micTestLevel = box->addRow(
|
||||
object_ptr<Ui::LevelMeter>(
|
||||
box.get(),
|
||||
st::groupCallLevelMeter),
|
||||
st::settingsLevelMeterPadding);
|
||||
state->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));
|
||||
|
||||
state->levelUpdateTimer.setCallback([=] {
|
||||
const auto was = state->micLevel;
|
||||
state->micLevel = state->micTester->getAndResetLevel();
|
||||
state->micLevelAnimation.start([=] {
|
||||
state->micTestLevel->setValue(
|
||||
state->micLevelAnimation.value(state->micLevel));
|
||||
}, was, state->micLevel, kMicTestAnimationDuration);
|
||||
});
|
||||
|
||||
Ui::AddSkip(layout);
|
||||
//Ui::AddDivider(layout);
|
||||
//Ui::AddSkip(layout);
|
||||
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_noise_suppression(),
|
||||
st::groupCallSettingsButton
|
||||
))->toggleOn(rpl::single(
|
||||
settings.groupCallNoiseSuppression()
|
||||
))->toggledChanges(
|
||||
) | rpl::on_next([=](bool enabled) {
|
||||
Core::App().settings().setGroupCallNoiseSuppression(enabled);
|
||||
call->setNoiseSuppression(enabled);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}, layout->lifetime());
|
||||
|
||||
using GlobalShortcut = base::GlobalShortcut;
|
||||
struct PushToTalkState {
|
||||
rpl::variable<QString> recordText = tr::lng_group_call_ptt_shortcut();
|
||||
rpl::variable<QString> shortcutText;
|
||||
rpl::event_stream<bool> pushToTalkToggles;
|
||||
std::shared_ptr<base::GlobalShortcutManager> manager;
|
||||
GlobalShortcut shortcut;
|
||||
crl::time delay = 0;
|
||||
bool recording = false;
|
||||
};
|
||||
if (base::GlobalShortcutsAvailable()) {
|
||||
const auto state = box->lifetime().make_state<PushToTalkState>();
|
||||
if (!base::GlobalShortcutsAllowed()) {
|
||||
Core::App().settings().setGroupCallPushToTalk(false);
|
||||
}
|
||||
const auto tryFillFromManager = [=] {
|
||||
state->shortcut = state->manager
|
||||
? state->manager->shortcutFromSerialized(
|
||||
Core::App().settings().groupCallPushToTalkShortcut())
|
||||
: nullptr;
|
||||
state->shortcutText = state->shortcut
|
||||
? state->shortcut->toDisplayString()
|
||||
: QString();
|
||||
};
|
||||
state->manager = settings.groupCallPushToTalk()
|
||||
? call->ensureGlobalShortcutManager()
|
||||
: nullptr;
|
||||
tryFillFromManager();
|
||||
|
||||
state->delay = settings.groupCallPushToTalkDelay();
|
||||
const auto pushToTalk = layout->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_push_to_talk(),
|
||||
st::groupCallSettingsButton
|
||||
))->toggleOn(rpl::single(
|
||||
settings.groupCallPushToTalk()
|
||||
) | rpl::then(state->pushToTalkToggles.events()));
|
||||
const auto pushToTalkWrap = layout->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
layout,
|
||||
object_ptr<Ui::VerticalLayout>(layout)));
|
||||
const auto pushToTalkInner = pushToTalkWrap->entity();
|
||||
const auto recording = pushToTalkInner->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
pushToTalkInner,
|
||||
state->recordText.value(),
|
||||
st::groupCallSettingsButton));
|
||||
CreateRightLabel(
|
||||
recording,
|
||||
state->shortcutText.value(),
|
||||
st::groupCallSettingsButton,
|
||||
state->recordText.value());
|
||||
|
||||
const auto applyAndSave = [=] {
|
||||
call->applyGlobalShortcutChanges();
|
||||
Core::App().saveSettingsDelayed();
|
||||
};
|
||||
const auto showPrivacyRequest = [=] {
|
||||
#ifdef Q_OS_MAC
|
||||
if (!Platform::IsMac10_14OrGreater()) {
|
||||
return;
|
||||
}
|
||||
const auto requestInputMonitoring = Platform::IsMac10_15OrGreater();
|
||||
box->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
rpl::combine(
|
||||
tr::lng_group_call_mac_access(),
|
||||
(requestInputMonitoring
|
||||
? tr::lng_group_call_mac_input()
|
||||
: tr::lng_group_call_mac_accessibility())
|
||||
) | rpl::map([](QString a, QString b) {
|
||||
auto result = tr::rich(a);
|
||||
result.append("\n\n").append(tr::rich(b));
|
||||
return result;
|
||||
}),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
box->addButton(tr::lng_group_call_mac_settings(), [=] {
|
||||
if (requestInputMonitoring) {
|
||||
Platform::OpenInputMonitoringPrivacySettings();
|
||||
} else {
|
||||
Platform::OpenAccessibilityPrivacySettings();
|
||||
}
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
if (!requestInputMonitoring) {
|
||||
// Accessibility is enabled without app restart, so short-poll it.
|
||||
base::timer_each(
|
||||
kCheckAccessibilityInterval
|
||||
) | rpl::filter([] {
|
||||
return base::GlobalShortcutsAllowed();
|
||||
}) | rpl::on_next([=] {
|
||||
box->closeBox();
|
||||
}, box->lifetime());
|
||||
}
|
||||
}));
|
||||
#endif // Q_OS_MAC
|
||||
};
|
||||
const auto ensureManager = [=] {
|
||||
if (state->manager) {
|
||||
return true;
|
||||
} else if (base::GlobalShortcutsAllowed()) {
|
||||
state->manager = call->ensureGlobalShortcutManager();
|
||||
tryFillFromManager();
|
||||
return true;
|
||||
}
|
||||
showPrivacyRequest();
|
||||
return false;
|
||||
};
|
||||
const auto stopRecording = [=] {
|
||||
state->recording = false;
|
||||
state->recordText = tr::lng_group_call_ptt_shortcut();
|
||||
state->shortcutText = state->shortcut
|
||||
? state->shortcut->toDisplayString()
|
||||
: QString();
|
||||
recording->setColorOverride(std::nullopt);
|
||||
if (state->manager) {
|
||||
state->manager->stopRecording();
|
||||
}
|
||||
};
|
||||
const auto startRecording = [=] {
|
||||
if (!ensureManager()) {
|
||||
state->pushToTalkToggles.fire(false);
|
||||
pushToTalkWrap->hide(anim::type::instant);
|
||||
return;
|
||||
}
|
||||
state->recording = true;
|
||||
state->recordText = tr::lng_group_call_ptt_recording();
|
||||
recording->setColorOverride(
|
||||
st::groupCallSettingsAttentionButton.textFg->c);
|
||||
auto progress = crl::guard(box, [=](GlobalShortcut shortcut) {
|
||||
state->shortcutText = shortcut->toDisplayString();
|
||||
});
|
||||
auto done = crl::guard(box, [=](GlobalShortcut shortcut) {
|
||||
state->shortcut = shortcut;
|
||||
Core::App().settings().setGroupCallPushToTalkShortcut(shortcut
|
||||
? shortcut->serialize()
|
||||
: QByteArray());
|
||||
applyAndSave();
|
||||
stopRecording();
|
||||
});
|
||||
state->manager->startRecording(std::move(progress), std::move(done));
|
||||
};
|
||||
recording->addClickHandler([=] {
|
||||
if (state->recording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
const auto label = pushToTalkInner->add(
|
||||
object_ptr<Ui::LabelSimple>(
|
||||
pushToTalkInner,
|
||||
st::groupCallDelayLabel),
|
||||
st::groupCallDelayLabelMargin);
|
||||
const auto value = std::clamp(
|
||||
state->delay,
|
||||
crl::time(0),
|
||||
DelayByIndex(kDelaysCount - 1));
|
||||
const auto callback = [=](crl::time delay) {
|
||||
state->delay = delay;
|
||||
label->setText(tr::lng_group_call_ptt_delay(
|
||||
tr::now,
|
||||
lt_delay,
|
||||
FormatDelay(delay)));
|
||||
if (Core::App().settings().groupCallPushToTalkDelay() != delay) {
|
||||
Core::App().settings().setGroupCallPushToTalkDelay(delay);
|
||||
applyAndSave();
|
||||
}
|
||||
};
|
||||
callback(value);
|
||||
const auto slider = pushToTalkInner->add(
|
||||
object_ptr<Ui::MediaSlider>(
|
||||
pushToTalkInner,
|
||||
st::groupCallDelaySlider),
|
||||
st::groupCallDelayMargin);
|
||||
slider->resize(st::groupCallDelaySlider.seekSize);
|
||||
slider->setPseudoDiscrete(
|
||||
kDelaysCount,
|
||||
DelayByIndex,
|
||||
value,
|
||||
callback);
|
||||
|
||||
pushToTalkWrap->toggle(
|
||||
settings.groupCallPushToTalk(),
|
||||
anim::type::instant);
|
||||
pushToTalk->toggledChanges(
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
if (!toggled) {
|
||||
stopRecording();
|
||||
} else if (!ensureManager()) {
|
||||
state->pushToTalkToggles.fire(false);
|
||||
pushToTalkWrap->hide(anim::type::instant);
|
||||
return;
|
||||
}
|
||||
Core::App().settings().setGroupCallPushToTalk(toggled);
|
||||
applyAndSave();
|
||||
pushToTalkWrap->toggle(toggled, anim::type::normal);
|
||||
}, pushToTalk->lifetime());
|
||||
|
||||
auto boxKeyFilter = [=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::KeyPress && state->recording)
|
||||
? base::EventFilterResult::Cancel
|
||||
: base::EventFilterResult::Continue;
|
||||
};
|
||||
box->lifetime().make_state<base::unique_qptr<QObject>>(
|
||||
base::install_event_filter(box, std::move(boxKeyFilter)));
|
||||
}
|
||||
|
||||
Ui::AddSkip(layout);
|
||||
//Ui::AddDivider(layout);
|
||||
//Ui::AddSkip(layout);
|
||||
}
|
||||
auto shareLink = Fn<void()>();
|
||||
if (peer->isChannel()
|
||||
&& peer->asChannel()->hasUsername()
|
||||
&& goodReal) {
|
||||
const auto showBox = crl::guard(box, [=](
|
||||
object_ptr<Ui::BoxContent> next) {
|
||||
box->getDelegate()->show(std::move(next));
|
||||
});
|
||||
const auto showToast = crl::guard(box, [=](QString text) {
|
||||
box->showToast(text);
|
||||
});
|
||||
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
|
||||
peer,
|
||||
box->uiShow());
|
||||
shareLink = std::move(shareLinkCallback);
|
||||
box->lifetime().add(std::move(shareLinkLifetime));
|
||||
} else {
|
||||
const auto lookupLink = [=] {
|
||||
if (const auto group = peer->asMegagroup()) {
|
||||
return group->hasUsername()
|
||||
? group->session().createInternalLinkFull(
|
||||
group->username())
|
||||
: group->inviteLink();
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
return chat->inviteLink();
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
const auto canCreateLink = [&] {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
return chat->canHaveInviteLink();
|
||||
} else if (const auto group = peer->asMegagroup()) {
|
||||
return group->canHaveInviteLink();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto alreadyHasLink = !lookupLink().isEmpty();
|
||||
if (alreadyHasLink || canCreateLink()) {
|
||||
if (!alreadyHasLink) {
|
||||
// Request invite link.
|
||||
peer->session().api().requestFullPeer(peer);
|
||||
}
|
||||
const auto copyLink = [=] {
|
||||
const auto link = lookupLink();
|
||||
if (link.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
if (weakBox) {
|
||||
box->showToast(
|
||||
tr::lng_create_channel_link_copied(tr::now));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
shareLink = [=] {
|
||||
if (!copyLink() && !state->generatingLink) {
|
||||
state->generatingLink = true;
|
||||
peer->session().api().inviteLinks().create({
|
||||
peer,
|
||||
crl::guard(layout, [=](auto&&) { copyLink(); })
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if (shareLink) {
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_share(),
|
||||
st::groupCallSettingsButton
|
||||
))->addClickHandler(std::move(shareLink));
|
||||
}
|
||||
if (rtmp && !call->rtmpInfo().url.isEmpty()) {
|
||||
Ui::AddSkip(layout);
|
||||
addDivider();
|
||||
Ui::AddSkip(layout);
|
||||
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
mtpRequestId requestId;
|
||||
rpl::event_stream<RtmpInfo> data;
|
||||
};
|
||||
const auto top = box->addTopButton(st::groupCallMenuToggle);
|
||||
const auto state = top->lifetime().make_state<State>();
|
||||
const auto revokeSure = [=] {
|
||||
const auto session = &peer->session();
|
||||
state->requestId = session->api().request(
|
||||
MTPphone_GetGroupCallStreamRtmpUrl(
|
||||
MTP_flags(0),
|
||||
peer->input(),
|
||||
MTP_bool(true)
|
||||
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
|
||||
auto data = result.match([&](
|
||||
const MTPDphone_groupCallStreamRtmpUrl &data) {
|
||||
return RtmpInfo{
|
||||
.url = qs(data.vurl()),
|
||||
.key = qs(data.vkey()),
|
||||
};
|
||||
});
|
||||
if (const auto call = weakCall.get()) {
|
||||
call->setRtmpInfo(data);
|
||||
}
|
||||
if (!top) {
|
||||
return;
|
||||
}
|
||||
state->requestId = 0;
|
||||
state->data.fire(std::move(data));
|
||||
}).fail([=] {
|
||||
state->requestId = 0;
|
||||
}).send();
|
||||
};
|
||||
const auto revoke = [=] {
|
||||
if (state->requestId || !top) {
|
||||
return;
|
||||
}
|
||||
box->getDelegate()->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_group_call_rtmp_revoke_sure(),
|
||||
.confirmed = [=](Fn<void()> &&close) {
|
||||
revokeSure();
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_group_invite_context_revoke(),
|
||||
.labelStyle = &st::groupCallBoxLabel,
|
||||
}));
|
||||
};
|
||||
top->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
box,
|
||||
st::groupCallPopupMenu);
|
||||
state->menu->addAction(
|
||||
tr::lng_group_call_rtmp_revoke(tr::now),
|
||||
revoke);
|
||||
state->menu->setForcedOrigin(
|
||||
Ui::PanelAnimation::Origin::TopRight);
|
||||
top->setForceRippled(true);
|
||||
const auto raw = state->menu.get();
|
||||
raw->setDestroyedCallback([=] {
|
||||
if ((state->menu == raw) && top) {
|
||||
top->setForceRippled(false);
|
||||
}
|
||||
});
|
||||
state->menu->popup(
|
||||
top->mapToGlobal(QPoint(top->width() / 2, top->height())));
|
||||
return true;
|
||||
});
|
||||
|
||||
StartRtmpProcess::FillRtmpRows(
|
||||
layout,
|
||||
false,
|
||||
box->uiShow(),
|
||||
state->data.events(),
|
||||
&st::groupCallBoxLabel,
|
||||
&st::groupCallSettingsRtmpShowButton,
|
||||
&st::groupCallSubsectionTitle,
|
||||
&st::groupCallAttentionBoxButton,
|
||||
&st::groupCallPopupMenu);
|
||||
state->data.fire(call->rtmpInfo());
|
||||
|
||||
addDivider();
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
if (rtmp) {
|
||||
const auto volumeItem = layout->add(
|
||||
object_ptr<MenuVolumeItem>(
|
||||
layout,
|
||||
st::groupCallVolumeSettings,
|
||||
st::groupCallVolumeSettingsSlider,
|
||||
call->otherParticipantStateValue(
|
||||
) | rpl::filter([=](const Group::ParticipantState &data) {
|
||||
return data.peer == peer;
|
||||
}),
|
||||
call->rtmpVolume(),
|
||||
Group::kMaxVolume,
|
||||
false,
|
||||
st::groupCallVolumeSettingsPadding));
|
||||
|
||||
const auto toggleMute = crl::guard(layout, [=](bool m, bool local) {
|
||||
if (call) {
|
||||
call->toggleMute({
|
||||
.peer = peer,
|
||||
.mute = m,
|
||||
.locallyOnly = local,
|
||||
});
|
||||
}
|
||||
});
|
||||
const auto changeVolume = crl::guard(layout, [=](int v, bool local) {
|
||||
if (call) {
|
||||
call->changeVolume({
|
||||
.peer = peer,
|
||||
.volume = std::clamp(v, 1, Group::kMaxVolume),
|
||||
.locallyOnly = local,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
volumeItem->toggleMuteLocallyRequests(
|
||||
) | rpl::on_next([=](bool muted) {
|
||||
toggleMute(muted, true);
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
volumeItem->changeVolumeLocallyRequests(
|
||||
) | rpl::on_next([=](int volume) {
|
||||
changeVolume(volume, true);
|
||||
}, volumeItem->lifetime());
|
||||
}
|
||||
|
||||
if (call->canManage()) {
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
(peer->isBroadcast()
|
||||
? tr::lng_group_call_end_channel()
|
||||
: tr::lng_group_call_end()),
|
||||
st::groupCallSettingsAttentionButton
|
||||
))->addClickHandler([=] {
|
||||
if (const auto call = weakCall.get()) {
|
||||
box->getDelegate()->show(Box(
|
||||
LeaveBox,
|
||||
call,
|
||||
true,
|
||||
BoxContext::GroupCallPanel));
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!rtmp) {
|
||||
box->setShowFinishedCallback([=] {
|
||||
// Means we finished showing the box.
|
||||
crl::on_main(box, [=] {
|
||||
state->deviceId = std::make_unique<Webrtc::DeviceResolver>(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue()));
|
||||
state->micTester = std::make_unique<Webrtc::AudioInputTester>(
|
||||
state->deviceId->value());
|
||||
state->levelUpdateTimer.callEach(kMicTestUpdateInterval);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
box->setTitle(tr::lng_group_call_settings_title());
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
if (canChangeJoinMuted
|
||||
&& muteJoined
|
||||
&& muteJoined->toggled() != joinMuted) {
|
||||
SaveCallJoinMuted(peer, id, muteJoined->toggled());
|
||||
}
|
||||
if (canChangeMessagesEnabled
|
||||
&& enableMessages
|
||||
&& enableMessages->toggled() != messagesEnabled) {
|
||||
const auto value = enableMessages->toggled();
|
||||
if (!call->conference()) {
|
||||
SaveCallMessagesEnabled(peer, id, value);
|
||||
} else if (const auto real = call->lookupReal()) {
|
||||
real->setMessagesEnabledLocally(value);
|
||||
}
|
||||
}
|
||||
}, box->lifetime());
|
||||
box->addButton(tr::lng_box_done(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
|
||||
std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
struct State {
|
||||
State(not_null<Main::Session*> session) : session(session) {
|
||||
}
|
||||
~State() {
|
||||
session->api().request(linkListenerRequestId).cancel();
|
||||
session->api().request(linkSpeakerRequestId).cancel();
|
||||
}
|
||||
|
||||
not_null<Main::Session*> session;
|
||||
std::optional<QString> linkSpeaker;
|
||||
QString linkListener;
|
||||
mtpRequestId linkListenerRequestId = 0;
|
||||
mtpRequestId linkSpeakerRequestId = 0;
|
||||
bool generatingLink = false;
|
||||
};
|
||||
const auto state = lifetime.make_state<State>(&peer->session());
|
||||
if (peer->isUser() || !peer->canManageGroupCall()) {
|
||||
state->linkSpeaker = QString();
|
||||
}
|
||||
|
||||
const auto shareReady = [=] {
|
||||
if (!state->linkSpeaker.has_value()
|
||||
|| state->linkListener.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
show->showBox(ShareInviteLinkBox(
|
||||
peer,
|
||||
*state->linkSpeaker,
|
||||
state->linkListener,
|
||||
show));
|
||||
return true;
|
||||
};
|
||||
auto callback = [=] {
|
||||
const auto real = peer->migrateToOrMe()->groupCall();
|
||||
if (shareReady() || state->generatingLink || !real) {
|
||||
return;
|
||||
}
|
||||
state->generatingLink = true;
|
||||
|
||||
state->linkListenerRequestId = peer->session().api().request(
|
||||
MTPphone_ExportGroupCallInvite(
|
||||
MTP_flags(0),
|
||||
real->input()
|
||||
)
|
||||
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
|
||||
state->linkListenerRequestId = 0;
|
||||
result.match([&](
|
||||
const MTPDphone_exportedGroupCallInvite &data) {
|
||||
state->linkListener = qs(data.vlink());
|
||||
shareReady();
|
||||
});
|
||||
}).send();
|
||||
|
||||
if (real->rtmp()) {
|
||||
state->linkSpeaker = QString();
|
||||
state->linkSpeakerRequestId = 0;
|
||||
shareReady();
|
||||
} else if (!state->linkSpeaker.has_value()) {
|
||||
using Flag = MTPphone_ExportGroupCallInvite::Flag;
|
||||
state->linkSpeakerRequestId = peer->session().api().request(
|
||||
MTPphone_ExportGroupCallInvite(
|
||||
MTP_flags(Flag::f_can_self_unmute),
|
||||
real->input())
|
||||
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
|
||||
state->linkSpeakerRequestId = 0;
|
||||
result.match([&](
|
||||
const MTPDphone_exportedGroupCallInvite &data) {
|
||||
state->linkSpeaker = qs(data.vlink());
|
||||
shareReady();
|
||||
});
|
||||
}).fail([=] {
|
||||
state->linkSpeakerRequestId = 0;
|
||||
state->linkSpeaker = QString();
|
||||
shareReady();
|
||||
}).send();
|
||||
}
|
||||
};
|
||||
return { std::move(callback), std::move(lifetime) };
|
||||
}
|
||||
|
||||
MicLevelTester::MicLevelTester(Fn<void()> show)
|
||||
: _show(std::move(show))
|
||||
, _timer([=] { check(); })
|
||||
, _deviceId(std::make_unique<Webrtc::DeviceResolver>(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue())))
|
||||
, _tester(std::make_unique<Webrtc::AudioInputTester>(_deviceId->value())) {
|
||||
_timer.callEach(kMicrophoneTooltipCheckInterval);
|
||||
}
|
||||
|
||||
bool MicLevelTester::showTooltip() const {
|
||||
return (_loudCount >= kMicrophoneTooltipAfterLoudCount);
|
||||
}
|
||||
|
||||
void MicLevelTester::check() {
|
||||
const auto level = _tester->getAndResetLevel();
|
||||
if (level >= kMicrophoneTooltipLevelThreshold) {
|
||||
_quietCount = 0;
|
||||
if (++_loudCount >= kMicrophoneTooltipAfterLoudCount) {
|
||||
_show();
|
||||
}
|
||||
} else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) {
|
||||
_quietCount = 0;
|
||||
_loudCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
49
Telegram/SourceFiles/calls/group/calls_group_settings.h
Normal file
49
Telegram/SourceFiles/calls/group/calls_group_settings.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace Webrtc {
|
||||
class AudioInputTester;
|
||||
class DeviceResolver;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
void SettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
[[nodiscard]] std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
|
||||
not_null<PeerData*> peer,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
|
||||
class MicLevelTester final {
|
||||
public:
|
||||
explicit MicLevelTester(Fn<void()> show);
|
||||
|
||||
[[nodiscard]] bool showTooltip() const;
|
||||
|
||||
private:
|
||||
void check();
|
||||
|
||||
Fn<void()> _show;
|
||||
base::Timer _timer;
|
||||
std::unique_ptr<Webrtc::DeviceResolver> _deviceId;
|
||||
std::unique_ptr<Webrtc::AudioInputTester> _tester;
|
||||
int _loudCount = 0;
|
||||
int _quietCount = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
147
Telegram/SourceFiles/calls/group/calls_group_stars_box.cpp
Normal file
147
Telegram/SourceFiles/calls/group/calls_group_stars_box.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_stars_box.h"
|
||||
|
||||
#include "boxes/send_credits_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "payments/ui/payments_reaction_box.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxStarsFallback = 10'000;
|
||||
constexpr auto kDefaultStars = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
int MaxVideoStreamStarsCount(not_null<Main::Session*> session) {
|
||||
const auto appConfig = &session->appConfig();
|
||||
return std::max(
|
||||
appConfig->get<int>(
|
||||
u"stars_groupcall_message_amount_max"_q,
|
||||
kMaxStarsFallback),
|
||||
2);
|
||||
}
|
||||
|
||||
void VideoStreamStarsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
VideoStreamStarsBoxArgs &&args) {
|
||||
args.show->session().credits().load();
|
||||
|
||||
const auto admin = args.admin;
|
||||
const auto sending = args.sending;
|
||||
auto submitText = [=](rpl::producer<int> amount) {
|
||||
auto nice = std::move(amount) | rpl::map([=](int count) {
|
||||
return Ui::CreditsEmojiSmall().append(
|
||||
Lang::FormatCountDecimal(count));
|
||||
});
|
||||
return admin
|
||||
? tr::lng_box_ok(tr::marked)
|
||||
: (sending
|
||||
? tr::lng_paid_reaction_button
|
||||
: tr::lng_paid_comment_button)(
|
||||
lt_stars,
|
||||
std::move(nice),
|
||||
tr::rich);
|
||||
};
|
||||
const auto &show = args.show;
|
||||
const auto session = &show->session();
|
||||
const auto max = std::max(args.min, MaxVideoStreamStarsCount(session));
|
||||
const auto chosen = std::clamp(
|
||||
args.current ? args.current : kDefaultStars,
|
||||
args.min,
|
||||
max);
|
||||
auto top = std::vector<Ui::PaidReactionTop>();
|
||||
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
|
||||
const auto peer = entry.peer;
|
||||
const auto name = peer
|
||||
? peer->shortName()
|
||||
: tr::lng_paid_react_anonymous(tr::now);
|
||||
const auto open = [=] {
|
||||
if (const auto controller = show->resolveWindow()) {
|
||||
controller->showPeerInfo(peer);
|
||||
}
|
||||
};
|
||||
top.push_back({
|
||||
.name = name,
|
||||
.photo = (peer
|
||||
? Ui::MakeUserpicThumbnail(peer)
|
||||
: Ui::MakeHiddenAuthorThumbnail()),
|
||||
.barePeerId = peer ? uint64(peer->id.value) : 0,
|
||||
.count = int(entry.count),
|
||||
.click = peer ? open : Fn<void()>(),
|
||||
.my = (entry.my == 1),
|
||||
});
|
||||
};
|
||||
|
||||
top.reserve(args.top.size() + 1);
|
||||
for (const auto &entry : args.top) {
|
||||
add(entry);
|
||||
}
|
||||
|
||||
auto myAdded = base::flat_set<uint64>();
|
||||
const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);
|
||||
if (i != end(top)) {
|
||||
myAdded.emplace(i->barePeerId);
|
||||
}
|
||||
const auto myCount = uint32((i != end(top)) ? i->count : 0);
|
||||
const auto myAdd = [&](not_null<PeerData*> peer) {
|
||||
const auto barePeerId = uint64(peer->id.value);
|
||||
if (!myAdded.emplace(barePeerId).second) {
|
||||
return;
|
||||
}
|
||||
add(Data::MessageReactionsTopPaid{
|
||||
.peer = peer,
|
||||
.count = myCount,
|
||||
.my = true,
|
||||
});
|
||||
};
|
||||
myAdd(session->user());
|
||||
ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
|
||||
const auto weak = base::make_weak(box);
|
||||
Ui::PaidReactionsBox(box, {
|
||||
.min = args.min,
|
||||
.chosen = chosen,
|
||||
.max = max,
|
||||
.top = std::move(top),
|
||||
.session = &show->session(),
|
||||
.name = args.name,
|
||||
.submit = std::move(submitText),
|
||||
.colorings = show->session().appConfig().groupCallColorings(),
|
||||
.balanceValue = session->credits().balanceValue(),
|
||||
.send = [=, save = args.save](int count, uint64 barePeerId) {
|
||||
if (!admin) {
|
||||
save(count);
|
||||
}
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
},
|
||||
.videoStreamChoosing = !sending,
|
||||
.videoStreamSending = sending,
|
||||
.videoStreamAdmin = admin,
|
||||
.dark = true,
|
||||
});
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
|
||||
VideoStreamStarsBoxArgs &&args) {
|
||||
return Box(VideoStreamStarsBox, std::move(args));
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
54
Telegram/SourceFiles/calls/group/calls_group_stars_box.h
Normal file
54
Telegram/SourceFiles/calls/group/calls_group_stars_box.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct MessageReactionsTopPaid;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
struct StarsColoring;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
[[nodiscard]] int MaxVideoStreamStarsCount(not_null<Main::Session*> session);
|
||||
|
||||
struct VideoStreamStarsBoxArgs {
|
||||
std::shared_ptr<ChatHelpers::Show> show;
|
||||
std::vector<Data::MessageReactionsTopPaid> top;
|
||||
int min = 0;
|
||||
int current = 0;
|
||||
bool sending = false;
|
||||
bool admin = false;
|
||||
Fn<void(int)> save;
|
||||
QString name;
|
||||
};
|
||||
|
||||
void VideoStreamStarsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
VideoStreamStarsBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
|
||||
VideoStreamStarsBoxArgs &&args);
|
||||
|
||||
} // namespace Calls::Group
|
||||
183
Telegram/SourceFiles/calls/group/calls_group_toasts.cpp
Normal file
183
Telegram/SourceFiles/calls/group/calls_group_toasts.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_toasts.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorDuration = 2 * crl::time(1000);
|
||||
|
||||
using State = GroupCall::State;
|
||||
|
||||
} // namespace
|
||||
|
||||
Toasts::Toasts(not_null<Panel*> panel)
|
||||
: _panel(panel)
|
||||
, _call(panel->call()) {
|
||||
setup();
|
||||
}
|
||||
|
||||
void Toasts::setup() {
|
||||
setupJoinAsChanged();
|
||||
setupTitleChanged();
|
||||
setupRequestedToSpeak();
|
||||
setupAllowedToSpeak();
|
||||
setupPinnedVideo();
|
||||
setupError();
|
||||
}
|
||||
|
||||
void Toasts::setupJoinAsChanged() {
|
||||
_call->rejoinEvents(
|
||||
) | rpl::filter([](RejoinEvent event) {
|
||||
return (event.wasJoinAs != event.nowJoinAs);
|
||||
}) | rpl::map([=] {
|
||||
return _call->stateValue() | rpl::filter([](State state) {
|
||||
return (state == State::Joined);
|
||||
}) | rpl::take(1);
|
||||
}) | rpl::flatten_latest() | rpl::on_next([=] {
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_join_as_changed_channel
|
||||
: tr::lng_group_call_join_as_changed)(
|
||||
tr::now,
|
||||
lt_name,
|
||||
tr::bold(_call->joinAs()->name()),
|
||||
tr::marked));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupTitleChanged() {
|
||||
_call->titleChanged(
|
||||
) | rpl::filter([=] {
|
||||
return (_call->lookupReal() != nullptr);
|
||||
}) | rpl::map([=] {
|
||||
const auto peer = _call->peer();
|
||||
return peer->groupCall()->title().isEmpty()
|
||||
? peer->name()
|
||||
: peer->groupCall()->title();
|
||||
}) | rpl::on_next([=](const QString &title) {
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_title_changed_channel
|
||||
: tr::lng_group_call_title_changed)(
|
||||
tr::now,
|
||||
lt_title,
|
||||
tr::bold(title),
|
||||
tr::marked));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupAllowedToSpeak() {
|
||||
_call->allowedToSpeakNotifications(
|
||||
) | rpl::on_next([=] {
|
||||
if (_panel->isActive()) {
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_can_speak_here(tr::now));
|
||||
} else {
|
||||
const auto real = _call->lookupReal();
|
||||
const auto name = (real && !real->title().isEmpty())
|
||||
? real->title()
|
||||
: _call->peer()->name();
|
||||
Ui::Toast::Show({
|
||||
.text = tr::lng_group_call_can_speak(
|
||||
tr::now,
|
||||
lt_chat,
|
||||
tr::bold(name),
|
||||
tr::marked),
|
||||
});
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupPinnedVideo() {
|
||||
_call->videoEndpointPinnedValue(
|
||||
) | rpl::map([=](bool pinned) {
|
||||
return pinned
|
||||
? _call->videoEndpointLargeValue()
|
||||
: rpl::single(_call->videoEndpointLarge());
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=] {
|
||||
return (_call->shownVideoTracks().size() > 1);
|
||||
}) | rpl::on_next([=](const VideoEndpoint &endpoint) {
|
||||
const auto pinned = _call->videoEndpointPinned();
|
||||
const auto peer = endpoint.peer;
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
const auto text = [&] {
|
||||
const auto me = (peer == _call->joinAs());
|
||||
const auto camera = (endpoint.type == VideoEndpointType::Camera);
|
||||
if (me) {
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera_me
|
||||
: tr::lng_group_call_unpinned_camera_me)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen_me
|
||||
: tr::lng_group_call_unpinned_screen_me);
|
||||
return key(tr::now);
|
||||
}
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera
|
||||
: tr::lng_group_call_unpinned_camera)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen
|
||||
: tr::lng_group_call_unpinned_screen);
|
||||
return key(tr::now, lt_user, peer->shortName());
|
||||
}();
|
||||
_panel->uiShow()->showToast(text);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupRequestedToSpeak() {
|
||||
_call->mutedValue(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::on_next([=](MuteState was, MuteState now) {
|
||||
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_tooltip_raised_hand(tr::now));
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupError() {
|
||||
_call->errors(
|
||||
) | rpl::on_next([=](Error error) {
|
||||
const auto key = [&] {
|
||||
switch (error) {
|
||||
case Error::NoCamera: return tr::lng_call_error_no_camera;
|
||||
case Error::CameraFailed:
|
||||
return tr::lng_group_call_failed_camera;
|
||||
case Error::ScreenFailed:
|
||||
return tr::lng_group_call_failed_screen;
|
||||
case Error::MutedNoCamera:
|
||||
return tr::lng_group_call_muted_no_camera;
|
||||
case Error::MutedNoScreen:
|
||||
return tr::lng_group_call_muted_no_screen;
|
||||
case Error::DisabledNoCamera:
|
||||
return tr::lng_group_call_chat_no_camera;
|
||||
case Error::DisabledNoScreen:
|
||||
return tr::lng_group_call_chat_no_screen;
|
||||
}
|
||||
Unexpected("Error in Calls::Group::Toasts::setupErrorToasts.");
|
||||
}();
|
||||
_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
38
Telegram/SourceFiles/calls/group/calls_group_toasts.h
Normal file
38
Telegram/SourceFiles/calls/group/calls_group_toasts.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Panel;
|
||||
|
||||
class Toasts final {
|
||||
public:
|
||||
explicit Toasts(not_null<Panel*> panel);
|
||||
|
||||
private:
|
||||
void setup();
|
||||
void setupJoinAsChanged();
|
||||
void setupTitleChanged();
|
||||
void setupRequestedToSpeak();
|
||||
void setupAllowedToSpeak();
|
||||
void setupPinnedVideo();
|
||||
void setupError();
|
||||
|
||||
const not_null<Panel*> _panel;
|
||||
const not_null<GroupCall*> _call;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
1039
Telegram/SourceFiles/calls/group/calls_group_viewport.cpp
Normal file
1039
Telegram/SourceFiles/calls/group/calls_group_viewport.cpp
Normal file
File diff suppressed because it is too large
Load Diff
234
Telegram/SourceFiles/calls/group/calls_group_viewport.h
Normal file
234
Telegram/SourceFiles/calls/group/calls_group_viewport.h
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Painter;
|
||||
class QOpenGLFunctions;
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class RpWidgetWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::GL {
|
||||
enum class Backend;
|
||||
struct Capabilities;
|
||||
struct ChosenRenderer;
|
||||
class Renderer;
|
||||
} // namespace Ui::GL
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct VideoEndpoint;
|
||||
struct VideoQualityRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Webrtc {
|
||||
class VideoTrack;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class MembersRow;
|
||||
enum class PanelMode;
|
||||
enum class VideoQuality;
|
||||
|
||||
struct VideoTileTrack {
|
||||
Webrtc::VideoTrack *track = nullptr;
|
||||
MembersRow *row = nullptr;
|
||||
rpl::variable<QSize> trackSize;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return track != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool operator==(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return (a.track == b.track) && (a.row == b.row);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool operator!=(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
class Viewport final {
|
||||
public:
|
||||
Viewport(
|
||||
not_null<QWidget*> parent,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend,
|
||||
Ui::RpWidgetWrap *borrowedRp = nullptr,
|
||||
bool borrowedOpenGL = false);
|
||||
~Viewport();
|
||||
|
||||
[[nodiscard]] not_null<QWidget*> widget() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
|
||||
|
||||
void setMode(PanelMode mode, not_null<QWidget*> parent);
|
||||
void setControlsShown(float64 shown);
|
||||
void setCursorShown(bool shown);
|
||||
void setGeometry(bool fullscreen, QRect geometry);
|
||||
void resizeToWidth(int newWidth);
|
||||
void setScrollTop(int scrollTop);
|
||||
|
||||
void add(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
bool self);
|
||||
void remove(const VideoEndpoint &endpoint);
|
||||
void showLarge(const VideoEndpoint &endpoint);
|
||||
|
||||
[[nodiscard]] bool requireARGB32() const;
|
||||
[[nodiscard]] int fullHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> fullHeightValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> pinToggled() const;
|
||||
[[nodiscard]] rpl::producer<VideoEndpoint> clicks() const;
|
||||
[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;
|
||||
|
||||
void ensureBorrowedRenderer(QOpenGLFunctions &f);
|
||||
void ensureBorrowedCleared(QOpenGLFunctions *f);
|
||||
void borrowedPaint(QOpenGLFunctions &f);
|
||||
|
||||
void ensureBorrowedRenderer();
|
||||
void ensureBorrowedCleared();
|
||||
void borrowedPaint(Painter &p, const QRegion &clip);
|
||||
|
||||
[[nodiscard]] QPoint borrowedOrigin() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
static constexpr auto kShadowMaxAlpha = 80;
|
||||
|
||||
private:
|
||||
struct Textures;
|
||||
class VideoTile;
|
||||
class RendererSW;
|
||||
class RendererGL;
|
||||
using TileId = quintptr;
|
||||
|
||||
struct Geometry {
|
||||
VideoTile *tile = nullptr;
|
||||
QSize size;
|
||||
QRect rows;
|
||||
QRect columns;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
std::vector<Geometry> list;
|
||||
QSize outer;
|
||||
bool useColumns = false;
|
||||
};
|
||||
|
||||
struct TileAnimation {
|
||||
QSize from;
|
||||
QSize to;
|
||||
float64 ratio = -1.;
|
||||
};
|
||||
|
||||
struct Selection {
|
||||
enum class Element {
|
||||
None,
|
||||
Tile,
|
||||
PinButton,
|
||||
BackButton,
|
||||
};
|
||||
VideoTile *tile = nullptr;
|
||||
Element element = Element::None;
|
||||
|
||||
inline bool operator==(Selection other) const {
|
||||
return (tile == other.tile) && (element == other.element);
|
||||
}
|
||||
};
|
||||
|
||||
void setup();
|
||||
[[nodiscard]] bool wide() const;
|
||||
[[nodiscard]] bool videoStream() const;
|
||||
|
||||
void updateCursor();
|
||||
void updateTilesGeometry();
|
||||
void updateTilesGeometry(int outerWidth);
|
||||
void updateTilesGeometryWide(int outerWidth, int outerHeight);
|
||||
void updateTilesGeometryNarrow(int outerWidth);
|
||||
void updateTilesGeometryColumn(int outerWidth);
|
||||
void setTileGeometry(not_null<VideoTile*> tile, QRect geometry);
|
||||
void refreshHasTwoOrMore();
|
||||
void updateTopControlsVisibility();
|
||||
|
||||
void prepareLargeChangeAnimation();
|
||||
void startLargeChangeAnimation();
|
||||
void updateTilesAnimated();
|
||||
[[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const;
|
||||
[[nodiscard]] Layout applyLarge(Layout layout) const;
|
||||
|
||||
void setSelected(Selection value);
|
||||
void setPressed(Selection value);
|
||||
|
||||
void handleMousePress(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseRelease(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseMove(QPoint position);
|
||||
void updateSelected(QPoint position);
|
||||
void updateSelected();
|
||||
|
||||
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
|
||||
Ui::GL::Backend backend);
|
||||
[[nodiscard]] std::unique_ptr<Ui::GL::Renderer> makeRenderer();
|
||||
void updateMyWidgetPart();
|
||||
|
||||
PanelMode _mode = PanelMode();
|
||||
bool _opengl = false;
|
||||
const std::unique_ptr<Ui::RpWidgetWrap> _content;
|
||||
std::vector<std::unique_ptr<VideoTile>> _tiles;
|
||||
std::vector<not_null<VideoTile*>> _tilesForOrder;
|
||||
rpl::variable<int> _fullHeight = 0;
|
||||
bool _hasTwoOrMore = false;
|
||||
bool _fullscreen = false;
|
||||
bool _cursorHidden = false;
|
||||
int _scrollTop = 0;
|
||||
QImage _shadow;
|
||||
rpl::event_stream<VideoEndpoint> _clicks;
|
||||
rpl::event_stream<bool> _pinToggles;
|
||||
rpl::event_stream<VideoQualityRequest> _qualityRequests;
|
||||
float64 _controlsShownRatio = 1.;
|
||||
VideoTile *_large = nullptr;
|
||||
Fn<void()> _updateLargeScheduled;
|
||||
Ui::Animations::Simple _largeChangeAnimation;
|
||||
Layout _startTilesLayout;
|
||||
Layout _finishTilesLayout;
|
||||
Selection _selected;
|
||||
Selection _pressed;
|
||||
rpl::variable<bool> _mouseInside = false;
|
||||
|
||||
Ui::RpWidgetWrap * const _borrowed = nullptr;
|
||||
QRect _borrowedGeometry;
|
||||
std::unique_ptr<Ui::GL::Renderer> _borrowedRenderer;
|
||||
QMetaObject::Connection _borrowedConnection;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage GenerateShadow(
|
||||
int height,
|
||||
int topAlpha,
|
||||
int bottomAlpha,
|
||||
QColor color = QColor(0, 0, 0));
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> MuteButtonTooltip(
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
} // namespace Calls::Group
|
||||
1482
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
Normal file
1482
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
Normal file
File diff suppressed because it is too large
Load Diff
165
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h
Normal file
165
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/gl/gl_primitives.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_image.h"
|
||||
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLShaderProgram>
|
||||
|
||||
namespace Webrtc {
|
||||
struct FrameWithInfo;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::RendererGL final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererGL(not_null<Viewport*> owner);
|
||||
|
||||
void init(QOpenGLFunctions &f) override;
|
||||
|
||||
void deinit(QOpenGLFunctions *f) override;
|
||||
|
||||
void paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
std::optional<QColor> clearColor() override;
|
||||
|
||||
private:
|
||||
struct TileData {
|
||||
quintptr id = 0;
|
||||
not_null<PeerData*> peer;
|
||||
Ui::GL::Textures<5> textures;
|
||||
Ui::GL::Framebuffers<2> framebuffers;
|
||||
Ui::Animations::Simple outlined;
|
||||
Ui::Animations::Simple paused;
|
||||
QImage userpicFrame;
|
||||
QRect nameRect;
|
||||
int nameVersion = 0;
|
||||
mutable int trackIndex = -1;
|
||||
mutable QSize rgbaSize;
|
||||
mutable QSize textureSize;
|
||||
mutable QSize textureChromaSize;
|
||||
mutable QSize textureBlurSize;
|
||||
bool stale = false;
|
||||
bool pause = false;
|
||||
bool outline = false;
|
||||
};
|
||||
struct Program {
|
||||
std::optional<QOpenGLShaderProgram> argb32;
|
||||
std::optional<QOpenGLShaderProgram> yuv420;
|
||||
};
|
||||
|
||||
void setDefaultViewport(QOpenGLFunctions &f);
|
||||
void paintTile(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject,
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &nameData);
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(
|
||||
const Ui::GL::Rect &raster) const;
|
||||
|
||||
void ensureARGB32Program();
|
||||
void ensureButtonsImage();
|
||||
void prepareObjects(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void bindFrame(
|
||||
QOpenGLFunctions &f,
|
||||
const Webrtc::FrameWithInfo &data,
|
||||
TileData &tileData,
|
||||
Program &program);
|
||||
void drawDownscalePass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData);
|
||||
void drawFirstBlurPass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void validateDatas();
|
||||
void validateNoiseTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject);
|
||||
void validateOutlineAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validatePausedAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &tileData);
|
||||
|
||||
void uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const;
|
||||
|
||||
[[nodiscard]] bool isExpanded(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
QSize tileSize) const;
|
||||
[[nodiscard]] float64 countExpandRatio(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
const TileAnimation &animation) const;
|
||||
|
||||
const not_null<Viewport*> _owner;
|
||||
|
||||
GLfloat _factor = 1.;
|
||||
int _ifactor = 1;
|
||||
QSize _viewport;
|
||||
bool _rgbaFrame = false;
|
||||
bool _userpicFrame;
|
||||
std::optional<QOpenGLBuffer> _frameBuffer;
|
||||
Program _downscaleProgram;
|
||||
std::optional<QOpenGLShaderProgram> _blurProgram;
|
||||
Program _frameProgram;
|
||||
std::optional<QOpenGLShaderProgram> _imageProgram;
|
||||
Ui::GL::Textures<1> _noiseTexture;
|
||||
Ui::GL::Framebuffers<1> _noiseFramebuffer;
|
||||
QOpenGLShader *_downscaleVertexShader = nullptr;
|
||||
QOpenGLShader *_frameVertexShader = nullptr;
|
||||
|
||||
Ui::GL::Image _buttons;
|
||||
QRect _pinOn;
|
||||
QRect _pinOff;
|
||||
QRect _back;
|
||||
QRect _muteOn;
|
||||
QRect _muteOff;
|
||||
QRect _paused;
|
||||
|
||||
Ui::GL::Image _names;
|
||||
QRect _pausedTextRect;
|
||||
std::vector<TileData> _tileData;
|
||||
std::vector<int> _tileDataIndices;
|
||||
|
||||
Ui::CrossLineAnimation _pinIcon;
|
||||
Ui::CrossLineAnimation _muteIcon;
|
||||
|
||||
Ui::RoundRect _pinBackground;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
347
Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
Normal file
347
Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_viewport_raster.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_viewport_tile.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBlurRadius = 15;
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::RendererSW::RendererSW(not_null<Viewport*> owner)
|
||||
: _owner(owner)
|
||||
, _pinIcon(st::groupCallVideoTile.pin)
|
||||
, _pinBackground(
|
||||
(st::groupCallVideoTile.pinPadding.top()
|
||||
+ st::groupCallVideoTile.pin.icon.height()
|
||||
+ st::groupCallVideoTile.pinPadding.bottom()) / 2,
|
||||
st::radialBg) {
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) {
|
||||
auto bg = clip;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto bounding = clip.boundingRect();
|
||||
for (auto &[tile, tileData] : _tileData) {
|
||||
tileData.stale = true;
|
||||
}
|
||||
for (const auto &tile : _owner->_tiles) {
|
||||
if (!tile->visible()) {
|
||||
continue;
|
||||
}
|
||||
paintTile(p, tile.get(), bounding, bg);
|
||||
}
|
||||
if (_owner->borrowedOrigin().isNull()) {
|
||||
const auto fullscreen = _owner->_fullscreen;
|
||||
const auto color = fullscreen
|
||||
? QColor(0, 0, 0)
|
||||
: st::groupCallBg->c;
|
||||
for (const auto &rect : bg) {
|
||||
p.fillRect(rect, color);
|
||||
}
|
||||
}
|
||||
for (auto i = _tileData.begin(); i != _tileData.end();) {
|
||||
if (i->second.stale) {
|
||||
i = _tileData.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data) {
|
||||
if (!_userpicFrame) {
|
||||
data.userpicFrame = QImage();
|
||||
return;
|
||||
} else if (!data.userpicFrame.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto size = tile->trackOrUserpicSize();
|
||||
data.userpicFrame = Images::BlurLargeImage(
|
||||
PeerData::GenerateUserpicImage(
|
||||
tile->peer(),
|
||||
tile->row()->ensureUserpicView(),
|
||||
size.width(),
|
||||
0),
|
||||
kBlurRadius);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTile(
|
||||
Painter &p,
|
||||
not_null<VideoTile*> tile,
|
||||
const QRect &clip,
|
||||
QRegion &bg) {
|
||||
const auto track = tile->track();
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
tile->track()->markFrameShown();
|
||||
});
|
||||
const auto data = track->frameWithInfo(true);
|
||||
auto &tileData = _tileData[tile];
|
||||
tileData.stale = false;
|
||||
_userpicFrame = (data.format == Webrtc::FrameFormat::None);
|
||||
_pausedFrame = (track->state() == Webrtc::VideoState::Paused);
|
||||
validateUserpicFrame(tile, tileData);
|
||||
if (_userpicFrame || !_pausedFrame) {
|
||||
tileData.blurredFrame = QImage();
|
||||
} else if (tileData.blurredFrame.isNull()) {
|
||||
tileData.blurredFrame = Images::BlurLargeImage(
|
||||
data.original.scaled(
|
||||
VideoTile::PausedVideoSize(),
|
||||
Qt::KeepAspectRatio).mirrored(tile->mirror(), false),
|
||||
kBlurRadius);
|
||||
}
|
||||
const auto &image = _userpicFrame
|
||||
? tileData.userpicFrame
|
||||
: _pausedFrame
|
||||
? tileData.blurredFrame
|
||||
: data.original.mirrored(tile->mirror(), false);
|
||||
const auto frameRotation = _userpicFrame ? 0 : data.rotation;
|
||||
Assert(!image.isNull());
|
||||
|
||||
const auto background = _owner->_fullscreen
|
||||
? QColor(0, 0, 0)
|
||||
: st::groupCallMembersBg->c;
|
||||
const auto fill = [&](QRect rect) {
|
||||
const auto intersected = rect.intersected(clip);
|
||||
if (!intersected.isEmpty()) {
|
||||
p.fillRect(intersected, background);
|
||||
bg -= intersected;
|
||||
}
|
||||
};
|
||||
|
||||
using namespace Media::View;
|
||||
const auto geometry = tile->geometry().translated(
|
||||
_owner->borrowedOrigin());
|
||||
const auto x = geometry.x();
|
||||
const auto y = geometry.y();
|
||||
const auto width = geometry.width();
|
||||
const auto height = geometry.height();
|
||||
const auto scaled = FlipSizeByRotation(
|
||||
image.size(),
|
||||
frameRotation
|
||||
).scaled(QSize(width, height), Qt::KeepAspectRatio);
|
||||
const auto left = (width - scaled.width()) / 2;
|
||||
const auto top = (height - scaled.height()) / 2;
|
||||
const auto target = QRect(QPoint(x + left, y + top), scaled);
|
||||
if (UsePainterRotation(frameRotation)) {
|
||||
if (frameRotation) {
|
||||
p.save();
|
||||
p.rotate(frameRotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(target, frameRotation), image);
|
||||
if (frameRotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (frameRotation) {
|
||||
p.drawImage(target, RotateFrameImage(image, frameRotation));
|
||||
} else {
|
||||
p.drawImage(target, image);
|
||||
}
|
||||
bg -= target;
|
||||
|
||||
if (left > 0) {
|
||||
fill({ x, y, left, height });
|
||||
}
|
||||
if (const auto right = left + scaled.width(); right < width) {
|
||||
fill({ x + right, y, width - right, height });
|
||||
}
|
||||
if (top > 0) {
|
||||
fill({ x, y, width, top });
|
||||
}
|
||||
if (const auto bottom = top + scaled.height(); bottom < height) {
|
||||
fill({ x, y + bottom, width, height - bottom });
|
||||
}
|
||||
|
||||
paintTileControls(p, x, y, width, height, tile);
|
||||
paintTileOutline(p, x, y, width, height, tile);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileOutline(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
if (!tile->row()->speaking()) {
|
||||
return;
|
||||
}
|
||||
const auto outline = st::groupCallOutline;
|
||||
const auto &color = st::groupCallMemberActiveIcon;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.fillRect(x, y, outline, height - outline, color);
|
||||
p.fillRect(x + outline, y, width - outline, outline, color);
|
||||
p.fillRect(
|
||||
x + width - outline,
|
||||
y + outline,
|
||||
outline,
|
||||
height - outline,
|
||||
color);
|
||||
p.fillRect(x, y + height - outline, width - outline, outline, color);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileControls(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
p.setClipRect(x, y, width, height);
|
||||
const auto guard = gsl::finally([&] { p.setClipping(false); });
|
||||
|
||||
const auto wide = _owner->wide();
|
||||
if (wide) {
|
||||
// Pin.
|
||||
const auto pinInner = tile->pinInner();
|
||||
VideoTile::PaintPinButton(
|
||||
p,
|
||||
tile->pinned(),
|
||||
x + pinInner.x(),
|
||||
y + pinInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground,
|
||||
&_pinIcon);
|
||||
|
||||
// Back.
|
||||
const auto backInner = tile->backInner();
|
||||
VideoTile::PaintBackButton(
|
||||
p,
|
||||
x + backInner.x(),
|
||||
y + backInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground);
|
||||
}
|
||||
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto nameTop = y + (height
|
||||
- st.namePosition.y()
|
||||
- st::semiboldFont->height);
|
||||
|
||||
if (_pausedFrame) {
|
||||
p.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha));
|
||||
|
||||
const auto middle = (st::groupCallVideoPlaceholderHeight
|
||||
- st::groupCallPaused.height()) / 2;
|
||||
const auto pausedSpace = (nameTop - y)
|
||||
- st::groupCallPaused.height()
|
||||
- st::semiboldFont->height;
|
||||
const auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop;
|
||||
const auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop
|
||||
- st::groupCallVideoPlaceholderIconTop;
|
||||
const auto pauseIconTop = !_owner->wide()
|
||||
? (y + (height - st::groupCallPaused.height()) / 2)
|
||||
: (pausedSpace < 3 * st::semiboldFont->height)
|
||||
? (pausedSpace / 3)
|
||||
: std::min(
|
||||
y + (height / 2) - pauseIconSkip,
|
||||
(nameTop
|
||||
- st::semiboldFont->height * 3
|
||||
- st::groupCallPaused.height()));
|
||||
const auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height)
|
||||
? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)
|
||||
: std::min(
|
||||
pauseIconTop + pauseTextSkip,
|
||||
nameTop - st::semiboldFont->height * 2);
|
||||
|
||||
st::groupCallPaused.paint(
|
||||
p,
|
||||
x + (width - st::groupCallPaused.width()) / 2,
|
||||
pauseIconTop,
|
||||
width);
|
||||
if (_owner->wide()) {
|
||||
p.drawText(
|
||||
QRect(x, pauseTextTop, width, y + height - pauseTextTop),
|
||||
tr::lng_group_call_video_paused(tr::now),
|
||||
style::al_top);
|
||||
}
|
||||
}
|
||||
|
||||
const auto shown = _owner->_controlsShownRatio;
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fullShift = st.namePosition.y() + st::normalFont->height;
|
||||
const auto shift = anim::interpolate(fullShift, 0, shown);
|
||||
|
||||
// Shadow.
|
||||
if (_shadow.isNull()) {
|
||||
_shadow = Images::GenerateShadow(
|
||||
st.shadowHeight,
|
||||
0,
|
||||
kShadowMaxAlpha);
|
||||
}
|
||||
const auto shadowRect = QRect(
|
||||
x,
|
||||
y + (height - anim::interpolate(0, st.shadowHeight, shown)),
|
||||
width,
|
||||
st.shadowHeight);
|
||||
const auto shadowFill = shadowRect.intersected({ x, y, width, height });
|
||||
if (shadowFill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
if (!_pausedFrame) {
|
||||
p.drawImage(
|
||||
shadowFill,
|
||||
_shadow,
|
||||
QRect(
|
||||
0,
|
||||
(shadowFill.y() - shadowRect.y()) * factor,
|
||||
_shadow.width(),
|
||||
shadowFill.height() * factor));
|
||||
}
|
||||
const auto row = tile->row();
|
||||
row->lazyInitialize(st::groupCallMembersListItem);
|
||||
|
||||
// Mute.
|
||||
const auto &icon = st::groupCallVideoCrossLine.icon;
|
||||
const auto iconLeft = x + width - st.iconPosition.x() - icon.width();
|
||||
const auto iconTop = y + (height
|
||||
- st.iconPosition.y()
|
||||
- icon.height()
|
||||
+ shift);
|
||||
row->paintMuteIcon(
|
||||
p,
|
||||
{ iconLeft, iconTop, icon.width(), icon.height() },
|
||||
MembersRowStyle::Video);
|
||||
|
||||
// Name.
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
const auto hasWidth = width
|
||||
- st.iconPosition.x() - icon.width()
|
||||
- st.namePosition.x();
|
||||
const auto nameLeft = x + st.namePosition.x();
|
||||
row->name().drawLeftElided(
|
||||
p,
|
||||
nameLeft,
|
||||
nameTop + shift,
|
||||
hasWidth,
|
||||
width);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::RendererSW final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererSW(not_null<Viewport*> owner);
|
||||
|
||||
void paintFallback(
|
||||
Painter &p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) override;
|
||||
|
||||
private:
|
||||
struct TileData {
|
||||
QImage userpicFrame;
|
||||
QImage blurredFrame;
|
||||
bool stale = false;
|
||||
};
|
||||
void paintTile(
|
||||
Painter &p,
|
||||
not_null<VideoTile*> tile,
|
||||
const QRect &clip,
|
||||
QRegion &bg);
|
||||
void paintTileOutline(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile);
|
||||
void paintTileControls(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile);
|
||||
void validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
|
||||
const not_null<Viewport*> _owner;
|
||||
|
||||
QImage _shadow;
|
||||
bool _userpicFrame = false;
|
||||
bool _pausedFrame = false;
|
||||
base::flat_map<not_null<VideoTile*>, TileData> _tileData;
|
||||
Ui::CrossLineAnimation _pinIcon;
|
||||
Ui::RoundRect _pinBackground;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
278
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp
Normal file
278
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_viewport_tile.h"
|
||||
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtGui/QOpenGLFunctions>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPausedVideoSize = 90;
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::VideoTile::VideoTile(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
Fn<void()> update,
|
||||
bool self)
|
||||
: _endpoint(endpoint)
|
||||
, _update(std::move(update))
|
||||
, _track(std::move(track))
|
||||
, _peer(_track.row->peer())
|
||||
, _trackSize(std::move(trackSize))
|
||||
, _rtmp(endpoint.rtmp())
|
||||
, _self(self) {
|
||||
Expects(_track.track != nullptr);
|
||||
|
||||
using namespace rpl::mappers;
|
||||
_track.track->stateValue(
|
||||
) | rpl::filter(
|
||||
_1 == Webrtc::VideoState::Paused
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
_wasPaused = true;
|
||||
}, _lifetime);
|
||||
|
||||
setup(std::move(pinned));
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::mirror() const {
|
||||
return _self && (_endpoint.type == VideoEndpointType::Camera);
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinOuter() const {
|
||||
return _pinOuter;
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinInner() const {
|
||||
return _pinInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backOuter() const {
|
||||
return _backOuter;
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backInner() const {
|
||||
return _backInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
int Viewport::VideoTile::topControlsSlide() const {
|
||||
return anim::interpolate(
|
||||
st::groupCallVideoTile.pinPosition.y() + _pinInner.height(),
|
||||
0,
|
||||
_topControlsShownAnimation.value(_topControlsShown ? 1. : 0.));
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::PausedVideoSize() {
|
||||
return QSize(kPausedVideoSize, kPausedVideoSize);
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::trackOrUserpicSize() const {
|
||||
if (const auto size = trackSize(); !size.isEmpty()) {
|
||||
return size;
|
||||
}
|
||||
return _wasPaused ? PausedVideoSize() : QSize();
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::screencast() const {
|
||||
return (_endpoint.type == VideoEndpointType::Screen);
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::setGeometry(
|
||||
QRect geometry,
|
||||
TileAnimation animation) {
|
||||
_hidden = false;
|
||||
_geometry = geometry;
|
||||
_animation = animation;
|
||||
updateTopControlsPosition();
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::hide() {
|
||||
_hidden = true;
|
||||
_quality = std::nullopt;
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::toggleTopControlsShown(bool shown) {
|
||||
if (_topControlsShown == shown) {
|
||||
return;
|
||||
}
|
||||
_topControlsShown = shown;
|
||||
_topControlsShownAnimation.start(
|
||||
_update,
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration);
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) {
|
||||
if (_hidden) {
|
||||
_quality = std::nullopt;
|
||||
return false;
|
||||
} else if (_quality && *_quality == quality) {
|
||||
return false;
|
||||
}
|
||||
_quality = quality;
|
||||
return true;
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::PinInnerSize(bool pinned) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto &icon = st::groupCallVideoTile.pin.icon;
|
||||
const auto innerWidth = icon.width()
|
||||
+ st.pinTextPosition.x()
|
||||
+ st::semiboldFont->width(pinned
|
||||
? tr::lng_pinned_unpin(tr::now)
|
||||
: tr::lng_pinned_pin(tr::now));
|
||||
const auto innerHeight = icon.height();
|
||||
const auto buttonWidth = st.pinPadding.left()
|
||||
+ innerWidth
|
||||
+ st.pinPadding.right();
|
||||
const auto buttonHeight = st.pinPadding.top()
|
||||
+ innerHeight
|
||||
+ st.pinPadding.bottom();
|
||||
return { buttonWidth, buttonHeight };
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::PaintPinButton(
|
||||
Painter &p,
|
||||
bool pinned,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background,
|
||||
not_null<Ui::CrossLineAnimation*> icon) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto rect = QRect(QPoint(x, y), PinInnerSize(pinned));
|
||||
background->paint(p, rect);
|
||||
icon->paint(
|
||||
p,
|
||||
rect.marginsRemoved(st.pinPadding).topLeft(),
|
||||
pinned ? 1. : 0.);
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
(x
|
||||
+ st.pinPadding.left()
|
||||
+ st::groupCallVideoTile.pin.icon.width()
|
||||
+ st.pinTextPosition.x()),
|
||||
(y
|
||||
+ st.pinPadding.top()
|
||||
+ st.pinTextPosition.y()),
|
||||
outerWidth,
|
||||
(pinned
|
||||
? tr::lng_pinned_unpin(tr::now)
|
||||
: tr::lng_pinned_pin(tr::now)));
|
||||
|
||||
}
|
||||
|
||||
QSize Viewport::VideoTile::BackInnerSize() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto &icon = st::groupCallVideoTile.back;
|
||||
const auto innerWidth = icon.width()
|
||||
+ st.pinTextPosition.x()
|
||||
+ st::semiboldFont->width(tr::lng_create_group_back(tr::now));
|
||||
const auto innerHeight = icon.height();
|
||||
const auto buttonWidth = st.pinPadding.left()
|
||||
+ innerWidth
|
||||
+ st.pinPadding.right();
|
||||
const auto buttonHeight = st.pinPadding.top()
|
||||
+ innerHeight
|
||||
+ st.pinPadding.bottom();
|
||||
return { buttonWidth, buttonHeight };
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto rect = QRect(QPoint(x, y), BackInnerSize());
|
||||
background->paint(p, rect);
|
||||
st.back.paint(
|
||||
p,
|
||||
rect.marginsRemoved(st.pinPadding).topLeft(),
|
||||
outerWidth);
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
(x
|
||||
+ st.pinPadding.left()
|
||||
+ st::groupCallVideoTile.pin.icon.width()
|
||||
+ st.pinTextPosition.x()),
|
||||
(y
|
||||
+ st.pinPadding.top()
|
||||
+ st.pinTextPosition.y()),
|
||||
outerWidth,
|
||||
tr::lng_create_group_back(tr::now));
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updateTopControlsSize() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
|
||||
const auto pinSize = PinInnerSize(_pinned);
|
||||
const auto pinWidth = st.pinPosition.x() * 2 + pinSize.width();
|
||||
const auto pinHeight = st.pinPosition.y() * 2 + pinSize.height();
|
||||
_pinInner = QRect(QPoint(), pinSize);
|
||||
_pinOuter = QRect(0, 0, pinWidth, pinHeight);
|
||||
|
||||
const auto backSize = BackInnerSize();
|
||||
const auto backWidth = st.pinPosition.x() * 2 + backSize.width();
|
||||
const auto backHeight = st.pinPosition.y() * 2 + backSize.height();
|
||||
_backInner = QRect(QPoint(), backSize);
|
||||
_backOuter = QRect(0, 0, backWidth, backHeight);
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updateTopControlsPosition() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
|
||||
_pinInner = QRect(
|
||||
_geometry.width() - st.pinPosition.x() - _pinInner.width(),
|
||||
st.pinPosition.y(),
|
||||
_pinInner.width(),
|
||||
_pinInner.height());
|
||||
_pinOuter = QRect(
|
||||
_geometry.width() - _pinOuter.width(),
|
||||
0,
|
||||
_pinOuter.width(),
|
||||
_pinOuter.height());
|
||||
_backInner = QRect(st.pinPosition, _backInner.size());
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::setup(rpl::producer<bool> pinned) {
|
||||
std::move(
|
||||
pinned
|
||||
) | rpl::filter([=](bool pinned) {
|
||||
return (_pinned != pinned);
|
||||
}) | rpl::on_next([=](bool pinned) {
|
||||
_pinned = pinned;
|
||||
updateTopControlsSize();
|
||||
if (!_hidden) {
|
||||
updateTopControlsPosition();
|
||||
_update();
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_track.track->renderNextFrame(
|
||||
) | rpl::on_next(_update, _lifetime);
|
||||
|
||||
updateTopControlsSize();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
142
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h
Normal file
142
Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
class Painter;
|
||||
class QOpenGLFunctions;
|
||||
|
||||
namespace Ui {
|
||||
class CrossLineAnimation;
|
||||
class RoundRect;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::VideoTile final {
|
||||
public:
|
||||
VideoTile(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<QSize> trackSize,
|
||||
rpl::producer<bool> pinned,
|
||||
Fn<void()> update,
|
||||
bool self);
|
||||
|
||||
[[nodiscard]] not_null<Webrtc::VideoTrack*> track() const {
|
||||
return _track.track;
|
||||
}
|
||||
[[nodiscard]] not_null<MembersRow*> row() const {
|
||||
return _track.row;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] bool rtmp() const {
|
||||
return _rtmp;
|
||||
}
|
||||
[[nodiscard]] QRect geometry() const {
|
||||
return _geometry;
|
||||
}
|
||||
[[nodiscard]] TileAnimation animation() const {
|
||||
return _animation;
|
||||
}
|
||||
[[nodiscard]] bool pinned() const {
|
||||
return _pinned;
|
||||
}
|
||||
[[nodiscard]] bool hidden() const {
|
||||
return _hidden;
|
||||
}
|
||||
[[nodiscard]] bool visible() const {
|
||||
return !_hidden && !_geometry.isEmpty();
|
||||
}
|
||||
[[nodiscard]] bool self() const {
|
||||
return _self;
|
||||
}
|
||||
[[nodiscard]] bool mirror() const;
|
||||
[[nodiscard]] QRect pinOuter() const;
|
||||
[[nodiscard]] QRect pinInner() const;
|
||||
[[nodiscard]] QRect backOuter() const;
|
||||
[[nodiscard]] QRect backInner() const;
|
||||
[[nodiscard]] const VideoEndpoint &endpoint() const {
|
||||
return _endpoint;
|
||||
}
|
||||
[[nodiscard]] QSize trackSize() const {
|
||||
return _trackSize.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<QSize> trackSizeValue() const {
|
||||
return _trackSize.value();
|
||||
}
|
||||
[[nodiscard]] QSize trackOrUserpicSize() const;
|
||||
[[nodiscard]] static QSize PausedVideoSize();
|
||||
|
||||
[[nodiscard]] bool screencast() const;
|
||||
void setGeometry(
|
||||
QRect geometry,
|
||||
TileAnimation animation = TileAnimation());
|
||||
void hide();
|
||||
void toggleTopControlsShown(bool shown);
|
||||
bool updateRequestedQuality(VideoQuality quality);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
[[nodiscard]] static QSize PinInnerSize(bool pinned);
|
||||
static void PaintPinButton(
|
||||
Painter &p,
|
||||
bool pinned,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background,
|
||||
not_null<Ui::CrossLineAnimation*> icon);
|
||||
|
||||
[[nodiscard]] static QSize BackInnerSize();
|
||||
static void PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background);
|
||||
|
||||
private:
|
||||
void setup(rpl::producer<bool> pinned);
|
||||
[[nodiscard]] int topControlsSlide() const;
|
||||
void updateTopControlsSize();
|
||||
void updateTopControlsPosition();
|
||||
|
||||
const VideoEndpoint _endpoint;
|
||||
const Fn<void()> _update;
|
||||
const VideoTileTrack _track;
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
QRect _geometry;
|
||||
TileAnimation _animation;
|
||||
rpl::variable<QSize> _trackSize;
|
||||
QRect _pinOuter;
|
||||
QRect _pinInner;
|
||||
QRect _backOuter;
|
||||
QRect _backInner;
|
||||
Ui::Animations::Simple _topControlsShownAnimation;
|
||||
bool _wasPaused = false;
|
||||
bool _topControlsShown = false;
|
||||
bool _pinned = false;
|
||||
bool _hidden = true;
|
||||
bool _rtmp = false;
|
||||
bool _self = false;
|
||||
std::optional<VideoQuality> _quality;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
301
Telegram/SourceFiles/calls/group/calls_volume_item.cpp
Normal file
301
Telegram/SourceFiles/calls/group/calls_volume_item.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_volume_item.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "ui/color_int_conversion.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include "ui/paint/arcs.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxVolumePercent = 200;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
10.0f / kMaxVolumePercent,
|
||||
50.0f / kMaxVolumePercent,
|
||||
150.0f / kMaxVolumePercent };
|
||||
|
||||
constexpr auto kVolumeStickedValues
|
||||
= std::array<std::pair<float64, float64>, 7>{{
|
||||
{ 25. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 50. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 75. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 100. / kMaxVolumePercent, 10. / kMaxVolumePercent },
|
||||
{ 125. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 150. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
MenuVolumeItem::MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MediaSlider &stSlider,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted,
|
||||
const QMargins &padding)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _maxVolume(maxVolume)
|
||||
, _cloudMuted(muted)
|
||||
, _localMuted(muted)
|
||||
, _slider(base::make_unique_q<Ui::MediaSlider>(this, stSlider))
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _stCross(st::groupCallMuteCrossLine)
|
||||
, _padding(padding)
|
||||
, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))
|
||||
, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
|
||||
st::groupCallSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
_localMuted ? 0. : (startVolume / float(maxVolume)),
|
||||
Ui::Paint::ArcsAnimation::Direction::Right)) {
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_slider.get());
|
||||
|
||||
_slider->setAlwaysDisplayMarker(true);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
const auto geometry = QRect(QPoint(), size);
|
||||
_itemRect = geometry - _padding;
|
||||
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
|
||||
_arcPosition = _speakerRect.center()
|
||||
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
|
||||
_slider->setGeometry(
|
||||
st::groupCallMenuVolumeMargin.left(),
|
||||
_speakerRect.y(),
|
||||
(geometry.width()
|
||||
- st::groupCallMenuVolumeMargin.left()
|
||||
- st::groupCallMenuVolumeMargin.right()),
|
||||
_speakerRect.height());
|
||||
}, lifetime());
|
||||
|
||||
setCloudVolume(startVolume);
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](const QRect &clip) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto volume = _localMuted
|
||||
? 0
|
||||
: base::SafeRound(_slider->value() * kMaxVolumePercent);
|
||||
const auto muteProgress
|
||||
= _crossLineAnimation.value((!volume) ? 1. : 0.);
|
||||
|
||||
const auto selected = isSelected();
|
||||
p.fillRect(clip, selected ? st.itemBgOver : st.itemBg);
|
||||
|
||||
const auto mutePen = anim::color(
|
||||
unmuteColor(),
|
||||
muteColor(),
|
||||
muteProgress);
|
||||
|
||||
_crossLineMute->paint(
|
||||
p,
|
||||
_speakerRect.topLeft(),
|
||||
muteProgress,
|
||||
(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
|
||||
|
||||
{
|
||||
p.translate(_arcPosition);
|
||||
_arcs->paint(p);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_slider->setChangeProgressCallback([=](float64 value) {
|
||||
const auto newMuted = (value == 0);
|
||||
if (_localMuted != newMuted) {
|
||||
_localMuted = newMuted;
|
||||
_toggleMuteLocallyRequests.fire_copy(newMuted);
|
||||
|
||||
_crossLineAnimation.start(
|
||||
[=] { update(_speakerRect); },
|
||||
_localMuted ? 0. : 1.,
|
||||
_localMuted ? 1. : 0.,
|
||||
st::callPanelDuration);
|
||||
}
|
||||
if (value > 0) {
|
||||
_changeVolumeLocallyRequests.fire(value * _maxVolume);
|
||||
}
|
||||
_arcs->setValue(value);
|
||||
updateSliderColor(value);
|
||||
});
|
||||
|
||||
const auto returnVolume = [=] {
|
||||
_changeVolumeLocallyRequests.fire_copy(_cloudVolume);
|
||||
};
|
||||
|
||||
_slider->setChangeFinishedCallback([=](float64 value) {
|
||||
const auto newVolume = base::SafeRound(value * _maxVolume);
|
||||
const auto muted = (value == 0);
|
||||
|
||||
if (!_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
_localMuted = true;
|
||||
_toggleMuteRequests.fire(true);
|
||||
}
|
||||
if (_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
}
|
||||
if (_cloudMuted && !muted) {
|
||||
_waitingForUpdateVolume = true;
|
||||
_localMuted = false;
|
||||
_toggleMuteRequests.fire(false);
|
||||
}
|
||||
if (!_cloudMuted && !muted) {
|
||||
_changeVolumeRequests.fire_copy(newVolume);
|
||||
}
|
||||
updateSliderColor(value);
|
||||
});
|
||||
|
||||
std::move(
|
||||
participantState
|
||||
) | rpl::on_next([=](const Group::ParticipantState &state) {
|
||||
const auto newMuted = state.mutedByMe;
|
||||
const auto newVolume = state.volume.value_or(0);
|
||||
|
||||
_cloudMuted = _localMuted = newMuted;
|
||||
|
||||
if (!newVolume) {
|
||||
return;
|
||||
}
|
||||
if (_waitingForUpdateVolume) {
|
||||
const auto localVolume
|
||||
= base::SafeRound(_slider->value() * _maxVolume);
|
||||
if ((localVolume != newVolume)
|
||||
&& (_cloudVolume == newVolume)) {
|
||||
_changeVolumeRequests.fire(int(localVolume));
|
||||
}
|
||||
} else {
|
||||
setCloudVolume(newVolume);
|
||||
}
|
||||
_waitingForUpdateVolume = false;
|
||||
}, lifetime());
|
||||
|
||||
_slider->setAdjustCallback([=](float64 value) {
|
||||
for (const auto &snap : kVolumeStickedValues) {
|
||||
if (value > (snap.first - snap.second)
|
||||
&& value < (snap.first + snap.second)) {
|
||||
return snap.first;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
initArcsAnimation();
|
||||
}
|
||||
|
||||
void MenuVolumeItem::initArcsAnimation() {
|
||||
const auto lastTime = lifetime().make_state<int>(0);
|
||||
_arcsAnimation.init([=](crl::time now) {
|
||||
_arcs->update(now);
|
||||
update(_speakerRect);
|
||||
});
|
||||
|
||||
_arcs->startUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
if (!_arcsAnimation.animating()) {
|
||||
*lastTime = crl::now();
|
||||
_arcsAnimation.start();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_arcs->stopUpdateRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_arcsAnimation.stop();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::unmuteColor() const {
|
||||
return (isSelected()
|
||||
? _st.itemFgOver
|
||||
: isEnabled()
|
||||
? _st.itemFg
|
||||
: _st.itemFgDisabled)->c;
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::muteColor() const {
|
||||
return (isSelected()
|
||||
? st::attentionButtonFgOver
|
||||
: st::attentionButtonFg)->c;
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setCloudVolume(int volume) {
|
||||
if (_cloudVolume == volume) {
|
||||
return;
|
||||
}
|
||||
_cloudVolume = volume;
|
||||
if (!_slider->isChanging()) {
|
||||
setSliderVolume(_cloudMuted ? 0. : volume);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setSliderVolume(int volume) {
|
||||
const auto value = float64(volume) / _maxVolume;
|
||||
_slider->setValue(value);
|
||||
updateSliderColor(value);
|
||||
}
|
||||
|
||||
void MenuVolumeItem::updateSliderColor(float64 value) {
|
||||
value = std::clamp(value, 0., 1.);
|
||||
const auto colors = std::array<QColor, 4>{ {
|
||||
Ui::ColorFromSerialized(0xF66464),
|
||||
Ui::ColorFromSerialized(0xD0B738),
|
||||
Ui::ColorFromSerialized(0x24CD80),
|
||||
Ui::ColorFromSerialized(0x3BBCEC),
|
||||
} };
|
||||
_slider->setColorOverrides({
|
||||
.activeFg = (value < 0.25)
|
||||
? anim::color(colors[0], colors[1], value / 0.25)
|
||||
: (value < 0.5)
|
||||
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
|
||||
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
|
||||
});
|
||||
}
|
||||
|
||||
not_null<QAction*> MenuVolumeItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool MenuVolumeItem::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
int MenuVolumeItem::contentHeight() const {
|
||||
return rect::m::sum::v(_padding) + _stCross.icon.height();
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {
|
||||
return _toggleMuteRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteLocallyRequests() const {
|
||||
return _toggleMuteLocallyRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeRequests() const {
|
||||
return _changeVolumeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeLocallyRequests() const {
|
||||
return _changeVolumeLocallyRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
90
Telegram/SourceFiles/calls/group/calls_volume_item.h
Normal file
90
Telegram/SourceFiles/calls/group/calls_volume_item.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
namespace Ui {
|
||||
class CrossLineAnimation;
|
||||
class MediaSlider;
|
||||
namespace Paint {
|
||||
class ArcsAnimation;
|
||||
} // namespace Paint
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
class MenuVolumeItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MediaSlider &stSlider,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted,
|
||||
const QMargins &padding);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteLocallyRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeLocallyRequests() const;
|
||||
|
||||
protected:
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void initArcsAnimation();
|
||||
|
||||
void setCloudVolume(int volume);
|
||||
void setSliderVolume(int volume);
|
||||
void updateSliderColor(float64 value);
|
||||
|
||||
QColor unmuteColor() const;
|
||||
QColor muteColor() const;
|
||||
|
||||
const int _maxVolume;
|
||||
int _cloudVolume = 0;
|
||||
bool _waitingForUpdateVolume = false;
|
||||
bool _cloudMuted = false;
|
||||
bool _localMuted = false;
|
||||
|
||||
QRect _itemRect;
|
||||
QRect _speakerRect;
|
||||
QPoint _arcPosition;
|
||||
|
||||
const base::unique_qptr<Ui::MediaSlider> _slider;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const style::CrossLineAnimation &_stCross;
|
||||
const QMargins &_padding;
|
||||
|
||||
const std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;
|
||||
Ui::Animations::Simple _crossLineAnimation;
|
||||
const std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;
|
||||
Ui::Animations::Basic _arcsAnimation;
|
||||
|
||||
rpl::event_stream<bool> _toggleMuteRequests;
|
||||
rpl::event_stream<bool> _toggleMuteLocallyRequests;
|
||||
rpl::event_stream<int> _changeVolumeRequests;
|
||||
rpl::event_stream<int> _changeVolumeLocallyRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/ui/calls_group_recording_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QSvgRenderer>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRoundRadius = 9;
|
||||
constexpr auto kMaxGroupCallLength = 40;
|
||||
constexpr auto kSwitchDuration = 200;
|
||||
|
||||
class GraphicButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
GraphicButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &filename,
|
||||
int selectWidth = 0);
|
||||
|
||||
void setToggled(bool value);
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e);
|
||||
|
||||
private:
|
||||
const style::margins _margins;
|
||||
QSvgRenderer _renderer;
|
||||
Ui::RoundRect _roundRect;
|
||||
Ui::RoundRect _roundRectSelect;
|
||||
Ui::Animations::Simple _animation;
|
||||
bool _toggled = false;
|
||||
};
|
||||
|
||||
class RecordingInfo final : public Ui::RpWidget {
|
||||
public:
|
||||
RecordingInfo(not_null<Ui::RpWidget*> parent);
|
||||
|
||||
void prepareAudio();
|
||||
void prepareVideo();
|
||||
|
||||
RecordingType type() const;
|
||||
|
||||
private:
|
||||
void setLabel(const QString &text);
|
||||
|
||||
const object_ptr<Ui::VerticalLayout> _container;
|
||||
RecordingType _type = RecordingType::AudioOnly;
|
||||
};
|
||||
|
||||
class Switcher final : public Ui::RpWidget {
|
||||
public:
|
||||
Switcher(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<bool> &&toggled);
|
||||
|
||||
RecordingType type() const;
|
||||
|
||||
private:
|
||||
const object_ptr<Ui::BoxContentDivider> _background;
|
||||
const object_ptr<RecordingInfo> _audio;
|
||||
const object_ptr<RecordingInfo> _video;
|
||||
bool _toggled = false;
|
||||
|
||||
Ui::Animations::Simple _animation;
|
||||
};
|
||||
|
||||
GraphicButton::GraphicButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &filename,
|
||||
int selectWidth)
|
||||
: AbstractButton(parent)
|
||||
, _margins(selectWidth, selectWidth, selectWidth, selectWidth)
|
||||
, _renderer(u":/gui/recording/%1.svg"_q.arg(filename))
|
||||
, _roundRect(kRoundRadius, st::groupCallMembersBg)
|
||||
, _roundRectSelect(kRoundRadius, st::groupCallActiveFg) {
|
||||
const auto size = style::ConvertScale(_renderer.defaultSize());
|
||||
resize((QRect(QPoint(), size) + _margins).size());
|
||||
}
|
||||
|
||||
void GraphicButton::setToggled(bool value) {
|
||||
if (_toggled == value) {
|
||||
return;
|
||||
}
|
||||
_toggled = value;
|
||||
_animation.start(
|
||||
[=] { update(); },
|
||||
_toggled ? 0. : 1.,
|
||||
_toggled ? 1. : 0.,
|
||||
st::universalDuration);
|
||||
}
|
||||
|
||||
void GraphicButton::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
const auto progress = _animation.value(_toggled ? 1. : 0.);
|
||||
p.setOpacity(progress);
|
||||
_roundRectSelect.paint(p, rect());
|
||||
p.setOpacity(1.);
|
||||
const auto r = rect() - _margins;
|
||||
_roundRect.paint(p, r);
|
||||
_renderer.render(&p, r);
|
||||
}
|
||||
|
||||
RecordingInfo::RecordingInfo(not_null<Ui::RpWidget*> parent)
|
||||
: RpWidget(parent)
|
||||
, _container(this) {
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
_container->resizeToWidth(size.width());
|
||||
}, _container->lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::prepareAudio() {
|
||||
_type = RecordingType::AudioOnly;
|
||||
setLabel(tr::lng_group_call_recording_start_audio_subtitle(tr::now));
|
||||
|
||||
const auto wrap = _container->add(
|
||||
object_ptr<Ui::RpWidget>(_container),
|
||||
style::margins(0, st::groupCallRecordingAudioSkip, 0, 0));
|
||||
const auto audioIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_audio");
|
||||
wrap->resize(width(), audioIcon->height());
|
||||
audioIcon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
audioIcon->moveToLeft((size.width() - audioIcon->width()) / 2, 0);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::prepareVideo() {
|
||||
setLabel(tr::lng_group_call_recording_start_video_subtitle(tr::now));
|
||||
|
||||
const auto wrap = _container->add(
|
||||
object_ptr<Ui::RpWidget>(_container),
|
||||
style::margins());
|
||||
|
||||
const auto landscapeIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_video_landscape",
|
||||
st::groupCallRecordingSelectWidth);
|
||||
const auto portraitIcon = Ui::CreateChild<GraphicButton>(
|
||||
wrap,
|
||||
"info_video_portrait",
|
||||
st::groupCallRecordingSelectWidth);
|
||||
wrap->resize(width(), portraitIcon->height());
|
||||
|
||||
landscapeIcon->setToggled(true);
|
||||
_type = RecordingType::VideoLandscape;
|
||||
|
||||
const auto icons = std::vector<GraphicButton*>{
|
||||
landscapeIcon,
|
||||
portraitIcon,
|
||||
};
|
||||
const auto types = std::map<GraphicButton*, RecordingType>{
|
||||
{ landscapeIcon, RecordingType::VideoLandscape },
|
||||
{ portraitIcon, RecordingType::VideoPortrait },
|
||||
};
|
||||
for (const auto icon : icons) {
|
||||
icon->clicks(
|
||||
) | rpl::on_next([=] {
|
||||
for (const auto &i : icons) {
|
||||
i->setToggled(icon == i);
|
||||
}
|
||||
_type = types.at(icon);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
wrap->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
const auto wHalf = size.width() / icons.size();
|
||||
for (auto i = 0; i < icons.size(); i++) {
|
||||
const auto &icon = icons[i];
|
||||
icon->moveToLeft(
|
||||
wHalf * i + (wHalf - icon->width()) / 2,
|
||||
(size.height() - icon->height()) / 2);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void RecordingInfo::setLabel(const QString &text) {
|
||||
_container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_container,
|
||||
text,
|
||||
st::groupCallRecordingSubLabel),
|
||||
st::groupCallRecordingSubLabelMargins,
|
||||
style::al_top);
|
||||
}
|
||||
|
||||
RecordingType RecordingInfo::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
Switcher::Switcher(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<bool> &&toggled)
|
||||
: RpWidget(parent)
|
||||
, _background(
|
||||
this,
|
||||
st::groupCallRecordingInfoHeight,
|
||||
st::groupCallDividerBar)
|
||||
, _audio(this)
|
||||
, _video(this) {
|
||||
_audio->prepareAudio();
|
||||
_video->prepareVideo();
|
||||
|
||||
resize(0, st::groupCallRecordingInfoHeight);
|
||||
|
||||
const auto updatePositions = [=](float64 progress) {
|
||||
_audio->moveToLeft(-width() * progress, 0);
|
||||
_video->moveToLeft(_audio->x() + _audio->width(), 0);
|
||||
};
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
_audio->resize(size.width(), size.height());
|
||||
_video->resize(size.width(), size.height());
|
||||
|
||||
updatePositions(_toggled ? 1. : 0.);
|
||||
|
||||
_background->lower();
|
||||
_background->setGeometry(QRect(QPoint(), size));
|
||||
}, lifetime());
|
||||
|
||||
std::move(
|
||||
toggled
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
_toggled = toggled;
|
||||
_animation.start(
|
||||
updatePositions,
|
||||
toggled ? 0. : 1.,
|
||||
toggled ? 1. : 0.,
|
||||
kSwitchDuration);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
RecordingType Switcher::type() const {
|
||||
return _toggled ? _video->type() : _audio->type();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditGroupCallTitleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &placeholder,
|
||||
const QString &title,
|
||||
bool livestream,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(livestream
|
||||
? tr::lng_group_call_edit_title_channel()
|
||||
: tr::lng_group_call_edit_title());
|
||||
const auto input = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::groupCallField,
|
||||
rpl::single(placeholder),
|
||||
title));
|
||||
input->setMaxLength(kMaxGroupCallLength);
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
const auto submit = [=] {
|
||||
const auto result = input->getLastText().trimmed();
|
||||
box->closeBox();
|
||||
done(result);
|
||||
};
|
||||
input->submits() | rpl::on_next(submit, input->lifetime());
|
||||
box->addButton(tr::lng_settings_save(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void StartGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(RecordingType)> done) {
|
||||
box->setTitle(tr::lng_group_call_recording_start());
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_group_call_recording_start_sure(),
|
||||
st::groupCallBoxLabel));
|
||||
|
||||
const auto checkbox = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::lng_group_call_recording_start_checkbox(),
|
||||
false,
|
||||
st::groupCallCheckbox),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxRowPadding.bottom()));
|
||||
|
||||
const auto switcher = box->addRow(
|
||||
object_ptr<Switcher>(box, checkbox->checkedChanges()),
|
||||
st::groupCallRecordingInfoMargins);
|
||||
|
||||
box->addButton(tr::lng_continue(), [=] {
|
||||
const auto type = switcher->type();
|
||||
box->closeBox();
|
||||
done(type);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void AddTitleGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &title,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(tr::lng_group_call_recording_start_title());
|
||||
|
||||
const auto input = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::groupCallField,
|
||||
tr::lng_group_call_recording_start_field(),
|
||||
title));
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
const auto submit = [=] {
|
||||
const auto result = input->getLastText().trimmed();
|
||||
box->closeBox();
|
||||
done(result);
|
||||
};
|
||||
input->submits() | rpl::on_next(submit, input->lifetime());
|
||||
box->addButton(tr::lng_group_call_recording_start_button(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
|
||||
void StopGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(QString)> done) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_group_call_recording_stop_sure(),
|
||||
st::groupCallBoxLabel),
|
||||
style::margins(
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] {
|
||||
box->closeBox();
|
||||
done(QString());
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class RecordingType {
|
||||
AudioOnly,
|
||||
VideoLandscape,
|
||||
VideoPortrait,
|
||||
};
|
||||
|
||||
void EditGroupCallTitleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &placeholder,
|
||||
const QString &title,
|
||||
bool livestream,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
void StartGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(RecordingType)> done);
|
||||
|
||||
void AddTitleGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &title,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
void StopGroupCallRecordingBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/ui/calls_group_scheduled_labels.h"
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
rpl::producer<QString> StartsWhenText(rpl::producer<TimeId> date) {
|
||||
return std::move(
|
||||
date
|
||||
) | rpl::map([](TimeId date) -> rpl::producer<QString> {
|
||||
const auto parsedDate = base::unixtime::parse(date);
|
||||
const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0));
|
||||
const auto previousDay = QDateTime(
|
||||
parsedDate.date().addDays(-1),
|
||||
QTime(0, 0));
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto kDay = int64(24 * 60 * 60);
|
||||
const auto tillTomorrow = int64(now.secsTo(previousDay));
|
||||
const auto tillToday = tillTomorrow + kDay;
|
||||
const auto tillAfter = tillToday + kDay;
|
||||
|
||||
const auto time = QLocale().toString(
|
||||
parsedDate.time(),
|
||||
QLocale::ShortFormat);
|
||||
auto exact = tr::lng_group_call_starts_short_date(
|
||||
lt_date,
|
||||
rpl::single(langDayOfMonthFull(dateDay.date())),
|
||||
lt_time,
|
||||
rpl::single(time)
|
||||
) | rpl::type_erased;
|
||||
auto tomorrow = tr::lng_group_call_starts_short_tomorrow(
|
||||
lt_time,
|
||||
rpl::single(time));
|
||||
auto today = tr::lng_group_call_starts_short_today(
|
||||
lt_time,
|
||||
rpl::single(time));
|
||||
|
||||
auto todayAndAfter = rpl::single(
|
||||
std::move(today)
|
||||
) | rpl::then(base::timer_once(
|
||||
std::min(tillAfter, kDay) * crl::time(1000)
|
||||
) | rpl::map([=] {
|
||||
return rpl::duplicate(exact);
|
||||
})) | rpl::flatten_latest() | rpl::type_erased;
|
||||
|
||||
auto tomorrowAndAfter = rpl::single(
|
||||
std::move(tomorrow)
|
||||
) | rpl::then(base::timer_once(
|
||||
std::min(tillToday, kDay) * crl::time(1000)
|
||||
) | rpl::map([=] {
|
||||
return rpl::duplicate(todayAndAfter);
|
||||
})) | rpl::flatten_latest() | rpl::type_erased;
|
||||
|
||||
auto full = rpl::single(
|
||||
rpl::duplicate(exact)
|
||||
) | rpl::then(base::timer_once(
|
||||
tillTomorrow * crl::time(1000)
|
||||
) | rpl::map([=] {
|
||||
return rpl::duplicate(tomorrowAndAfter);
|
||||
})) | rpl::flatten_latest() | rpl::type_erased;
|
||||
|
||||
if (tillTomorrow > 0) {
|
||||
return full;
|
||||
} else if (tillToday > 0) {
|
||||
return tomorrowAndAfter;
|
||||
} else if (tillAfter > 0) {
|
||||
return todayAndAfter;
|
||||
} else {
|
||||
return exact;
|
||||
}
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateGradientLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> text) {
|
||||
struct State {
|
||||
QBrush brush;
|
||||
QPainterPath path;
|
||||
};
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = result.data();
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
|
||||
std::move(
|
||||
text
|
||||
) | rpl::on_next([=](const QString &text) {
|
||||
state->path = QPainterPath();
|
||||
const auto &font = st::groupCallCountdownFont;
|
||||
state->path.addText(0, font->ascent, font->f, text);
|
||||
const auto width = font->width(text);
|
||||
raw->resize(width, font->height);
|
||||
auto gradient = QLinearGradient(QPoint(width, 0), QPoint());
|
||||
gradient.setStops(QGradientStops{
|
||||
{ 0.0, st::groupCallForceMutedBar1->c },
|
||||
{ .7, st::groupCallForceMutedBar2->c },
|
||||
{ 1.0, st::groupCallForceMutedBar3->c }
|
||||
});
|
||||
state->brush = QBrush(std::move(gradient));
|
||||
raw->update();
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto skip = st::groupCallWidth / 20;
|
||||
const auto available = parent->width() - 2 * skip;
|
||||
const auto full = raw->width();
|
||||
if (available > 0 && full > available) {
|
||||
const auto scale = available / float64(full);
|
||||
const auto shift = raw->rect().center();
|
||||
p.translate(shift);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-shift);
|
||||
}
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(state->brush);
|
||||
p.drawPath(state->path);
|
||||
}, raw->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> StartsWhenText(
|
||||
rpl::producer<TimeId> date);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> CreateGradientLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/ui/calls_group_stars_coloring.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "payments/ui/payments_reaction_box.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
StarsColoring StarsColoringForCount(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
int stars) {
|
||||
for (auto i = begin(colorings), e = end(colorings); i != e; ++i) {
|
||||
if (i->fromStars > stars) {
|
||||
Assert(i != begin(colorings));
|
||||
return *(std::prev(i));
|
||||
}
|
||||
}
|
||||
return colorings.back();
|
||||
}
|
||||
|
||||
int StarsRequiredForMessage(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
const TextWithTags &text) {
|
||||
Expects(!colorings.empty());
|
||||
|
||||
auto emojis = 0;
|
||||
auto outLength = 0;
|
||||
auto view = QStringView(text.text);
|
||||
const auto length = int(view.size());
|
||||
while (!view.isEmpty()) {
|
||||
if (Emoji::Find(view, &outLength)) {
|
||||
view = view.mid(outLength);
|
||||
++emojis;
|
||||
} else {
|
||||
view = view.mid(1);
|
||||
}
|
||||
}
|
||||
for (const auto &entry : colorings) {
|
||||
if (emojis <= entry.emojiLimit && length <= entry.charactersMax) {
|
||||
return entry.fromStars;
|
||||
}
|
||||
}
|
||||
return colorings.back().fromStars + 1;
|
||||
}
|
||||
|
||||
object_ptr<RpWidget> VideoStreamStarsLevel(
|
||||
not_null<RpWidget*> box,
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
rpl::producer<int> starsValue) {
|
||||
struct State {
|
||||
rpl::variable<int> stars;
|
||||
rpl::variable<StarsColoring> coloring;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->stars = std::move(starsValue);
|
||||
state->coloring = state->stars.value(
|
||||
) | rpl::map([=](int stars) {
|
||||
return StarsColoringForCount(colorings, stars);
|
||||
});
|
||||
|
||||
auto pinTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
const auto seconds = value.secondsPin;
|
||||
return (seconds >= 3600)
|
||||
? tr::lng_hours_tiny(tr::now, lt_count, seconds / 3600)
|
||||
: (seconds >= 60)
|
||||
? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)
|
||||
: tr::lng_seconds_tiny(tr::now, lt_count, seconds);
|
||||
});
|
||||
auto limitTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return QString::number(value.charactersMax);
|
||||
});
|
||||
auto limitSubtext = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return tr::lng_paid_comment_limit_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
value.charactersMax);
|
||||
});
|
||||
auto emojiTitle = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return QString::number(value.emojiLimit);
|
||||
});
|
||||
auto emojiSubtext = state->coloring.value(
|
||||
) | rpl::map([=](const StarsColoring &value) {
|
||||
return tr::lng_paid_comment_emoji_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
value.emojiLimit);
|
||||
});
|
||||
return MakeStarSelectInfoBlocks(box, {
|
||||
{
|
||||
.title = std::move(pinTitle) | rpl::map(tr::marked),
|
||||
.subtext = tr::lng_paid_comment_pin_about(),
|
||||
},
|
||||
{
|
||||
.title = std::move(limitTitle) | rpl::map(tr::marked),
|
||||
.subtext = std::move(limitSubtext),
|
||||
},
|
||||
{
|
||||
.title = std::move(emojiTitle) | rpl::map(tr::marked),
|
||||
.subtext = std::move(emojiSubtext),
|
||||
},
|
||||
}, {}, true);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct StarsColoring {
|
||||
int bgLight = 0;
|
||||
int bgDark = 0;
|
||||
int fromStars = 0;
|
||||
TimeId secondsPin = 0;
|
||||
int charactersMax = 0;
|
||||
int emojiLimit = 0;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const StarsColoring &,
|
||||
const StarsColoring &) = default;
|
||||
friend inline bool operator==(
|
||||
const StarsColoring &,
|
||||
const StarsColoring &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] StarsColoring StarsColoringForCount(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
int stars);
|
||||
|
||||
[[nodiscard]] int StarsRequiredForMessage(
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
const TextWithTags &text);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> VideoStreamStarsLevel(
|
||||
not_null<Ui::RpWidget*> box,
|
||||
const std::vector<StarsColoring> &colorings,
|
||||
rpl::producer<int> starsValue);
|
||||
|
||||
} // namespace Calls::Group::Ui
|
||||
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
|
||||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
|
||||
#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
namespace Calls::Group::Ui::DesktopCapture {
|
||||
namespace {
|
||||
|
||||
constexpr auto kColumns = 3;
|
||||
constexpr auto kRows = 2;
|
||||
|
||||
struct Preview {
|
||||
explicit Preview(tgcalls::DesktopCaptureSource source);
|
||||
|
||||
tgcalls::DesktopCaptureSourceHelper helper;
|
||||
Webrtc::VideoTrack track;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
class SourceButton final : public RippleButton {
|
||||
public:
|
||||
using RippleButton::RippleButton;
|
||||
|
||||
private:
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
};
|
||||
|
||||
QImage SourceButton::prepareRippleMask() const {
|
||||
return RippleAnimation::RoundRectMask(size(), st::roundRadiusLarge);
|
||||
}
|
||||
|
||||
class Source final {
|
||||
public:
|
||||
Source(
|
||||
not_null<QWidget*> parent,
|
||||
tgcalls::DesktopCaptureSource source,
|
||||
const QString &title);
|
||||
|
||||
void setGeometry(QRect geometry);
|
||||
void clearHelper();
|
||||
|
||||
[[nodiscard]] rpl::producer<> activations() const;
|
||||
void setActive(bool active);
|
||||
[[nodiscard]] QString deviceIdKey() const;
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void paint();
|
||||
void setupPreview();
|
||||
|
||||
SourceButton _widget;
|
||||
FlatLabel _label;
|
||||
Ui::RoundRect _selectedRect;
|
||||
Ui::RoundRect _activeRect;
|
||||
tgcalls::DesktopCaptureSource _source;
|
||||
std::unique_ptr<Preview> _preview;
|
||||
rpl::event_stream<> _activations;
|
||||
QImage _frame;
|
||||
bool _active = false;
|
||||
|
||||
};
|
||||
|
||||
class ChooseSourceProcess final {
|
||||
public:
|
||||
static void Start(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
explicit ChooseSourceProcess(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
void activate();
|
||||
|
||||
private:
|
||||
void setupPanel();
|
||||
void setupSources();
|
||||
void setupGeometryWithParent(not_null<QWidget*> parent);
|
||||
void fillSources();
|
||||
void setupSourcesGeometry();
|
||||
void updateButtonsVisibility();
|
||||
void destroy();
|
||||
|
||||
static base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>> &Map();
|
||||
|
||||
const not_null<ChooseSourceDelegate*> _delegate;
|
||||
const std::unique_ptr<RpWindow> _window;
|
||||
const std::unique_ptr<ScrollArea> _scroll;
|
||||
const not_null<RpWidget*> _inner;
|
||||
const not_null<RpWidget*> _bottom;
|
||||
const not_null<RoundButton*> _submit;
|
||||
const not_null<RoundButton*> _finish;
|
||||
const not_null<Checkbox*> _withAudio;
|
||||
|
||||
QSize _fixedSize;
|
||||
std::vector<std::unique_ptr<Source>> _sources;
|
||||
Source *_selected = nullptr;
|
||||
QString _selectedId;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = st::desktopCaptureSourceSize * factor;
|
||||
return {
|
||||
.aspectSize = { size.width(), size.height() },
|
||||
.fps = 1,
|
||||
.captureMouse = false,
|
||||
};
|
||||
}
|
||||
|
||||
Preview::Preview(tgcalls::DesktopCaptureSource source)
|
||||
: helper(source, SourceData())
|
||||
, track(Webrtc::VideoState::Active) {
|
||||
helper.setOutput(track.sink());
|
||||
helper.start();
|
||||
}
|
||||
|
||||
Source::Source(
|
||||
not_null<QWidget*> parent,
|
||||
tgcalls::DesktopCaptureSource source,
|
||||
const QString &title)
|
||||
: _widget(parent, st::groupCallRipple)
|
||||
, _label(&_widget, title, st::desktopCaptureLabel)
|
||||
, _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver)
|
||||
, _activeRect(ImageRoundRadius::Large, st::groupCallMuted1)
|
||||
, _source(source) {
|
||||
_widget.paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
paint();
|
||||
}, _widget.lifetime());
|
||||
|
||||
_label.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_widget.sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
_label.resizeToNaturalWidth(
|
||||
size.width() - padding.left() - padding.right());
|
||||
_label.move(
|
||||
(size.width() - _label.width()) / 2,
|
||||
size.height() - _label.height() - st::desktopCaptureLabelBottom);
|
||||
}, _label.lifetime());
|
||||
|
||||
_widget.setClickedCallback([=] {
|
||||
setActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<> Source::activations() const {
|
||||
return _activations.events();
|
||||
}
|
||||
|
||||
QString Source::deviceIdKey() const {
|
||||
return QString::fromStdString(_source.deviceIdKey());
|
||||
}
|
||||
|
||||
void Source::setActive(bool active) {
|
||||
if (_active != active) {
|
||||
_active = active;
|
||||
_widget.update();
|
||||
if (active) {
|
||||
_activations.fire({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Source::setGeometry(QRect geometry) {
|
||||
_widget.setGeometry(geometry);
|
||||
}
|
||||
|
||||
void Source::clearHelper() {
|
||||
_preview = nullptr;
|
||||
}
|
||||
|
||||
void Source::paint() {
|
||||
auto p = QPainter(&_widget);
|
||||
|
||||
if (_frame.isNull() && !_preview) {
|
||||
setupPreview();
|
||||
}
|
||||
if (_active) {
|
||||
_activeRect.paint(p, _widget.rect());
|
||||
} else if (_widget.isOver() || _widget.isDown()) {
|
||||
_selectedRect.paint(p, _widget.rect());
|
||||
}
|
||||
_widget.paintRipple(
|
||||
p,
|
||||
{ 0, 0 },
|
||||
_active ? &st::shadowFg->c : nullptr);
|
||||
|
||||
const auto size = _preview ? _preview->track.frameSize() : QSize();
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
const auto rect = _widget.rect();
|
||||
const auto inner = rect.marginsRemoved(padding);
|
||||
if (!size.isEmpty()) {
|
||||
const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio);
|
||||
const auto request = Webrtc::FrameRequest{
|
||||
.resize = scaled * factor,
|
||||
.outer = scaled * factor,
|
||||
};
|
||||
_frame = _preview->track.frame(request);
|
||||
_preview->track.markFrameShown();
|
||||
}
|
||||
if (!_frame.isNull()) {
|
||||
clearHelper();
|
||||
const auto size = _frame.size() / factor;
|
||||
const auto x = inner.x() + (inner.width() - size.width()) / 2;
|
||||
const auto y = inner.y() + (inner.height() - size.height()) / 2;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawImage(QRect(x, y, size.width(), size.height()), _frame);
|
||||
}
|
||||
}
|
||||
|
||||
void Source::setupPreview() {
|
||||
_preview = std::make_unique<Preview>(_source);
|
||||
_preview->track.renderNextFrame(
|
||||
) | rpl::on_next([=] {
|
||||
if (_preview->track.frameSize().isEmpty()) {
|
||||
_preview->track.markFrameShown();
|
||||
}
|
||||
_widget.update();
|
||||
}, _preview->lifetime);
|
||||
}
|
||||
|
||||
rpl::lifetime &Source::lifetime() {
|
||||
return _widget.lifetime();
|
||||
}
|
||||
|
||||
ChooseSourceProcess::ChooseSourceProcess(
|
||||
not_null<ChooseSourceDelegate*> delegate)
|
||||
: _delegate(delegate)
|
||||
, _window(std::make_unique<RpWindow>())
|
||||
, _scroll(std::make_unique<ScrollArea>(_window->body()))
|
||||
, _inner(_scroll->setOwnedWidget(object_ptr<RpWidget>(_scroll.get())))
|
||||
, _bottom(CreateChild<RpWidget>(_window->body().get()))
|
||||
, _submit(
|
||||
CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_start(),
|
||||
st::desktopCaptureSubmit))
|
||||
, _finish(
|
||||
CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_stop(),
|
||||
st::desktopCaptureFinish))
|
||||
, _withAudio(
|
||||
CreateChild<Checkbox>(
|
||||
_bottom.get(),
|
||||
tr::lng_group_call_screen_share_audio(tr::now),
|
||||
false,
|
||||
st::desktopCaptureWithAudio)) {
|
||||
setupPanel();
|
||||
setupSources();
|
||||
activate();
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::Start(not_null<ChooseSourceDelegate*> delegate) {
|
||||
auto &map = Map();
|
||||
auto i = map.find(delegate);
|
||||
if (i == end(map)) {
|
||||
i = map.emplace(delegate, nullptr).first;
|
||||
delegate->chooseSourceInstanceLifetime().add([=] {
|
||||
Map().erase(delegate);
|
||||
});
|
||||
}
|
||||
if (!i->second) {
|
||||
i->second = std::make_unique<ChooseSourceProcess>(delegate);
|
||||
} else {
|
||||
i->second->activate();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::activate() {
|
||||
if (_window->windowState() & Qt::WindowMinimized) {
|
||||
_window->showNormal();
|
||||
} else {
|
||||
_window->show();
|
||||
}
|
||||
_window->raise();
|
||||
_window->activateWindow();
|
||||
}
|
||||
|
||||
[[nodiscard]] base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>> &ChooseSourceProcess::Map() {
|
||||
static auto result = base::flat_map<
|
||||
not_null<ChooseSourceDelegate*>,
|
||||
std::unique_ptr<ChooseSourceProcess>>();
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupPanel() {
|
||||
#ifndef Q_OS_LINUX
|
||||
//_window->setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
#endif // Q_OS_LINUX
|
||||
//_window->setAttribute(Qt::WA_NoSystemBackground);
|
||||
|
||||
_window->setWindowIcon(QIcon(
|
||||
QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
|
||||
_window->setTitleStyle(st::desktopCaptureSourceTitle);
|
||||
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto padding = st::desktopCapturePadding;
|
||||
const auto bottomSkip = margins.right() + padding.right();
|
||||
const auto bottomHeight = 2 * bottomSkip
|
||||
+ st::desktopCaptureCancel.height;
|
||||
const auto width = margins.left()
|
||||
+ kColumns * st::desktopCaptureSourceSize.width()
|
||||
+ (kColumns - 1) * skips.width()
|
||||
+ margins.right();
|
||||
const auto height = margins.top()
|
||||
+ kRows * st::desktopCaptureSourceSize.height()
|
||||
+ (kRows - 1) * skips.height()
|
||||
+ (st::desktopCaptureSourceSize.height() / 2)
|
||||
+ bottomHeight;
|
||||
_fixedSize = QSize(width, height);
|
||||
_window->setStaysOnTop(true);
|
||||
|
||||
_window->body()->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg);
|
||||
}, _window->lifetime());
|
||||
|
||||
_bottom->setGeometry(0, height - bottomHeight, width, bottomHeight);
|
||||
|
||||
_submit->setClickedCallback([=] {
|
||||
if (_selectedId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto weak = base::make_weak(_window.get());
|
||||
_delegate->chooseSourceAccepted(
|
||||
_selectedId,
|
||||
!_withAudio->isHidden() && _withAudio->checked());
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->close();
|
||||
}
|
||||
});
|
||||
_finish->setClickedCallback([=] {
|
||||
const auto weak = base::make_weak(_window.get());
|
||||
_delegate->chooseSourceStop();
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->close();
|
||||
}
|
||||
});
|
||||
const auto cancel = CreateChild<RoundButton>(
|
||||
_bottom.get(),
|
||||
tr::lng_cancel(),
|
||||
st::desktopCaptureCancel);
|
||||
cancel->setClickedCallback([=] {
|
||||
_window->close();
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
_submit->widthValue(),
|
||||
_submit->shownValue(),
|
||||
_finish->widthValue(),
|
||||
_finish->shownValue(),
|
||||
cancel->widthValue()
|
||||
) | rpl::on_next([=](
|
||||
int submitWidth,
|
||||
bool submitShown,
|
||||
int finishWidth,
|
||||
bool finishShown,
|
||||
int cancelWidth) {
|
||||
_finish->moveToRight(bottomSkip, bottomSkip);
|
||||
_submit->moveToRight(bottomSkip, bottomSkip);
|
||||
cancel->moveToRight(
|
||||
bottomSkip * 2 + (submitShown ? submitWidth : finishWidth),
|
||||
bottomSkip);
|
||||
}, _bottom->lifetime());
|
||||
|
||||
_withAudio->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2;
|
||||
_withAudio->moveToLeft(bottomSkip, top);
|
||||
}, _withAudio->lifetime());
|
||||
|
||||
_withAudio->setChecked(_delegate->chooseSourceActiveWithAudio());
|
||||
_withAudio->checkedChanges(
|
||||
) | rpl::on_next([=] {
|
||||
updateButtonsVisibility();
|
||||
}, _withAudio->lifetime());
|
||||
|
||||
const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty();
|
||||
_finish->setVisible(sharing);
|
||||
_submit->setVisible(!sharing);
|
||||
|
||||
_window->body()->sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
_scroll->setGeometry(
|
||||
0,
|
||||
0,
|
||||
size.width(),
|
||||
size.height() - _bottom->height());
|
||||
}, _scroll->lifetime());
|
||||
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto innerHeight = margins.top()
|
||||
+ rows * st::desktopCaptureSourceSize.height()
|
||||
+ (rows - 1) * skips.height()
|
||||
+ margins.bottom();
|
||||
_inner->resize(width, innerHeight);
|
||||
}, _inner->lifetime());
|
||||
|
||||
if (const auto parent = _delegate->chooseSourceParent()) {
|
||||
setupGeometryWithParent(parent);
|
||||
}
|
||||
|
||||
_window->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return e->type() == QEvent::Close;
|
||||
}) | rpl::on_next([=] {
|
||||
destroy();
|
||||
}, _window->lifetime());
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupSources() {
|
||||
fillSources();
|
||||
setupSourcesGeometry();
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::fillSources() {
|
||||
using Type = tgcalls::DesktopCaptureType;
|
||||
auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen);
|
||||
auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window);
|
||||
|
||||
_withAudio->setVisible(_delegate->chooseSourceWithAudioSupported());
|
||||
|
||||
auto screenIndex = 0;
|
||||
auto windowIndex = 0;
|
||||
auto firstScreenSelected = false;
|
||||
const auto active = _delegate->chooseSourceActiveDeviceId();
|
||||
const auto append = [&](const tgcalls::DesktopCaptureSource &source) {
|
||||
const auto firstScreen = !source.isWindow() && !screenIndex;
|
||||
const auto title = !source.isWindow()
|
||||
? tr::lng_group_call_screen_title(
|
||||
tr::now,
|
||||
lt_index,
|
||||
QString::number(++screenIndex))
|
||||
: !source.title().empty()
|
||||
? QString::fromStdString(source.title())
|
||||
: "Window " + QString::number(++windowIndex);
|
||||
const auto id = source.deviceIdKey();
|
||||
_sources.push_back(std::make_unique<Source>(_inner, source, title));
|
||||
|
||||
const auto raw = _sources.back().get();
|
||||
if (!active.isEmpty() && active.toStdString() == id) {
|
||||
_selected = raw;
|
||||
raw->setActive(true);
|
||||
} else if (active.isEmpty() && firstScreen) {
|
||||
_selected = raw;
|
||||
raw->setActive(true);
|
||||
firstScreenSelected = true;
|
||||
}
|
||||
_sources.back()->activations(
|
||||
) | rpl::filter([=] {
|
||||
return (_selected != raw);
|
||||
}) | rpl::on_next([=]{
|
||||
if (_selected) {
|
||||
_selected->setActive(false);
|
||||
}
|
||||
_selected = raw;
|
||||
updateButtonsVisibility();
|
||||
}, raw->lifetime());
|
||||
};
|
||||
for (const auto &source : screensManager.sources()) {
|
||||
append(source);
|
||||
}
|
||||
for (const auto &source : windowsManager.sources()) {
|
||||
append(source);
|
||||
}
|
||||
if (firstScreenSelected) {
|
||||
updateButtonsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::updateButtonsVisibility() {
|
||||
const auto selectedId = _selected
|
||||
? _selected->deviceIdKey()
|
||||
: QString();
|
||||
if (selectedId == _delegate->chooseSourceActiveDeviceId()
|
||||
&& (!_delegate->chooseSourceWithAudioSupported()
|
||||
|| (_withAudio->checked()
|
||||
== _delegate->chooseSourceActiveWithAudio()))) {
|
||||
_selectedId = QString();
|
||||
_finish->setVisible(true);
|
||||
_submit->setVisible(false);
|
||||
} else {
|
||||
_selectedId = selectedId;
|
||||
_finish->setVisible(false);
|
||||
_submit->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupSourcesGeometry() {
|
||||
if (_sources.empty()) {
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
_inner->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto single = (width
|
||||
- margins.left()
|
||||
- margins.right()
|
||||
- (kColumns - 1) * skips.width()) / kColumns;
|
||||
const auto height = st::desktopCaptureSourceSize.height();
|
||||
auto top = margins.top();
|
||||
auto index = 0;
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
auto left = margins.left();
|
||||
for (auto column = 0; column != kColumns; ++column) {
|
||||
_sources[index]->setGeometry({ left, top, single, height });
|
||||
if (++index == _sources.size()) {
|
||||
break;
|
||||
}
|
||||
left += single + skips.width();
|
||||
}
|
||||
if (index >= _sources.size()) {
|
||||
break;
|
||||
}
|
||||
top += height + skips.height();
|
||||
}
|
||||
}, _inner->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue()
|
||||
) | rpl::on_next([=](int scrollTop, int scrollHeight) {
|
||||
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
|
||||
const auto margins = st::desktopCaptureMargins;
|
||||
const auto skips = st::desktopCaptureSourceSkips;
|
||||
const auto height = st::desktopCaptureSourceSize.height();
|
||||
auto top = margins.top();
|
||||
auto index = 0;
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
const auto hidden = (top + height <= scrollTop)
|
||||
|| (top >= scrollTop + scrollHeight);
|
||||
if (hidden) {
|
||||
for (auto column = 0; column != kColumns; ++column) {
|
||||
_sources[index]->clearHelper();
|
||||
if (++index == _sources.size()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
index += kColumns;
|
||||
}
|
||||
if (index >= _sources.size()) {
|
||||
break;
|
||||
}
|
||||
top += height + skips.height();
|
||||
}
|
||||
}, _inner->lifetime());
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::setupGeometryWithParent(
|
||||
not_null<QWidget*> parent) {
|
||||
const auto parentScreen = parent->screen();
|
||||
const auto myScreen = _window->screen();
|
||||
if (parentScreen && myScreen != parentScreen) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
_window->setScreen(parentScreen);
|
||||
#else // Qt >= 6.0.0
|
||||
_window->createWinId();
|
||||
_window->windowHandle()->setScreen(parentScreen);
|
||||
#endif // Qt < 6.0.0
|
||||
}
|
||||
_window->setFixedSize(_fixedSize);
|
||||
_window->move(
|
||||
parent->x() + (parent->width() - _window->width()) / 2,
|
||||
parent->y() + (parent->height() - _window->height()) / 2);
|
||||
}
|
||||
|
||||
void ChooseSourceProcess::destroy() {
|
||||
auto &map = Map();
|
||||
if (const auto i = map.find(_delegate); i != end(map)) {
|
||||
if (i->second.get() == this) {
|
||||
base::take(i->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseSource(not_null<ChooseSourceDelegate*> delegate) {
|
||||
ChooseSourceProcess::Start(delegate);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group::Ui::DesktopCapture
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group::Ui {
|
||||
using namespace ::Ui;
|
||||
} // namespace Calls::Group::Ui
|
||||
|
||||
namespace Calls::Group::Ui::DesktopCapture {
|
||||
|
||||
class ChooseSourceDelegate {
|
||||
public:
|
||||
virtual QWidget *chooseSourceParent() = 0;
|
||||
virtual QString chooseSourceActiveDeviceId() = 0;
|
||||
virtual bool chooseSourceActiveWithAudio() = 0;
|
||||
virtual bool chooseSourceWithAudioSupported() = 0;
|
||||
virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0;
|
||||
virtual void chooseSourceAccepted(
|
||||
const QString &deviceId,
|
||||
bool withAudio) = 0;
|
||||
virtual void chooseSourceStop() = 0;
|
||||
};
|
||||
|
||||
void ChooseSource(not_null<ChooseSourceDelegate*> delegate);
|
||||
|
||||
} // namespace Calls::Group::Ui::DesktopCapture
|
||||
250
Telegram/SourceFiles/calls/ui/calls_device_menu.cpp
Normal file
250
Telegram/SourceFiles/calls/ui/calls_device_menu.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
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/ui/calls_device_menu.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "webrtc/webrtc_device_common.h"
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
class Subsection final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
Subsection(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &text);
|
||||
|
||||
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;
|
||||
|
||||
};
|
||||
|
||||
class Selector final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
Selector(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<std::vector<Webrtc::DeviceInfo>> devices,
|
||||
rpl::producer<Webrtc::DeviceResolvedId> chosen,
|
||||
Fn<void(QString)> selected);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
[[nodiscard]] int registerId(const QString &id);
|
||||
|
||||
const base::unique_qptr<Ui::ScrollArea> _scroll;
|
||||
const not_null<Ui::VerticalLayout*> _list;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
|
||||
base::flat_map<QString, int> _ids;
|
||||
|
||||
};
|
||||
|
||||
Subsection::Subsection(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &text)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _st(st)
|
||||
, _text(base::make_unique_q<Ui::FlatLabel>(
|
||||
this,
|
||||
text,
|
||||
st::callDeviceSelectionLabel))
|
||||
, _dummyAction(new QAction(parent)) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
|
||||
_text->resizeToWidth(st::callDeviceSelectionLabel.minWidth);
|
||||
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
|
||||
}
|
||||
|
||||
not_null<QAction*> Subsection::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool Subsection::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int Subsection::contentHeight() const {
|
||||
return _st.itemPadding.top()
|
||||
+ _text->height()
|
||||
+ _st.itemPadding.bottom();
|
||||
}
|
||||
|
||||
Selector::Selector(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<std::vector<Webrtc::DeviceInfo>> devices,
|
||||
rpl::producer<Webrtc::DeviceResolvedId> chosen,
|
||||
Fn<void(QString)> selected)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _scroll(base::make_unique_q<Ui::ScrollArea>(this))
|
||||
, _list(_scroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
|
||||
, _dummyAction(new QAction(parent)) {
|
||||
setPointerCursor(false);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
|
||||
const auto padding = st.itemPadding;
|
||||
const auto group = std::make_shared<Ui::RadiobuttonGroup>();
|
||||
std::move(
|
||||
chosen
|
||||
) | rpl::on_next([=](Webrtc::DeviceResolvedId id) {
|
||||
const auto value = id.isDefault() ? 0 : registerId(id.value);
|
||||
if (!group->hasValue() || group->current() != value) {
|
||||
group->setValue(value);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
group->setChangedCallback([=](int value) {
|
||||
if (value == 0) {
|
||||
selected({});
|
||||
} else {
|
||||
for (const auto &[id, index] : _ids) {
|
||||
if (index == value) {
|
||||
selected(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::move(
|
||||
devices
|
||||
) | rpl::on_next([=](const std::vector<Webrtc::DeviceInfo> &v) {
|
||||
while (_list->count()) {
|
||||
delete _list->widgetAt(0);
|
||||
}
|
||||
_list->add(
|
||||
object_ptr<Ui::Radiobutton>(
|
||||
_list.get(),
|
||||
group,
|
||||
0,
|
||||
tr::lng_settings_call_device_default(tr::now),
|
||||
st::groupCallCheckbox,
|
||||
st::groupCallRadio),
|
||||
padding);
|
||||
for (const auto &device : v) {
|
||||
if (device.inactive) {
|
||||
continue;
|
||||
}
|
||||
_list->add(
|
||||
object_ptr<Ui::Radiobutton>(
|
||||
_list.get(),
|
||||
group,
|
||||
registerId(device.id),
|
||||
device.name,
|
||||
st::groupCallCheckbox,
|
||||
st::groupCallRadio),
|
||||
padding);
|
||||
}
|
||||
resize(width(), contentHeight());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> Selector::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool Selector::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int Selector::contentHeight() const {
|
||||
_list->resizeToWidth(width());
|
||||
if (_list->count() <= 3) {
|
||||
_scroll->resize(width(), _list->height());
|
||||
} else {
|
||||
_scroll->resize(
|
||||
width(),
|
||||
3.5 * st::defaultRadio.diameter);
|
||||
}
|
||||
return _scroll->height();
|
||||
}
|
||||
|
||||
int Selector::registerId(const QString &id) {
|
||||
auto &result = _ids[id];
|
||||
if (!result) {
|
||||
result = int(_ids.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AddDeviceSelection(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<Webrtc::Environment*> environment,
|
||||
DeviceSelection type,
|
||||
Fn<void(QString)> selected) {
|
||||
const auto title = [&] {
|
||||
switch (type.type) {
|
||||
case Webrtc::DeviceType::Camera:
|
||||
return tr::lng_settings_call_camera(tr::now);
|
||||
case Webrtc::DeviceType::Playback:
|
||||
return tr::lng_settings_call_section_output(tr::now);
|
||||
case Webrtc::DeviceType::Capture:
|
||||
return tr::lng_settings_call_section_input(tr::now);
|
||||
}
|
||||
Unexpected("Type in AddDeviceSelection.");
|
||||
}();
|
||||
menu->addAction(
|
||||
base::make_unique_q<Subsection>(menu, menu->st().menu, title));
|
||||
menu->addAction(
|
||||
base::make_unique_q<Selector>(
|
||||
menu,
|
||||
menu->st().menu,
|
||||
environment->devicesValue(type.type),
|
||||
std::move(type.chosen),
|
||||
selected));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> MakeDeviceSelectionMenu(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Webrtc::Environment*> environment,
|
||||
std::vector<DeviceSelection> types,
|
||||
Fn<void(Webrtc::DeviceType, QString)> choose) {
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::callDeviceSelectionMenu);
|
||||
const auto raw = result.get();
|
||||
for (auto type : types) {
|
||||
if (!raw->empty()) {
|
||||
raw->addSeparator();
|
||||
}
|
||||
const auto selected = [=, type = type.type](QString id) {
|
||||
choose(type, id);
|
||||
};
|
||||
AddDeviceSelection(raw, environment, std::move(type), selected);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
36
Telegram/SourceFiles/calls/ui/calls_device_menu.h
Normal file
36
Telegram/SourceFiles/calls/ui/calls_device_menu.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Webrtc {
|
||||
class Environment;
|
||||
struct DeviceResolvedId;
|
||||
enum class DeviceType : uchar;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
struct DeviceSelection {
|
||||
Webrtc::DeviceType type;
|
||||
rpl::producer<Webrtc::DeviceResolvedId> chosen;
|
||||
};
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> MakeDeviceSelectionMenu(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Webrtc::Environment*> environment,
|
||||
std::vector<DeviceSelection> types,
|
||||
Fn<void(Webrtc::DeviceType, QString)> choose);
|
||||
|
||||
} // namespace Calls
|
||||
Reference in New Issue
Block a user