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

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

File diff suppressed because it is too large Load Diff

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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 &region) {
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

View 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 &region);
[[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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,67 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,501 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_menu.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_group_call.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include "ui/layers/generic_box.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "boxes/peer_list_box.h"
#include "base/unixtime.h"
#include "base/timer_rpl.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_calls.h"
#include "styles/style_chat_helpers.h"
namespace Calls::Group {
namespace {
constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
using Context = ChooseJoinAsProcess::Context;
class ListController : public PeerListController {
public:
ListController(
std::vector<not_null<PeerData*>> list,
not_null<PeerData*> selected);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
[[nodiscard]] not_null<PeerData*> selected() const;
private:
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);
std::vector<not_null<PeerData*>> _list;
not_null<PeerData*> _selected;
};
ListController::ListController(
std::vector<not_null<PeerData*>> list,
not_null<PeerData*> selected)
: PeerListController()
, _list(std::move(list))
, _selected(selected) {
}
Main::Session &ListController::session() const {
return _selected->session();
}
std::unique_ptr<PeerListRow> ListController::createRow(
not_null<PeerData*> peer) {
auto result = std::make_unique<PeerListRow>(peer);
if (peer->isSelf()) {
result->setCustomStatus(
tr::lng_group_call_join_as_personal(tr::now));
} else if (const auto channel = peer->asChannel()) {
result->setCustomStatus(
(channel->isMegagroup()
? tr::lng_chat_status_members
: tr::lng_chat_status_subscribers)(
tr::now,
lt_count,
channel->membersCount()));
}
return result;
}
void ListController::prepare() {
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
for (const auto &peer : _list) {
auto row = createRow(peer);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (peer == _selected) {
delegate()->peerListSetRowChecked(raw, true);
raw->finishCheckedAnimation();
}
}
delegate()->peerListRefreshRows();
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
if (peer == _selected) {
return;
}
const auto previous = delegate()->peerListFindRow(_selected->id.value);
Assert(previous != nullptr);
delegate()->peerListSetRowChecked(previous, false);
delegate()->peerListSetRowChecked(row, true);
_selected = peer;
}
not_null<PeerData*> ListController::selected() const {
return _selected;
}
void ScheduleGroupCallBox(
not_null<Ui::GenericBox*> box,
const JoinInfo &info,
Fn<void(JoinInfo)> done) {
const auto send = [=](TimeId date) {
box->closeBox();
auto copy = info;
copy.scheduleDate = date;
done(std::move(copy));
};
const auto livestream = info.peer->isBroadcast();
const auto duration = box->lifetime().make_state<
rpl::variable<QString>>();
auto description = (info.peer->isBroadcast()
? tr::lng_group_call_schedule_notified_channel
: tr::lng_group_call_schedule_notified_group)(
lt_duration,
duration->value());
const auto now = QDateTime::currentDateTime();
const auto min = [] {
return base::unixtime::serialize(
QDateTime::currentDateTime().addSecs(12));
};
const auto max = [] {
return base::unixtime::serialize(
QDateTime(QDate::currentDate().addDays(8), QTime(0, 0))) - 1;
};
// At least half an hour later, at zero minutes/seconds.
const auto schedule = QDateTime(
now.date(),
QTime(now.time().hour(), 0)
).addSecs(60 * 60 * (now.time().minute() < 30 ? 1 : 2));
auto descriptor = Ui::ChooseDateTimeBox(box, {
.title = (livestream
? tr::lng_group_call_schedule_title_channel()
: tr::lng_group_call_schedule_title()),
.submit = tr::lng_schedule_button(),
.done = send,
.min = min,
.time = base::unixtime::serialize(schedule),
.max = max,
.description = std::move(description),
});
using namespace rpl::mappers;
*duration = rpl::combine(
rpl::single(rpl::empty) | rpl::then(
base::timer_each(kLabelRefreshInterval)
),
std::move(descriptor.values) | rpl::filter(_1 != 0),
_2
) | rpl::map([](TimeId date) {
const auto now = base::unixtime::now();
const auto duration = (date - now);
if (duration >= 24 * 60 * 60) {
return tr::lng_days(tr::now, lt_count, duration / (24 * 60 * 60));
} else if (duration >= 60 * 60) {
return tr::lng_hours(tr::now, lt_count, duration / (60 * 60));
}
return tr::lng_minutes(tr::now, lt_count, std::max(duration / 60, 1));
});
}
void ChooseJoinAsBox(
not_null<Ui::GenericBox*> box,
Context context,
JoinInfo info,
Fn<void(JoinInfo)> done) {
box->setWidth(st::groupCallJoinAsWidth);
const auto livestream = info.peer->isBroadcast();
box->setTitle([&] {
switch (context) {
case Context::Create: return livestream
? tr::lng_group_call_start_as_header_channel()
: tr::lng_group_call_start_as_header();
case Context::Join:
case Context::JoinWithConfirm: return livestream
? tr::lng_group_call_join_as_header_channel()
: tr::lng_group_call_join_as_header();
case Context::Switch: return tr::lng_group_call_display_as_header();
}
Unexpected("Context in ChooseJoinAsBox.");
}());
const auto &labelSt = (context == Context::Switch)
? st::groupCallJoinAsLabel
: st::confirmPhoneAboutLabel;
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_group_call_join_as_about(),
labelSt));
auto &lifetime = box->lifetime();
const auto delegate = lifetime.make_state<
PeerListContentDelegateSimple
>();
const auto controller = lifetime.make_state<ListController>(
info.possibleJoinAs,
info.joinAs);
if (context == Context::Switch) {
controller->setStyleOverrides(
&st::groupCallJoinAsList,
&st::groupCallMultiSelect);
} else {
controller->setStyleOverrides(
&st::defaultChooseSendAs.list,
nullptr);
}
const auto content = box->addRow(
object_ptr<PeerListContent>(box, controller),
style::margins());
delegate->setContent(content);
controller->setDelegate(delegate);
const auto &peer = info.peer;
if ((context == Context::Create)
&& (peer->isChannel() && peer->asChannel()->hasAdminRights())) {
const auto makeLink = [](const QString &text) {
return tr::link(text);
};
const auto label = box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_group_call_or_schedule(
lt_link,
(livestream
? tr::lng_group_call_schedule_channel
: tr::lng_group_call_schedule)(makeLink),
tr::marked),
labelSt));
label->overrideLinkClickHandler([=] {
auto withJoinAs = info;
withJoinAs.joinAs = controller->selected();
box->getDelegate()->show(
Box(ScheduleGroupCallBox, withJoinAs, done));
});
}
auto next = (context == Context::Switch)
? tr::lng_settings_save()
: tr::lng_continue();
box->addButton(std::move(next), [=] {
auto copy = info;
copy.joinAs = controller->selected();
done(std::move(copy));
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
[[nodiscard]] TextWithEntities CreateOrJoinConfirmation(
not_null<PeerData*> peer,
ChooseJoinAsProcess::Context context,
bool joinAsAlreadyUsed) {
const auto existing = peer->groupCall();
if (!existing) {
return { peer->isBroadcast()
? tr::lng_group_call_create_sure_channel(tr::now)
: tr::lng_group_call_create_sure(tr::now) };
}
const auto channel = peer->asChannel();
const auto anonymouseAdmin = channel
&& ((channel->isMegagroup() && channel->amAnonymous())
|| (channel->isBroadcast()
&& (channel->amCreator() || channel->hasAdminRights())));
if (anonymouseAdmin && !joinAsAlreadyUsed) {
return { tr::lng_group_call_join_sure_personal(tr::now) };
} else if (context != ChooseJoinAsProcess::Context::JoinWithConfirm) {
return {};
}
const auto name = !existing->title().isEmpty()
? existing->title()
: peer->name();
return (peer->isBroadcast()
? tr::lng_group_call_join_confirm_channel
: tr::lng_group_call_join_confirm)(
tr::now,
lt_chat,
tr::bold(name),
tr::marked);
}
} // namespace
ChooseJoinAsProcess::~ChooseJoinAsProcess() {
if (_request) {
_request->peer->session().api().request(_request->id).cancel();
}
}
void ChooseJoinAsProcess::start(
not_null<PeerData*> peer,
Context context,
std::shared_ptr<Ui::Show> show,
Fn<void(JoinInfo)> done,
PeerData *changingJoinAsFrom) {
Expects(done != nullptr);
const auto isScheduled = (context == Context::CreateScheduled);
const auto session = &peer->session();
if (_request) {
if (_request->peer == peer && !isScheduled) {
_request->context = context;
_request->show = std::move(show);
_request->done = std::move(done);
_request->changingJoinAsFrom = changingJoinAsFrom;
return;
}
session->api().request(_request->id).cancel();
_request = nullptr;
}
const auto createRequest = [=, done = std::move(done)] {
_request = std::make_unique<ChannelsListRequest>(ChannelsListRequest{
.peer = peer,
.show = show,
.done = std::move(done),
.context = context,
.changingJoinAsFrom = changingJoinAsFrom });
};
if (isScheduled) {
auto box = Box(
ScheduleGroupCallBox,
JoinInfo{ .peer = peer, .joinAs = peer },
[=, createRequest = std::move(createRequest)](JoinInfo info) {
createRequest();
finish(info);
});
show->showBox(std::move(box));
return;
}
createRequest();
session->account().sessionChanges(
) | rpl::on_next([=] {
_request = nullptr;
}, _request->lifetime);
requestList();
}
void ChooseJoinAsProcess::requestList() {
const auto session = &_request->peer->session();
_request->id = session->api().request(MTPphone_GetGroupCallJoinAs(
_request->peer->input()
)).done([=](const MTPphone_JoinAsPeers &result) {
auto list = result.match([&](const MTPDphone_joinAsPeers &data) {
session->data().processUsers(data.vusers());
session->data().processChats(data.vchats());
const auto &peers = data.vpeers().v;
auto list = std::vector<not_null<PeerData*>>();
list.reserve(peers.size());
for (const auto &peer : peers) {
const auto peerId = peerFromMTP(peer);
if (const auto peer = session->data().peerLoaded(peerId)) {
if (!ranges::contains(list, not_null{ peer })) {
list.push_back(peer);
}
}
}
return list;
});
processList(std::move(list));
}).fail([=] {
finish({
.peer = _request->peer,
.joinAs = _request->peer->session().user(),
});
}).send();
}
void ChooseJoinAsProcess::finish(JoinInfo info) {
const auto done = std::move(_request->done);
const auto box = _request->box;
_request = nullptr;
done(std::move(info));
if (const auto strong = box.get()) {
strong->closeBox();
}
}
void ChooseJoinAsProcess::processList(
std::vector<not_null<PeerData*>> &&list) {
const auto session = &_request->peer->session();
const auto peer = _request->peer;
const auto self = peer->session().user();
auto info = JoinInfo{ .peer = peer, .joinAs = self };
const auto selectedId = peer->groupCallDefaultJoinAs();
if (list.empty()) {
_request->show->showToast(Lang::Hard::ServerError());
return;
}
info.joinAs = [&]() -> not_null<PeerData*> {
const auto loaded = selectedId
? session->data().peerLoaded(selectedId)
: nullptr;
const auto changingJoinAsFrom = _request->changingJoinAsFrom;
return (changingJoinAsFrom
&& ranges::contains(list, not_null{ changingJoinAsFrom }))
? not_null(changingJoinAsFrom)
: (loaded && ranges::contains(list, not_null{ loaded }))
? not_null(loaded)
: ranges::contains(list, self)
? self
: list.front();
}();
info.possibleJoinAs = std::move(list);
const auto onlyByMe = (info.possibleJoinAs.size() == 1)
&& (info.possibleJoinAs.front() == self);
// We already joined this voice chat, just rejoin with the same.
const auto byAlreadyUsed = selectedId
&& (info.joinAs->id == selectedId)
&& (peer->groupCall() != nullptr);
if (!_request->changingJoinAsFrom && (onlyByMe || byAlreadyUsed)) {
auto confirmation = CreateOrJoinConfirmation(
peer,
_request->context,
byAlreadyUsed);
if (confirmation.text.isEmpty()) {
finish(info);
return;
}
const auto livestream = peer->isBroadcast();
const auto creating = !peer->groupCall();
if (creating) {
confirmation
.append("\n\n")
.append(tr::lng_group_call_or_schedule(
tr::now,
lt_link,
tr::link((livestream
? tr::lng_group_call_schedule_channel
: tr::lng_group_call_schedule)(tr::now)),
tr::marked));
}
const auto guard = base::make_weak(&_request->guard);
const auto safeFinish = crl::guard(guard, [=] { finish(info); });
const auto filter = [=](const auto &...) {
if (guard) {
_request->show->showBox(Box(
ScheduleGroupCallBox,
info,
crl::guard(guard, [=](auto info) { finish(info); })));
}
return false;
};
auto box = Ui::MakeConfirmBox({
.text = confirmation,
.confirmed = crl::guard(guard, [=] { finish(info); }),
.confirmText = (creating
? tr::lng_create_group_create()
: tr::lng_group_call_join()),
.labelFilter = filter,
});
box->boxClosing(
) | rpl::on_next([=] {
_request = nullptr;
}, _request->lifetime);
_request->box = box.data();
_request->show->showBox(std::move(box));
return;
}
auto box = Box(
ChooseJoinAsBox,
_request->context,
std::move(info),
crl::guard(&_request->guard, [=](auto info) { finish(info); }));
box->boxClosing(
) | rpl::on_next([=] {
_request = nullptr;
}, _request->lifetime);
_request->box = box.data();
_request->show->showBox(std::move(box));
}
} // namespace Calls::Group

View File

@@ -0,0 +1,64 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/object_ptr.h"
class PeerData;
namespace Ui {
class Show;
class BoxContent;
} // namespace Ui
namespace Calls::Group {
struct JoinInfo;
class ChooseJoinAsProcess final {
public:
ChooseJoinAsProcess() = default;
~ChooseJoinAsProcess();
enum class Context {
Create,
CreateScheduled,
Join,
JoinWithConfirm,
Switch,
};
void start(
not_null<PeerData*> peer,
Context context,
std::shared_ptr<Ui::Show> show,
Fn<void(JoinInfo)> done,
PeerData *changingJoinAsFrom = nullptr);
private:
void requestList();
void processList(std::vector<not_null<PeerData*>> &&list);
void finish(JoinInfo info);
struct ChannelsListRequest {
not_null<PeerData*> peer;
std::shared_ptr<Ui::Show> show;
Fn<void(JoinInfo)> done;
base::has_weak_ptr guard;
base::weak_qptr<Ui::BoxContent> box;
rpl::lifetime lifetime;
Context context = Context();
mtpRequestId id = 0;
PeerData *changingJoinAsFrom = nullptr;
};
std::unique_ptr<ChannelsListRequest> _request;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,92 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_cover_item.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "styles/style_calls.h"
#include "styles/style_info.h"
namespace Calls {
CoverItem::CoverItem(
not_null<RpWidget*> parent,
const style::Menu &stMenu,
const style::ShortInfoCover &st,
rpl::producer<QString> name,
rpl::producer<QString> status,
PreparedShortInfoUserpic userpic)
: Ui::Menu::ItemBase(parent, stMenu)
, _cover(
this,
st,
std::move(name),
std::move(status),
std::move(userpic.value),
[] { return false; })
, _dummyAction(new QAction(parent))
, _st(st) {
setPointerCursor(false);
initResizeHook(parent->sizeValue());
enableMouseSelecting();
enableMouseSelecting(_cover.widget());
_cover.widget()->move(0, 0);
_cover.moveRequests(
) | rpl::on_next(userpic.move, lifetime());
}
not_null<QAction*> CoverItem::action() const {
return _dummyAction;
}
bool CoverItem::isEnabled() const {
return false;
}
int CoverItem::contentHeight() const {
return _st.size + st::groupCallMenu.separator.padding.bottom();
}
AboutItem::AboutItem(
not_null<RpWidget*> parent,
const style::Menu &st,
TextWithEntities &&about)
: Ui::Menu::ItemBase(parent, st)
, _st(st)
, _text(base::make_unique_q<Ui::FlatLabel>(
this,
rpl::single(std::move(about)),
st::groupCallMenuAbout))
, _dummyAction(new QAction(parent)) {
setPointerCursor(false);
initResizeHook(parent->sizeValue());
enableMouseSelecting();
enableMouseSelecting(_text.get());
_text->setSelectable(true);
_text->resizeToWidth(st::groupCallMenuAbout.minWidth);
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
}
not_null<QAction*> AboutItem::action() const {
return _dummyAction;
}
bool AboutItem::isEnabled() const {
return false;
}
int AboutItem::contentHeight() const {
return _st.itemPadding.top()
+ _text->height()
+ _st.itemPadding.bottom();
}
} // namespace Calls

View File

@@ -0,0 +1,69 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "boxes/peers/peer_short_info_box.h"
struct PreparedShortInfoUserpic;
namespace style {
struct ShortInfoCover;
} // namespace style
namespace Calls {
namespace Group {
struct MuteRequest;
struct VolumeRequest;
struct ParticipantState;
} // namespace Group
class CoverItem final : public Ui::Menu::ItemBase {
public:
CoverItem(
not_null<RpWidget*> parent,
const style::Menu &stMenu,
const style::ShortInfoCover &st,
rpl::producer<QString> name,
rpl::producer<QString> status,
PreparedShortInfoUserpic userpic);
not_null<QAction*> action() const override;
bool isEnabled() const override;
private:
int contentHeight() const override;
const PeerShortInfoCover _cover;
const not_null<QAction*> _dummyAction;
const style::ShortInfoCover &_st;
};
class AboutItem final : public Ui::Menu::ItemBase {
public:
AboutItem(
not_null<RpWidget*> parent,
const style::Menu &st,
TextWithEntities &&about);
not_null<QAction*> action() const override;
bool isEnabled() const override;
private:
int contentHeight() const override;
const style::Menu &_st;
const base::unique_qptr<Ui::FlatLabel> _text;
const not_null<QAction*> _dummyAction;
};
} // namespace Calls

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,815 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/timer.h"
#include "base/bytes.h"
#include "mtproto/sender.h"
#include "mtproto/mtproto_auth_key.h"
#include "webrtc/webrtc_device_common.h"
#include "webrtc/webrtc_device_resolver.h"
class History;
namespace tgcalls {
class GroupInstanceCustomImpl;
struct GroupLevelsUpdate;
struct GroupNetworkState;
struct GroupParticipantDescription;
class VideoCaptureInterface;
enum class VideoCodecName;
} // namespace tgcalls
namespace base {
class GlobalShortcutManager;
class GlobalShortcutValue;
} // namespace base
namespace Webrtc {
class MediaDevices;
class VideoTrack;
enum class VideoState;
} // namespace Webrtc
namespace Data {
struct LastSpokeTimes;
struct GroupCallParticipant;
class GroupCall;
enum class GroupCallOrigin : uchar;
} // namespace Data
namespace TdE2E {
class Call;
class EncryptDecrypt;
} // namespace TdE2E
namespace Calls::Group {
struct MuteRequest;
struct VolumeRequest;
struct ParticipantState;
struct JoinInfo;
struct ConferenceInfo;
struct RejoinEvent;
struct RtmpInfo;
enum class VideoQuality;
enum class Error;
class Messages;
} // namespace Calls::Group
namespace Calls {
struct InviteRequest;
struct InviteResult;
struct StartConferenceInfo;
enum class MuteState {
Active,
PushToTalk,
Muted,
ForceMuted,
RaisedHand,
};
[[nodiscard]] inline auto MapPushToTalkToActive() {
return rpl::map([=](MuteState state) {
return (state == MuteState::PushToTalk) ? MuteState::Active : state;
});
}
[[nodiscard]] bool IsGroupCallAdmin(
not_null<PeerData*> peer,
not_null<PeerData*> participantPeer);
struct LevelUpdate {
uint32 ssrc = 0;
float value = 0.;
bool voice = false;
bool me = false;
};
enum class VideoEndpointType {
Camera,
Screen,
};
struct VideoEndpoint {
VideoEndpoint() = default;
VideoEndpoint(
VideoEndpointType type,
not_null<PeerData*> peer,
std::string id)
: type(type)
, peer(peer)
, id(std::move(id)) {
}
VideoEndpointType type = VideoEndpointType::Camera;
PeerData *peer = nullptr;
std::string id;
[[nodiscard]] bool rtmp() const noexcept;
[[nodiscard]] bool empty() const noexcept {
Expects(id.empty() || peer != nullptr);
return id.empty();
}
[[nodiscard]] explicit operator bool() const noexcept {
return !empty();
}
};
inline bool operator==(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return (a.id == b.id);
}
inline bool operator!=(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return !(a == b);
}
inline bool operator<(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return (a.peer < b.peer)
|| (a.peer == b.peer && a.id < b.id);
}
inline bool operator>(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return (b < a);
}
inline bool operator<=(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return !(b < a);
}
inline bool operator>=(
const VideoEndpoint &a,
const VideoEndpoint &b) noexcept {
return !(a < b);
}
struct VideoStateToggle {
VideoEndpoint endpoint;
bool value = false;
};
struct VideoQualityRequest {
VideoEndpoint endpoint;
Group::VideoQuality quality = Group::VideoQuality();
};
struct ParticipantVideoParams;
[[nodiscard]] std::shared_ptr<ParticipantVideoParams> ParseVideoParams(
const tl::conditional<MTPGroupCallParticipantVideo> &camera,
const tl::conditional<MTPGroupCallParticipantVideo> &screen,
const std::shared_ptr<ParticipantVideoParams> &existing);
[[nodiscard]] const std::string &GetCameraEndpoint(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] const std::string &GetScreenEndpoint(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] bool IsCameraPaused(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] bool IsScreenPaused(
const std::shared_ptr<ParticipantVideoParams> &params);
[[nodiscard]] uint32 GetAdditionalAudioSsrc(
const std::shared_ptr<ParticipantVideoParams> &params);
class GroupCall final
: public base::has_weak_ptr
, private Webrtc::CaptureMuteTracker {
public:
class Delegate {
public:
virtual ~Delegate() = default;
virtual void groupCallFinished(not_null<GroupCall*> call) = 0;
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
virtual void groupCallRequestPermissionsOrFail(
Fn<void()> onSuccess) = 0;
enum class GroupCallSound {
Started,
Connecting,
AllowedToSpeak,
Ended,
RecordingStarted,
};
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
virtual auto groupCallGetVideoCapture(const QString &deviceId)
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
[[nodiscard]] virtual FnMut<void()> groupCallAddAsyncWaiter() = 0;
};
using GlobalShortcutManager = base::GlobalShortcutManager;
struct VideoTrack;
[[nodiscard]] static not_null<PeerData*> TrackPeer(
const std::unique_ptr<VideoTrack> &track);
[[nodiscard]] static not_null<Webrtc::VideoTrack*> TrackPointer(
const std::unique_ptr<VideoTrack> &track);
[[nodiscard]] static rpl::producer<QSize> TrackSizeValue(
const std::unique_ptr<VideoTrack> &track);
GroupCall(
not_null<Delegate*> delegate,
Group::JoinInfo info,
const MTPInputGroupCall &inputCall);
GroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);
~GroupCall();
[[nodiscard]] CallId id() const {
return _id;
}
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
[[nodiscard]] not_null<PeerData*> joinAs() const {
return _joinAs.current();
}
[[nodiscard]] rpl::producer<not_null<PeerData*>> joinAsValue() const {
return _joinAs.value();
}
[[nodiscard]] not_null<Group::Messages*> messages() const {
return _messages.get();
}
[[nodiscard]] not_null<PeerData*> messagesFrom() const;
[[nodiscard]] bool showChooseJoinAs() const;
[[nodiscard]] TimeId scheduleDate() const {
return _scheduleDate;
}
[[nodiscard]] bool scheduleStartSubscribed() const;
[[nodiscard]] bool rtmp() const;
[[nodiscard]] bool conference() const;
[[nodiscard]] bool videoStream() const;
[[nodiscard]] Data::GroupCallOrigin origin() const;
[[nodiscard]] bool listenersHidden() const;
[[nodiscard]] bool emptyRtmp() const;
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
[[nodiscard]] int rtmpVolume() const;
[[nodiscard]] Group::RtmpInfo rtmpInfo() const;
void setRtmpInfo(const Group::RtmpInfo &value);
[[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] std::shared_ptr<Data::GroupCall> sharedCall() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
void applyInputCall(const MTPInputGroupCall &inputCall);
void startConference();
void start(TimeId scheduleDate, bool rtmp);
void hangup();
void discard();
void rejoinAs(Group::JoinInfo info);
void rejoinWithHash(const QString &hash);
void initialJoin();
void initialJoinRequested();
void handleUpdate(const MTPUpdate &update);
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
void handlePossibleCreateOrJoinResponse(
const MTPDupdateGroupCallConnection &data);
void handleIncomingMessage(const MTPDupdateGroupCallMessage &data);
void handleIncomingMessage(
const MTPDupdateGroupCallEncryptedMessage &data);
void handleDeleteMessages(const MTPDupdateDeleteGroupCallMessages &data);
void handleMessageSent(const MTPDupdateMessageID &data);
void changeTitle(const QString &title);
void toggleRecording(
bool enabled,
const QString &title,
bool video,
bool videoPortrait);
void playSoundRecordingStarted() const;
[[nodiscard]] bool recordingStoppedByMe() const {
return _recordingStoppedByMe;
}
void startScheduledNow();
void toggleScheduleStartSubscribed(bool subscribed);
void setNoiseSuppression(bool enabled);
void removeConferenceParticipants(
const base::flat_set<UserId> userIds,
bool removingStale = false);
bool emitShareScreenError();
bool emitShareCameraError();
void joinDone(
int64 serverTimeMs,
const MTPUpdates &result,
MuteState wasMuteState,
bool wasVideoStopped,
bool justCreated = false);
void joinFail(const QString &error);
[[nodiscard]] rpl::producer<Group::Error> errors() const {
return _errors.events();
}
void addVideoOutput(
const std::string &endpoint,
not_null<Webrtc::VideoTrack*> track);
void setMuted(MuteState mute);
void setMutedAndUpdate(MuteState mute);
[[nodiscard]] MuteState muted() const {
return _muted.current();
}
[[nodiscard]] rpl::producer<MuteState> mutedValue() const {
return _muted.value();
}
[[nodiscard]] auto otherParticipantStateValue() const
-> rpl::producer<Group::ParticipantState>;
enum State {
Creating,
Waiting,
Joining,
Connecting,
Joined,
FailedHangingUp,
Failed,
HangingUp,
Ended,
};
[[nodiscard]] State state() const {
return _state.current();
}
[[nodiscard]] rpl::producer<State> stateValue() const {
return _state.value();
}
enum class InstanceState {
Disconnected,
TransitionToRtc,
Connected,
};
[[nodiscard]] InstanceState instanceState() const {
return _instanceState.current();
}
[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {
return _instanceState.value();
}
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
return _levelUpdates.events();
}
[[nodiscard]] auto videoStreamActiveUpdates() const
-> rpl::producer<VideoStateToggle> {
return _videoStreamActiveUpdates.events();
}
[[nodiscard]] auto videoStreamShownUpdates() const
-> rpl::producer<VideoStateToggle> {
return _videoStreamShownUpdates.events();
}
void requestVideoQuality(
const VideoEndpoint &endpoint,
Group::VideoQuality quality);
[[nodiscard]] bool videoEndpointPinned() const {
return _videoEndpointPinned.current();
}
[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {
return _videoEndpointPinned.value();
}
void pinVideoEndpoint(VideoEndpoint endpoint);
void showVideoEndpointLarge(VideoEndpoint endpoint);
[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {
return _videoEndpointLarge.current();
}
[[nodiscard]] auto videoEndpointLargeValue() const
-> rpl::producer<VideoEndpoint> {
return _videoEndpointLarge.value();
}
[[nodiscard]] auto activeVideoTracks() const
-> const base::flat_map<VideoEndpoint, std::unique_ptr<VideoTrack>> & {
return _activeVideoTracks;
}
[[nodiscard]] auto shownVideoTracks() const
-> const base::flat_set<VideoEndpoint> & {
return _shownVideoTracks;
}
[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {
return _rejoinEvents.events();
}
[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {
return _allowedToSpeakNotifications.events();
}
[[nodiscard]] rpl::producer<> titleChanged() const {
return _titleChanged.events();
}
static constexpr auto kSpeakLevelThreshold = 0.2;
[[nodiscard]] bool mutedByAdmin() const;
[[nodiscard]] bool canManage() const;
[[nodiscard]] rpl::producer<bool> canManageValue() const;
[[nodiscard]] bool videoIsWorking() const {
return _videoIsWorking.current();
}
[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {
return _videoIsWorking.value();
}
[[nodiscard]] bool messagesEnabled() const {
return _messagesEnabled.current();
}
[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {
return _messagesEnabled.value();
}
[[nodiscard]] bool isSharingScreen() const;
[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;
[[nodiscard]] bool isScreenPaused() const;
[[nodiscard]] const std::string &screenSharingEndpoint() const;
[[nodiscard]] bool isSharingCamera() const;
[[nodiscard]] rpl::producer<bool> isSharingCameraValue() const;
[[nodiscard]] bool isCameraPaused() const;
[[nodiscard]] const std::string &cameraSharingEndpoint() const;
[[nodiscard]] QString screenSharingDeviceId() const;
[[nodiscard]] bool screenSharingWithAudio() const;
void toggleVideo(bool active);
void toggleScreenSharing(
std::optional<QString> uniqueId,
bool withAudio = false);
[[nodiscard]] bool hasVideoWithFrames() const;
[[nodiscard]] rpl::producer<bool> hasVideoWithFramesValue() const;
void toggleMute(const Group::MuteRequest &data);
void changeVolume(const Group::VolumeRequest &data);
void inviteUsers(
const std::vector<InviteRequest> &requests,
Fn<void(InviteResult)> done);
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
void applyGlobalShortcutChanges();
void pushToTalk(bool pressed, crl::time delay);
void setNotRequireARGB32();
[[nodiscard]] std::function<std::vector<uint8_t>(
std::vector<uint8_t> const &,
int64_t, bool,
int32_t)> e2eEncryptDecrypt() const;
void sendMessage(TextWithTags message);
[[nodiscard]] MTPInputGroupCall inputCall() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
class LoadPartTask;
class MediaChannelDescriptionsTask;
class RequestCurrentTimeTask;
using GlobalShortcutValue = base::GlobalShortcutValue;
using Error = Group::Error;
struct SinkPointer;
static constexpr uint32 kDisabledSsrc = uint32(-1);
static constexpr int kSubChainsCount = 2;
struct LoadingPart {
std::shared_ptr<LoadPartTask> task;
mtpRequestId requestId = 0;
};
enum class FinishType {
None,
Ended,
Failed,
};
enum class InstanceMode {
None,
Rtc,
Stream,
};
enum class SendUpdateType {
Mute = 0x01,
RaiseHand = 0x02,
CameraStopped = 0x04,
CameraPaused = 0x08,
ScreenPaused = 0x10,
};
enum class JoinAction {
None,
Joining,
Leaving,
};
struct JoinPayload {
uint32 ssrc = 0;
QByteArray json;
};
struct JoinState {
uint32 ssrc = 0;
JoinAction action = JoinAction::None;
JoinPayload payload;
bool nextActionPending = false;
void finish(uint32 updatedSsrc = 0) {
action = JoinAction::None;
ssrc = updatedSsrc;
}
};
struct SubChainPending {
QVector<MTPbytes> blocks;
int next = 0;
};
struct SubChainState {
std::vector<SubChainPending> pending;
mtpRequestId requestId = 0;
bool inShortPoll = false;
};
friend inline constexpr bool is_flag_type(SendUpdateType) {
return true;
}
GroupCall(
not_null<Delegate*> delegate,
Group::JoinInfo join,
StartConferenceInfo conference,
const MTPInputGroupCall &inputCall);
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
void broadcastPartCancel(not_null<LoadPartTask*> task);
void mediaChannelDescriptionsStart(
std::shared_ptr<MediaChannelDescriptionsTask> task);
void mediaChannelDescriptionsCancel(
not_null<MediaChannelDescriptionsTask*> task);
void requestCurrentTimeStart(
std::shared_ptr<RequestCurrentTimeTask> task);
void requestCurrentTimeCancel(
not_null<RequestCurrentTimeTask*> task);
[[nodiscard]] int64 approximateServerTimeInMs() const;
[[nodiscard]] bool mediaChannelDescriptionsFill(
not_null<MediaChannelDescriptionsTask*> task,
Fn<bool(uint32)> resolved = nullptr);
void checkMediaChannelDescriptions(Fn<bool(uint32)> resolved = nullptr);
void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
void handleUpdate(const MTPDupdateGroupCall &data);
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
void applySubChainUpdate(
int subchain,
const QVector<MTPbytes> &blocks,
int next);
[[nodiscard]] auto lookupVideoCodecPreferences() const
-> std::vector<tgcalls::VideoCodecName>;
bool tryCreateController();
void destroyController();
bool tryCreateScreencast();
void destroyScreencast();
void emitShareCameraError(Error error);
void emitShareScreenError(Error error);
void setState(State state);
void finish(FinishType type);
void maybeSendMutedUpdate(MuteState previous);
void sendSelfUpdate(SendUpdateType type);
void updateInstanceMuteState();
void updateInstanceVolumes();
void updateInstanceVolume(
const std::optional<Data::GroupCallParticipant> &was,
const Data::GroupCallParticipant &now);
void applyMeInCallLocally();
void startRejoin();
void rejoin();
void leave();
void rejoin(not_null<PeerData*> as);
void setJoinAs(not_null<PeerData*> as);
void saveDefaultJoinAs(not_null<PeerData*> as);
void subscribeToReal(not_null<Data::GroupCall*> real);
void setScheduledDate(TimeId date);
void setMessagesEnabled(bool enabled);
void rejoinPresentation();
void leavePresentation();
void checkNextJoinAction();
void sendJoinRequest();
void refreshLastBlockAndJoin();
void requestSubchainBlocks(int subchain, int height);
void sendOutboundBlock(QByteArray block);
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
void setInstanceMode(InstanceMode mode);
void setScreenInstanceConnected(tgcalls::GroupNetworkState networkState);
void setScreenInstanceMode(InstanceMode mode);
void checkLastSpoke();
void pushToTalkCancel();
void checkGlobalShortcutAvailability();
void checkJoined();
void checkFirstTimeJoined();
void notifyAboutAllowedToSpeak();
void playConnectingSound();
void stopConnectingSound();
void playConnectingSoundOnce();
void updateRequestedVideoChannels();
void updateRequestedVideoChannelsDelayed();
void fillActiveVideoEndpoints();
void editParticipant(
not_null<PeerData*> participantPeer,
bool mute,
std::optional<int> volume);
void applyParticipantLocally(
not_null<PeerData*> participantPeer,
bool mute,
std::optional<int> volume);
void applyQueuedSelfUpdates();
void sendPendingSelfUpdates();
void applySelfUpdate(const MTPDgroupCallParticipant &data);
void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);
void captureMuteChanged(bool mute) override;
rpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;
void setupMediaDevices();
void setupOutgoingVideo();
void initConferenceE2E();
void setupConferenceCall();
void trackParticipantsWithAccess();
void setScreenEndpoint(std::string endpoint);
void setCameraEndpoint(std::string endpoint);
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
void setVideoEndpointLarge(VideoEndpoint endpoint);
void markEndpointActive(
VideoEndpoint endpoint,
bool active,
bool paused);
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
void processConferenceStart(StartConferenceInfo conference);
void inviteToConference(
InviteRequest request,
Fn<not_null<InviteResult*>()> resultAddress,
Fn<void()> finishRequest);
[[nodiscard]] float64 singleSourceVolumeValue() const;
[[nodiscard]] int activeVideoSendersCount() const;
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
const not_null<Delegate*> _delegate;
std::shared_ptr<Data::GroupCall> _sharedCall;
std::unique_ptr<TdE2E::Call> _e2e;
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
rpl::variable<QByteArray> _emojiHash;
QByteArray _pendingOutboundBlock;
std::shared_ptr<StartConferenceInfo> _startConferenceInfo;
not_null<PeerData*> _peer; // Can change in legacy group migration.
rpl::event_stream<PeerData*> _peerStream;
not_null<History*> _history; // Can change in legacy group migration.
MTP::Sender _api;
rpl::event_stream<not_null<Data::GroupCall*>> _realChanges;
rpl::variable<State> _state = State::Creating;
base::flat_set<uint32> _unresolvedSsrcs;
rpl::event_stream<Error> _errors;
std::vector<Fn<void()>> _rejoinedCallbacks;
const std::unique_ptr<Group::Messages> _messages;
bool _recordingStoppedByMe = false;
bool _requestedVideoChannelsUpdateScheduled = false;
MTP::DcId _broadcastDcId = 0;
base::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;
base::flat_set<
std::shared_ptr<MediaChannelDescriptionsTask>,
base::pointer_comparator<
MediaChannelDescriptionsTask>> _mediaChannelDescriptionses;
base::flat_set<
std::shared_ptr<RequestCurrentTimeTask>,
base::pointer_comparator<
RequestCurrentTimeTask>> _requestCurrentTimes;
mtpRequestId _requestCurrentTimeRequestId = 0;
rpl::variable<not_null<PeerData*>> _joinAs;
std::vector<not_null<PeerData*>> _possibleJoinAs;
QString _joinHash;
QString _conferenceLinkSlug;
MsgId _conferenceJoinMessageId;
int64 _serverTimeMs = 0;
crl::time _serverTimeMsGotAt = 0;
QString _rtmpUrl;
QString _rtmpKey;
rpl::variable<MuteState> _muted = MuteState::Muted;
rpl::variable<bool> _canManage = false;
rpl::variable<bool> _videoIsWorking = false;
rpl::variable<bool> _emptyRtmp = false;
rpl::variable<bool> _messagesEnabled = false;
bool _initialMuteStateSent = false;
bool _acceptFields = false;
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
std::vector<MTPGroupCallParticipant> _queuedSelfUpdates;
CallId _id = 0;
CallId _accessHash = 0;
JoinState _joinState;
JoinState _screenJoinState;
std::string _cameraEndpoint;
std::string _screenEndpoint;
TimeId _scheduleDate = 0;
base::flat_set<uint32> _mySsrcs;
mtpRequestId _createRequestId = 0;
mtpRequestId _selfUpdateRequestId = 0;
rpl::variable<InstanceState> _instanceState
= InstanceState::Disconnected;
bool _instanceTransitioning = false;
InstanceMode _instanceMode = InstanceMode::None;
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;
base::has_weak_ptr _instanceGuard;
std::shared_ptr<tgcalls::VideoCaptureInterface> _cameraCapture;
rpl::variable<Webrtc::VideoState> _cameraState;
rpl::variable<bool> _isSharingCamera = false;
base::flat_map<std::string, SinkPointer> _pendingVideoOutputs;
rpl::variable<InstanceState> _screenInstanceState
= InstanceState::Disconnected;
InstanceMode _screenInstanceMode = InstanceMode::None;
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _screenInstance;
base::has_weak_ptr _screenInstanceGuard;
std::shared_ptr<tgcalls::VideoCaptureInterface> _screenCapture;
rpl::variable<Webrtc::VideoState> _screenState;
rpl::variable<bool> _isSharingScreen = false;
QString _screenDeviceId;
bool _screenWithAudio = false;
base::flags<SendUpdateType> _pendingSelfUpdates;
bool _requireARGB32 = true;
rpl::event_stream<LevelUpdate> _levelUpdates;
rpl::event_stream<VideoStateToggle> _videoStreamActiveUpdates;
rpl::event_stream<VideoStateToggle> _videoStreamPausedUpdates;
rpl::event_stream<VideoStateToggle> _videoStreamShownUpdates;
base::flat_map<
VideoEndpoint,
std::unique_ptr<VideoTrack>> _activeVideoTracks;
base::flat_set<VideoEndpoint> _shownVideoTracks;
rpl::variable<VideoEndpoint> _videoEndpointLarge;
rpl::variable<bool> _videoEndpointPinned = false;
crl::time _videoLargeTillTime = 0;
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
rpl::event_stream<Group::RejoinEvent> _rejoinEvents;
rpl::event_stream<> _allowedToSpeakNotifications;
rpl::event_stream<> _titleChanged;
base::Timer _lastSpokeCheckTimer;
base::Timer _checkJoinedTimer;
crl::time _lastSendProgressUpdate = 0;
Fn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;
Webrtc::DeviceResolver _playbackDeviceId;
Webrtc::DeviceResolver _captureDeviceId;
Webrtc::DeviceResolver _cameraDeviceId;
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
base::Timer _pushToTalkCancelTimer;
base::Timer _connectingSoundTimer;
bool _hadJoinedState = false;
bool _listenersHidden = false;
bool _rtmp = false;
bool _reloadedStaleCall = false;
int _singleSourceVolume = 0;
SubChainState _subchains[kSubChainsCount];
rpl::lifetime _lifetime;
};
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
const InviteResult &result);
} // namespace Calls

View File

@@ -0,0 +1,506 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_common.h"
#include "apiwrap.h"
#include "base/platform/base_platform_info.h"
#include "base/random.h"
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
#include "boxes/share_box.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "core/local_url_handlers.h"
#include "data/data_group_call.h"
#include "data/data_session.h"
#include "info/bot/starref/info_bot_starref_common.h"
#include "tde2e/tde2e_api.h"
#include "tde2e/tde2e_integration.h"
#include "ui/boxes/boost_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_media_view.h"
#include "styles/style_menu_icons.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h"
#include <QtWidgets/QApplication>
#include <QtGui/QClipboard>
namespace Calls::Group {
object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
#ifdef Q_OS_MAC
if (!Platform::IsMac10_15OrGreater()) {
return { nullptr };
}
return Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
rpl::combine(
tr::lng_group_call_mac_screencast_access(),
tr::lng_group_call_mac_recording()
) | rpl::map([](QString a, QString b) {
auto result = tr::rich(a);
result.append("\n\n").append(tr::rich(b));
return result;
}),
st::groupCallBoxLabel),
style::margins(
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom()));
box->addButton(tr::lng_group_call_mac_settings(), [=] {
Platform::OpenDesktopCapturePrivacySettings();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
});
#else // Q_OS_MAC
return { nullptr };
#endif // Q_OS_MAC
}
object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
not_null<QWidget*> parent,
const style::icon &icon,
const style::margins &padding) {
const auto logoSize = icon.size();
const auto logoOuter = logoSize.grownBy(padding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto logo = result.data();
logo->resize(logo->width(), logoOuter.height());
logo->paintRequest() | rpl::on_next([=, &icon] {
if (logo->width() < logoOuter.width()) {
return;
}
auto p = QPainter(logo);
auto hq = PainterHighQualityEnabler(p);
const auto x = (logo->width() - logoOuter.width()) / 2;
const auto outer = QRect(QPoint(x, 0), logoOuter);
p.setBrush(st::windowBgActive);
p.setPen(Qt::NoPen);
p.drawEllipse(outer);
icon.paintInCenter(p, outer);
}, logo->lifetime());
return result;
}
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
return MakeRoundActiveLogo(
parent,
st::confcallJoinLogo,
st::confcallJoinLogoPadding);
}
void ConferenceCallJoinConfirm(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Data::GroupCall> call,
UserData *maybeInviter,
Fn<void(Fn<void()> close)> join) {
box->setStyle(st::confcallJoinBox);
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
box->addRow(
MakeJoinCallLogo(box),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_join_title(),
st::boxTitle),
st::boxRowPadding + st::confcallLinkTitlePadding,
style::al_top);
const auto wrapName = [&](not_null<PeerData*> peer) {
return rpl::single(tr::bold(peer->shortName()));
};
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(maybeInviter
? tr::lng_confcall_join_text_inviter(
lt_user,
wrapName(maybeInviter),
tr::rich)
: tr::lng_confcall_join_text(tr::rich)),
st::confcallLinkCenteredText),
st::boxRowPadding,
style::al_top
)->setTryMakeSimilarLines(true);
const auto &participants = call->participants();
const auto known = int(participants.size());
if (known) {
const auto sep = box->addRow(
object_ptr<Ui::RpWidget>(box),
st::boxRowPadding + st::confcallJoinSepPadding);
sep->resize(sep->width(), st::normalFont->height);
sep->paintRequest() | rpl::on_next([=] {
auto p = QPainter(sep);
const auto line = st::lineWidth;
const auto top = st::confcallLinkFooterOrLineTop;
const auto fg = st::windowSubTextFg->b;
p.setOpacity(0.2);
p.fillRect(0, top, sep->width(), line, fg);
}, sep->lifetime());
auto peers = std::vector<not_null<PeerData*>>();
for (const auto &participant : participants) {
peers.push_back(participant.peer);
if (peers.size() == 3) {
break;
}
}
box->addRow(
CreateUserpicsWithMoreBadge(
box,
rpl::single(peers),
st::confcallJoinUserpics,
known),
st::boxRowPadding + st::confcallJoinUserpicsPadding);
const auto wrapByIndex = [&](int index) {
Expects(index >= 0 && index < known);
return wrapName(participants[index].peer);
};
auto text = (known == 1)
? tr::lng_confcall_already_joined_one(
lt_user,
wrapByIndex(0),
tr::rich)
: (known == 2)
? tr::lng_confcall_already_joined_two(
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
tr::rich)
: (known == 3)
? tr::lng_confcall_already_joined_three(
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
lt_third,
wrapByIndex(2),
tr::rich)
: tr::lng_confcall_already_joined_many(
lt_count,
rpl::single(1. * (std::max(known, call->fullCount()) - 2)),
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
tr::rich);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::confcallLinkCenteredText),
st::boxRowPadding,
style::al_top
)->setTryMakeSimilarLines(true);
}
box->addButton(tr::lng_confcall_join_button(), [=] {
join([weak = base::make_weak(box)] {
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
});
}
ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {
return {
.box = &st::groupCallLinkBox,
.menuToggle = &st::groupCallLinkMenu,
.menu = &st::groupCallPopupMenuWithIcons,
.close = &st::storiesStealthBoxClose,
.centerLabel = &st::groupCallLinkCenteredText,
.linkPreview = &st::groupCallLinkPreview,
.contextRevoke = &st::mediaMenuIconRemove,
.shareBox = std::make_shared<ShareBoxStyleOverrides>(
DarkShareBoxStyle()),
};
}
void ShowConferenceCallLinkBox(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<Data::GroupCall> call,
const ConferenceCallLinkArgs &args) {
const auto st = args.st;
const auto initial = args.initial;
const auto link = call->conferenceInviteLink();
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
bool resetting = false;
};
const auto state = box->lifetime().make_state<State>();
box->setStyle(st.box
? *st.box
: initial
? st::confcallLinkBoxInitial
: st::confcallLinkBox);
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
const auto close = box->addTopButton(
st.close ? *st.close : st::boxTitleClose,
[=] { box->closeBox(); });
if (!args.initial && call->canManage()) {
const auto toggle = Ui::CreateChild<Ui::IconButton>(
close->parentWidget(),
st.menuToggle ? *st.menuToggle : st::boxTitleMenu);
const auto handler = [=] {
if (state->resetting) {
return;
}
state->resetting = true;
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
const auto weak = base::make_weak(box);
call->session().api().request(
MTPphone_ToggleGroupCallSettings(
MTP_flags(Flag::f_reset_invite_hash),
call->input(),
MTPBool(), // join_muted
MTPBool(), // messages_enabled
MTPlong()) // send_paid_messages_stars
).done([=](const MTPUpdates &result) {
call->session().api().applyUpdates(result);
ShowConferenceCallLinkBox(show, call, args);
if (const auto strong = weak.get()) {
strong->closeBox();
}
show->showToast({
.title = tr::lng_confcall_link_revoked_title(
tr::now),
.text = {
tr::lng_confcall_link_revoked_text(tr::now),
},
});
}).send();
};
toggle->setClickedCallback([=] {
state->menu = base::make_unique_q<Ui::PopupMenu>(
toggle,
st.menu ? *st.menu : st::popupMenuWithIcons);
state->menu->addAction(
tr::lng_confcall_link_revoke(tr::now),
handler,
(st.contextRevoke
? st.contextRevoke
: &st::menuIconRemove));
state->menu->popup(QCursor::pos());
});
close->geometryValue(
) | rpl::on_next([=](QRect geometry) {
toggle->moveToLeft(
geometry.x() - toggle->width(),
geometry.y());
}, close->lifetime());
}
box->addRow(
Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_link_title(),
st.box ? st.box->title : st::boxTitle),
st::boxRowPadding + st::confcallLinkTitlePadding,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_link_about(),
(st.centerLabel
? *st.centerLabel
: st::confcallLinkCenteredText)),
st::boxRowPadding,
style::al_top
)->setTryMakeSimilarLines(true);
Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
const auto preview = box->addRow(
Info::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));
Ui::AddSkip(box->verticalLayout());
const auto copyCallback = [=] {
QApplication::clipboard()->setText(link);
show->showToast(tr::lng_username_copied(tr::now));
};
const auto shareCallback = [=] {
FastShareLink(
show,
link,
st.shareBox ? *st.shareBox : ShareBoxStyleOverrides());
};
preview->setClickedCallback(copyCallback);
const auto share = box->addButton(
tr::lng_group_invite_share(),
shareCallback,
st::confcallLinkShareButton);
const auto copy = box->addButton(
tr::lng_group_invite_copy(),
copyCallback,
st::confcallLinkCopyButton);
rpl::combine(
box->widthValue(),
copy->widthValue(),
share->widthValue()
) | rpl::on_next([=] {
const auto width = st::boxWideWidth;
const auto padding = st::confcallLinkBox.buttonPadding;
const auto available = width - 2 * padding.right();
const auto buttonWidth = (available - padding.left()) / 2;
copy->resizeToWidth(buttonWidth);
share->resizeToWidth(buttonWidth);
copy->moveToLeft(padding.right(), copy->y(), width);
share->moveToRight(padding.right(), share->y(), width);
}, box->lifetime());
if (!initial) {
return;
}
const auto sep = Ui::CreateChild<Ui::FlatLabel>(
copy->parentWidget(),
tr::lng_confcall_link_or(),
st::confcallLinkFooterOr);
sep->paintRequest() | rpl::on_next([=] {
auto p = QPainter(sep);
const auto text = sep->textMaxWidth();
const auto white = (sep->width() - 2 * text) / 2;
const auto line = st::lineWidth;
const auto top = st::confcallLinkFooterOrLineTop;
const auto fg = st::windowSubTextFg->b;
p.setOpacity(0.4);
p.fillRect(0, top, white, line, fg);
p.fillRect(sep->width() - white, top, white, line, fg);
}, sep->lifetime());
const auto footer = Ui::CreateChild<Ui::FlatLabel>(
copy->parentWidget(),
tr::lng_confcall_link_join(
lt_link,
tr::lng_confcall_link_join_link(
lt_arrow,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
[](QString v) { return tr::link(v); }),
tr::marked),
(st.centerLabel
? *st.centerLabel
: st::confcallLinkCenteredText));
footer->setTryMakeSimilarLines(true);
footer->setClickHandlerFilter([=](const auto &...) {
if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.linkSlug = std::move(slug),
});
}
return false;
});
copy->geometryValue() | rpl::on_next([=](QRect geometry) {
const auto width = st::boxWideWidth
- st::boxRowPadding.left()
- st::boxRowPadding.right();
footer->resizeToWidth(width);
const auto top = geometry.y()
+ geometry.height()
+ st::confcallLinkFooterOrTop;
sep->resizeToWidth(width / 2);
sep->move(
st::boxRowPadding.left() + (width - sep->width()) / 2,
top);
footer->moveToLeft(
st::boxRowPadding.left(),
top + sep->height() + st::confcallLinkFooterOrSkip);
}, footer->lifetime());
}));
}
void MakeConferenceCall(ConferenceFactoryArgs &&args) {
const auto show = std::move(args.show);
const auto finished = std::move(args.finished);
const auto session = &show->session();
const auto fail = [=](QString error) {
show->showToast(error);
if (const auto onstack = finished) {
onstack(false);
}
};
session->api().request(MTPphone_CreateConferenceCall(
MTP_flags(0),
MTP_int(base::RandomValue<int32>()),
MTPint256(), // public_key
MTPbytes(), // block
MTPDataJSON() // params
)).done([=](const MTPUpdates &result) {
auto call = session->data().sharedConferenceCallFind(result);
if (!call) {
fail(u"Call not found!"_q);
return;
}
session->api().applyUpdates(result);
const auto link = call ? call->conferenceInviteLink() : QString();
if (link.isEmpty()) {
fail(u"Call link not found!"_q);
return;
}
Calls::Group::ShowConferenceCallLinkBox(
show,
call,
{ .initial = true });
if (const auto onstack = finished) {
finished(true);
}
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
QString ExtractConferenceSlug(const QString &link) {
const auto local = Core::TryConvertUrlToLocal(link);
const auto parts1 = QStringView(local).split('#');
if (!parts1.isEmpty()) {
const auto parts2 = parts1.front().split('&');
if (!parts2.isEmpty()) {
const auto parts3 = parts2.front().split(u"slug="_q);
if (parts3.size() > 1) {
return parts3.back().toString();
}
}
}
return QString();
}
} // namespace Calls::Group

View File

@@ -0,0 +1,209 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "base/weak_ptr.h"
class UserData;
struct ShareBoxStyleOverrides;
namespace style {
struct Box;
struct FlatLabel;
struct IconButton;
struct InputField;
struct PopupMenu;
} // namespace style
namespace Data {
class GroupCall;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Ui {
class Show;
class RpWidget;
class GenericBox;
} // namespace Ui
namespace TdE2E {
class Call;
} // namespace TdE2E
namespace tgcalls {
class VideoCaptureInterface;
} // namespace tgcalls
namespace Window {
class SessionController;
} // namespace Window
namespace Calls {
class Window;
struct InviteRequest {
not_null<UserData*> user;
bool video = false;
};
struct InviteResult {
std::vector<not_null<UserData*>> invited;
std::vector<not_null<UserData*>> alreadyIn;
std::vector<not_null<UserData*>> privacyRestricted;
std::vector<not_null<UserData*>> kicked;
std::vector<not_null<UserData*>> failed;
};
struct StartConferenceInfo {
std::shared_ptr<Main::SessionShow> show;
std::shared_ptr<Data::GroupCall> call;
QString linkSlug;
MsgId joinMessageId;
std::vector<InviteRequest> invite;
bool sharingLink = false;
bool migrating = false;
bool muted = false;
std::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;
QString videoCaptureScreenId;
};
struct ConferencePanelMigration {
std::shared_ptr<Window> window;
};
} // namespace Calls
namespace Calls::Group {
constexpr auto kDefaultVolume = 10000;
constexpr auto kMaxVolume = 20000;
constexpr auto kBlobsEnterDuration = crl::time(250);
struct MuteRequest {
not_null<PeerData*> peer;
bool mute = false;
bool locallyOnly = false;
};
struct VolumeRequest {
not_null<PeerData*> peer;
int volume = kDefaultVolume;
bool finalized = true;
bool locallyOnly = false;
};
struct ParticipantState {
not_null<PeerData*> peer;
std::optional<int> volume;
bool mutedByMe = false;
bool locallyOnly = false;
};
struct RejoinEvent {
not_null<PeerData*> wasJoinAs;
not_null<PeerData*> nowJoinAs;
};
struct RtmpInfo {
QString url;
QString key;
};
struct JoinInfo {
not_null<PeerData*> peer;
not_null<PeerData*> joinAs;
std::vector<not_null<PeerData*>> possibleJoinAs;
QString joinHash;
RtmpInfo rtmpInfo;
TimeId scheduleDate = 0;
bool rtmp = false;
};
enum class PanelMode {
Default,
Wide,
VideoStream,
};
enum class VideoQuality {
Thumbnail,
Medium,
Full,
};
enum class Error {
NoCamera,
CameraFailed,
ScreenFailed,
MutedNoCamera,
MutedNoScreen,
DisabledNoCamera,
DisabledNoScreen,
};
enum class StickedTooltip {
Camera = 0x01,
Microphone = 0x02,
};
constexpr inline bool is_flag_type(StickedTooltip) {
return true;
}
using StickedTooltips = base::flags<StickedTooltip>;
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
not_null<QWidget*> parent,
const style::icon &icon,
const style::margins &padding);
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
not_null<QWidget*> parent);
void ConferenceCallJoinConfirm(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Data::GroupCall> call,
UserData *maybeInviter,
Fn<void(Fn<void()> close)> join);
struct ConferenceCallLinkStyleOverrides {
const style::Box *box = nullptr;
const style::IconButton *menuToggle = nullptr;
const style::PopupMenu *menu = nullptr;
const style::IconButton *close = nullptr;
const style::FlatLabel *centerLabel = nullptr;
const style::InputField *linkPreview = nullptr;
const style::icon *contextRevoke = nullptr;
std::shared_ptr<ShareBoxStyleOverrides> shareBox;
};
[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();
struct ConferenceCallLinkArgs {
ConferenceCallLinkStyleOverrides st;
bool initial = false;
};
void ShowConferenceCallLinkBox(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<Data::GroupCall> call,
const ConferenceCallLinkArgs &args);
struct ConferenceFactoryArgs {
std::shared_ptr<Main::SessionShow> show;
Fn<void(bool)> finished;
bool joining = false;
StartConferenceInfo info;
};
void MakeConferenceCall(ConferenceFactoryArgs &&args);
[[nodiscard]] QString ExtractConferenceSlug(const QString &link);
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/add_participants_box.h"
namespace Calls {
class Call;
class GroupCall;
struct InviteRequest;
} // namespace Calls
namespace Data {
class GroupCall;
} // namespace Data
namespace Calls::Group {
class InviteController final : public ParticipantsBoxController {
public:
InviteController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void itemDeselectedHook(not_null<PeerData*> peer) override;
[[nodiscard]] auto peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*>;
[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;
[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;
private:
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const override;
not_null<PeerData*> _peer;
const base::flat_set<not_null<UserData*>> _alreadyIn;
mutable base::flat_set<not_null<UserData*>> _inGroup;
rpl::event_stream<not_null<UserData*>> _rowAdded;
};
class InviteContactsController final : public AddParticipantsBoxController {
public:
InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> discoveredInGroup);
private:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
bool needsInviteLinkButton() override {
return false;
}
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
rpl::producer<not_null<UserData*>> _discoveredInGroup;
rpl::lifetime _lifetime;
};
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<GroupCall*> call,
Fn<void(TextWithEntities&&)> showToast,
Fn<void()> shareConferenceLink = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<InviteRequest>)> inviteUsers,
Fn<void()> shareLink);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
std::shared_ptr<Data::GroupCall> call,
MsgId inviteMsgId,
std::vector<not_null<UserData*>> prioritize);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(
not_null<::Window::SessionController*> window,
Fn<void()> created = nullptr,
MsgId discardedInviteMsgId = 0,
std::vector<not_null<UserData*>> prioritize = {});
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "boxes/peer_list_box.h"
namespace Ui {
class RpWidget;
class ScrollArea;
class VerticalLayout;
class SettingsButton;
namespace GL {
enum class Backend;
} // namespace GL
} // namespace Ui
namespace Data {
class GroupCall;
} // namespace Data
namespace Calls {
class GroupCall;
struct FingerprintBadgeState;
} // namespace Calls
namespace Calls::Group {
class Viewport;
class MembersRow;
struct VolumeRequest;
struct MuteRequest;
enum class PanelMode;
class Members final
: public Ui::RpWidget
, private PeerListContentDelegate {
public:
Members(
not_null<QWidget*> parent,
not_null<GroupCall*> call,
PanelMode mode,
Ui::GL::Backend backend);
~Members();
[[nodiscard]] not_null<Viewport*> viewport() const;
[[nodiscard]] int desiredHeight() const;
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
[[nodiscard]] auto toggleMuteRequests() const
-> rpl::producer<Group::MuteRequest>;
[[nodiscard]] auto changeVolumeRequests() const
-> rpl::producer<Group::VolumeRequest>;
[[nodiscard]] auto kickParticipantRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] rpl::producer<> addMembersRequests() const {
return _addMemberRequests.events();
}
[[nodiscard]] rpl::producer<> shareLinkRequests() const {
return _shareLinkRequests.events();
}
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(
not_null<PeerData*> peer) const;
void setMode(PanelMode mode);
[[nodiscard]] QRect getInnerGeometry() const;
private:
class Controller;
struct VideoTile;
using ListWidget = PeerListContent;
void resizeEvent(QResizeEvent *e) override;
// PeerListContentDelegate interface.
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
void peerListSetHideEmpty(bool hide) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
not_null<PeerListRow*> row) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
void setupAddMember(not_null<GroupCall*> call);
void resizeToList();
void setupList();
void setupFingerprint();
void setupFakeRoundCorners();
void trackViewportGeometry();
void updateControlsGeometry();
const not_null<GroupCall*> _call;
rpl::variable<PanelMode> _mode = PanelMode();
object_ptr<Ui::ScrollArea> _scroll;
std::unique_ptr<Controller> _listController;
not_null<Ui::VerticalLayout*> _layout;
Ui::RpWidget *_fingerprint = nullptr;
rpl::event_stream<> _fingerprintRepaints;
const FingerprintBadgeState *_fingerprintState = nullptr;
const not_null<Ui::RpWidget*> _videoWrap;
std::unique_ptr<Viewport> _viewport;
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
rpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;
RpWidget *_topSkip = nullptr;
RpWidget *_bottomSkip = nullptr;
ListWidget *_list = nullptr;
rpl::event_stream<> _addMemberRequests;
rpl::event_stream<> _shareLinkRequests;
mutable std::unique_ptr<MembersRow> _rtmpFakeRow;
rpl::variable<bool> _canInviteByLink;
rpl::variable<bool> _canAddMembers;
};
} // namespace Calls

View File

@@ -0,0 +1,822 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_members_row.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "data/data_peer.h"
#include "data/data_group_call.h"
#include "ui/paint/arcs.h"
#include "ui/paint/blobs.h"
#include "ui/text/text_options.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "webrtc/webrtc_video_track.h"
#include "styles/style_calls.h"
namespace Calls::Group {
namespace {
constexpr auto kLevelDuration = 100. + 500. * 0.23;
constexpr auto kBlobScale = 0.605;
constexpr auto kMinorBlobFactor = 0.9f;
constexpr auto kUserpicMinScale = 0.8;
constexpr auto kMaxLevel = 1.;
constexpr auto kWideScale = 5;
constexpr auto kArcsStrokeRatio = 0.8;
const auto kSpeakerThreshold = std::vector<float>{
Group::kDefaultVolume * 0.1f / Group::kMaxVolume,
Group::kDefaultVolume * 0.9f / Group::kMaxVolume };
auto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {
return { {
{
.segmentsCount = 6,
.minScale = kBlobScale * kMinorBlobFactor,
.minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor,
.maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor,
.speedScale = 1.,
.alpha = .5,
},
{
.segmentsCount = 8,
.minScale = kBlobScale,
.minRadius = (float)st::groupCallRowBlobMinRadius,
.maxRadius = (float)st::groupCallRowBlobMaxRadius,
.speedScale = 1.,
.alpha = .2,
},
} };
}
[[nodiscard]] QString StatusPercentString(float volume) {
return QString::number(int(base::SafeRound(volume * 200))) + '%';
}
[[nodiscard]] int StatusPercentWidth(const QString &percent) {
return st::normalFont->width(percent);
}
} // namespace
struct MembersRow::BlobsAnimation {
BlobsAnimation(
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
float levelDuration,
float maxLevel);
Ui::Paint::Blobs blobs;
crl::time lastTime = 0;
crl::time lastSoundingUpdateTime = 0;
float64 enter = 0.;
QImage userpicCache;
InMemoryKey userpicKey;
rpl::lifetime lifetime;
};
struct MembersRow::StatusIcon {
StatusIcon(bool shown, float volume);
const style::icon &speaker;
Ui::Paint::ArcsAnimation arcs;
Ui::Animations::Simple arcsAnimation;
Ui::Animations::Simple shownAnimation;
QString percent;
int percentWidth = 0;
int arcsWidth = 0;
int wasArcsWidth = 0;
bool shown = true;
rpl::lifetime lifetime;
};
MembersRow::BlobsAnimation::BlobsAnimation(
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
float levelDuration,
float maxLevel)
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
style::PaletteChanged(
) | rpl::on_next([=] {
userpicCache = QImage();
}, lifetime);
}
MembersRow::StatusIcon::StatusIcon(bool shown, float volume)
: speaker(st::groupCallStatusSpeakerIcon)
, arcs(
st::groupCallStatusSpeakerArcsAnimation,
kSpeakerThreshold,
volume,
Ui::Paint::ArcsAnimation::Direction::Right)
, percent(StatusPercentString(volume))
, percentWidth(StatusPercentWidth(percent))
, shown(shown) {
}
MembersRow::MembersRow(
not_null<MembersRowDelegate*> delegate,
not_null<PeerData*> participantPeer)
: PeerListRow(participantPeer)
, _delegate(delegate) {
refreshStatus();
_about.setText(st::defaultTextStyle, participantPeer->about());
}
MembersRow::~MembersRow() = default;
void MembersRow::setSkipLevelUpdate(bool value) {
_skipLevelUpdate = value;
}
void MembersRow::updateStateInvited(bool calling) {
setVolume(Group::kDefaultVolume);
setState(calling ? State::Calling : State::Invited);
setSounding(false);
setSpeaking(false);
_mutedByMe = false;
_raisedHandRating = 0;
refreshStatus();
}
void MembersRow::updateStateWithAccess() {
setVolume(Group::kDefaultVolume);
setState(State::WithAccess);
setSounding(false);
setSpeaking(false);
_mutedByMe = false;
_raisedHandRating = 0;
refreshStatus();
}
void MembersRow::updateState(const Data::GroupCallParticipant &participant) {
setVolume(participant.volume);
if (!participant.muted
|| (participant.sounding && participant.ssrc != 0)
|| (participant.additionalSounding
&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {
setState(State::Active);
setSounding((participant.sounding && participant.ssrc != 0)
|| (participant.additionalSounding
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
setSpeaking((participant.speaking && participant.ssrc != 0)
|| (participant.additionalSpeaking
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
_mutedByMe = participant.mutedByMe;
_raisedHandRating = 0;
} else if (participant.canSelfUnmute) {
setState(State::Inactive);
setSounding(false);
setSpeaking(false);
_mutedByMe = participant.mutedByMe;
_raisedHandRating = 0;
} else {
setSounding(false);
setSpeaking(false);
_mutedByMe = participant.mutedByMe;
_raisedHandRating = participant.raisedHandRating;
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
}
refreshStatus();
}
void MembersRow::setSpeaking(bool speaking) {
if (_speaking == speaking) {
return;
}
_speaking = speaking;
_speakingAnimation.start(
[=] { _delegate->rowUpdateRow(this); },
_speaking ? 0. : 1.,
_speaking ? 1. : 0.,
st::widgetFadeDuration);
if (!_speaking
|| _mutedByMe
|| (_state == State::Muted)
|| (_state == State::RaisedHand)) {
if (_statusIcon) {
_statusIcon = nullptr;
_delegate->rowUpdateRow(this);
}
} else if (!_statusIcon) {
_statusIcon = std::make_unique<StatusIcon>(
(_volume != Group::kDefaultVolume),
(float)_volume / Group::kMaxVolume);
_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);
_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();
_statusIcon->arcs.startUpdateRequests(
) | rpl::on_next([=] {
if (!_statusIcon->arcsAnimation.animating()) {
_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;
}
auto callback = [=](float64 value) {
_statusIcon->arcs.update(crl::now());
_statusIcon->arcsWidth = anim::interpolate(
_statusIcon->wasArcsWidth,
_statusIcon->arcs.finishedWidth(),
value);
_delegate->rowUpdateRow(this);
};
_statusIcon->arcsAnimation.start(
std::move(callback),
0.,
1.,
st::groupCallSpeakerArcsAnimation.duration);
}, _statusIcon->lifetime);
}
}
void MembersRow::setSounding(bool sounding) {
if (_sounding == sounding) {
return;
}
_sounding = sounding;
if (!_sounding) {
_blobsAnimation = nullptr;
} else if (!_blobsAnimation) {
_blobsAnimation = std::make_unique<BlobsAnimation>(
RowBlobs() | ranges::to_vector,
kLevelDuration,
kMaxLevel);
_blobsAnimation->lastTime = crl::now();
updateLevel(GroupCall::kSpeakLevelThreshold);
}
}
void MembersRow::clearRaisedHandStatus() {
if (!_raisedHandStatus) {
return;
}
_raisedHandStatus = false;
refreshStatus();
_delegate->rowUpdateRow(this);
}
void MembersRow::setState(State state) {
if (_state == state) {
return;
}
const auto wasActive = (_state == State::Active);
const auto wasMuted = (_state == State::Muted)
|| (_state == State::RaisedHand);
const auto wasRaisedHand = (_state == State::RaisedHand);
_state = state;
const auto nowActive = (_state == State::Active);
const auto nowMuted = (_state == State::Muted)
|| (_state == State::RaisedHand);
const auto nowRaisedHand = (_state == State::RaisedHand);
if (!wasRaisedHand && nowRaisedHand) {
_raisedHandStatus = true;
_delegate->rowScheduleRaisedHandStatusRemove(this);
}
if (nowActive != wasActive) {
_activeAnimation.start(
[=] { _delegate->rowUpdateRow(this); },
nowActive ? 0. : 1.,
nowActive ? 1. : 0.,
st::widgetFadeDuration);
}
if (nowMuted != wasMuted) {
_mutedAnimation.start(
[=] { _delegate->rowUpdateRow(this); },
nowMuted ? 0. : 1.,
nowMuted ? 1. : 0.,
st::widgetFadeDuration);
}
}
void MembersRow::setVolume(int volume) {
_volume = volume;
if (_statusIcon) {
const auto floatVolume = (float)volume / Group::kMaxVolume;
_statusIcon->arcs.setValue(floatVolume);
_statusIcon->percent = StatusPercentString(floatVolume);
_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);
const auto shown = (volume != Group::kDefaultVolume);
if (_statusIcon->shown != shown) {
_statusIcon->shown = shown;
_statusIcon->shownAnimation.start(
[=] { _delegate->rowUpdateRow(this); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::groupCallSpeakerArcsAnimation.duration);
}
}
}
void MembersRow::updateLevel(float level) {
Expects(_blobsAnimation != nullptr);
const auto spoke = (level >= GroupCall::kSpeakLevelThreshold)
? crl::now()
: crl::time();
if (spoke && _speaking) {
_speakingLastTime = spoke;
}
if (_skipLevelUpdate) {
return;
}
if (spoke) {
_blobsAnimation->lastSoundingUpdateTime = spoke;
}
_blobsAnimation->blobs.setLevel(level);
}
void MembersRow::updateBlobAnimation(crl::time now) {
Expects(_blobsAnimation != nullptr);
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
+ Data::GroupCall::kSoundStatusKeptFor;
const auto soundingStartsFinishing = soundingFinishesAt
- kBlobsEnterDuration;
const auto soundingFinishes = (soundingStartsFinishing < now);
if (soundingFinishes) {
_blobsAnimation->enter = std::clamp(
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
0.,
1.);
} else if (_blobsAnimation->enter < 1.) {
_blobsAnimation->enter = std::clamp(
(_blobsAnimation->enter
+ ((now - _blobsAnimation->lastTime)
/ float64(kBlobsEnterDuration))),
0.,
1.);
}
_blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime);
_blobsAnimation->lastTime = now;
}
void MembersRow::ensureUserpicCache(
Ui::PeerUserpicView &view,
int size) {
Expects(_blobsAnimation != nullptr);
const auto user = peer();
const auto key = user->userpicUniqueKey(view);
const auto full = QSize(size, size)
* kWideScale
* style::DevicePixelRatio();
auto &cache = _blobsAnimation->userpicCache;
if (cache.isNull()) {
cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(style::DevicePixelRatio());
} else if (_blobsAnimation->userpicKey == key
&& cache.size() == full) {
return;
}
_blobsAnimation->userpicKey = key;
cache.fill(Qt::transparent);
{
Painter p(&cache);
const auto skip = (kWideScale - 1) / 2 * size;
user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size);
}
}
void MembersRow::paintBlobs(
Painter &p,
int x,
int y,
int sizew,
int sizeh,
PanelMode mode) {
if (!_blobsAnimation) {
return;
}
auto size = sizew;
const auto shift = QPointF(x + size / 2., y + size / 2.);
auto hq = PainterHighQualityEnabler(p);
p.translate(shift);
const auto brush = _mutedByMe
? st::groupCallMemberMutedIcon->b
: anim::brush(
st::groupCallMemberInactiveStatus,
st::groupCallMemberActiveStatus,
_speakingAnimation.value(_speaking ? 1. : 0.));
_blobsAnimation->blobs.paint(p, brush);
p.translate(-shift);
p.setOpacity(1.);
}
void MembersRow::paintScaledUserpic(
Painter &p,
Ui::PeerUserpicView &userpic,
int x,
int y,
int outerWidth,
int sizew,
int sizeh,
PanelMode mode) {
auto size = sizew;
if (!_blobsAnimation) {
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
return;
}
const auto enter = _blobsAnimation->enter;
const auto &minScale = kUserpicMinScale;
const auto scaleUserpic = minScale
+ (1. - minScale) * _blobsAnimation->blobs.currentLevel();
const auto scale = scaleUserpic * enter + 1. * (1. - enter);
if (scale == 1.) {
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
return;
}
ensureUserpicCache(userpic, size);
PainterHighQualityEnabler hq(p);
auto target = QRect(
x + (1 - kWideScale) / 2 * size,
y + (1 - kWideScale) / 2 * size,
kWideScale * size,
kWideScale * size);
auto shrink = anim::interpolate(
(1 - kWideScale) / 2 * size,
0,
scale);
auto margins = QMargins(shrink, shrink, shrink, shrink);
p.drawImage(
target.marginsAdded(margins),
_blobsAnimation->userpicCache);
}
void MembersRow::paintMuteIcon(
QPainter &p,
QRect iconRect,
MembersRowStyle style) {
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
}
QString MembersRow::generateName() {
const auto result = peer()->name();
if (result.isEmpty()) {
DEBUG_LOG(("UnknownParticipant: %1, Loaded: %2, Name Version: %3"
).arg(peerToUser(peer()->id).bare
).arg(peer()->isLoaded() ? "TRUE" : "FALSE"
).arg(peer()->nameVersion()));
}
return result.isEmpty()
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
: result;
}
QString MembersRow::generateShortName() {
const auto result = peer()->shortName();
return result.isEmpty()
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
: result;
}
auto MembersRow::generatePaintUserpicCallback(bool forceRound)
-> PaintRoundImageCallback {
return [=](Painter &p, int x, int y, int outerWidth, int size) {
const auto outer = outerWidth;
paintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default);
};
}
void MembersRow::paintComplexUserpic(
Painter &p,
int x,
int y,
int outerWidth,
int sizew,
int sizeh,
PanelMode mode,
bool selected) {
paintBlobs(p, x, y, sizew, sizeh, mode);
paintScaledUserpic(
p,
ensureUserpicView(),
x,
y,
outerWidth,
sizew,
sizeh,
mode);
}
int MembersRow::statusIconWidth(bool skipIcon) const {
if (!_statusIcon || !_speaking) {
return 0;
}
const auto shown = _statusIcon->shownAnimation.value(
_statusIcon->shown ? 1. : 0.);
const auto iconWidth = skipIcon
? 0
: (_statusIcon->speaker.width() + _statusIcon->arcsWidth);
const auto full = iconWidth
+ _statusIcon->percentWidth
+ st::normalFont->spacew;
return int(base::SafeRound(shown * full));
}
int MembersRow::statusIconHeight() const {
return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;
}
void MembersRow::paintStatusIcon(
Painter &p,
int x,
int y,
const style::PeerListItem &st,
const style::font &font,
bool selected,
bool skipIcon) {
if (!_statusIcon) {
return;
}
const auto shown = _statusIcon->shownAnimation.value(
_statusIcon->shown ? 1. : 0.);
if (shown == 0.) {
return;
}
p.setFont(font);
const auto color = (_speaking
? st.statusFgActive
: (selected ? st.statusFgOver : st.statusFg))->c;
p.setPen(color);
const auto speakerRect = QRect(
QPoint(x, y + (font->height - statusIconHeight()) / 2),
_statusIcon->speaker.size());
const auto arcPosition = speakerRect.topLeft()
+ QPoint(
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
speakerRect.height() / 2);
const auto iconWidth = skipIcon
? 0
: (speakerRect.width() + _statusIcon->arcsWidth);
const auto fullWidth = iconWidth
+ _statusIcon->percentWidth
+ st::normalFont->spacew;
p.save();
if (shown < 1.) {
const auto centerx = speakerRect.x() + fullWidth / 2;
const auto centery = speakerRect.y() + speakerRect.height() / 2;
p.translate(centerx, centery);
p.scale(shown, shown);
p.translate(-centerx, -centery);
}
if (!skipIcon) {
_statusIcon->speaker.paint(
p,
speakerRect.topLeft(),
speakerRect.width(),
color);
p.translate(arcPosition);
_statusIcon->arcs.paint(p, color);
p.translate(-arcPosition);
}
p.setFont(st::normalFont);
p.setPen(st.statusFgActive);
p.drawTextLeft(
x + iconWidth,
y,
fullWidth,
_statusIcon->percent);
p.restore();
}
void MembersRow::setAbout(const QString &about) {
if (_about.toString() == about) {
return;
}
_about.setText(st::defaultTextStyle, about);
_delegate->rowUpdateRow(this);
}
void MembersRow::paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) {
paintComplexStatusText(
p,
st,
x,
y,
availableWidth,
outerWidth,
selected,
MembersRowStyle::Default);
}
void MembersRow::paintComplexStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected,
MembersRowStyle style) {
const auto skip = (style == MembersRowStyle::Default)
? _delegate->rowPaintStatusIcon(
p,
x,
y,
outerWidth,
this,
computeIconState(MembersRowStyle::Narrow))
: 0;
const auto narrowMode = (skip > 0);
x += skip;
availableWidth -= skip;
const auto &font = st::normalFont;
const auto useAbout = !_about.isEmpty()
&& (_state != State::WithAccess)
&& (_state != State::Invited)
&& (_state != State::Calling)
&& (style != MembersRowStyle::Video)
&& ((_state == State::RaisedHand && !_raisedHandStatus)
|| (_state != State::RaisedHand && !_speaking));
if (!useAbout
&& _state != State::Invited
&& _state != State::Calling
&& _state != State::WithAccess
&& !_mutedByMe) {
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
const auto translatedWidth = statusIconWidth(narrowMode);
p.translate(translatedWidth, 0);
const auto guard = gsl::finally([&] {
p.translate(-translatedWidth, 0);
});
const auto &style = (!narrowMode
|| (_state == State::RaisedHand && _raisedHandStatus))
? st
: st::groupCallNarrowMembersListItem;
PeerListRow::paintStatusText(
p,
style,
x,
y,
availableWidth - translatedWidth,
outerWidth,
selected);
return;
}
p.setPen((style == MembersRowStyle::Video)
? st::groupCallVideoSubTextFg
: _mutedByMe
? st::groupCallMemberMutedIcon
: st::groupCallMemberNotJoinedStatus);
if (!_mutedByMe && useAbout) {
return _about.draw(p, {
.position = QPoint(x, y),
.outerWidth = outerWidth,
.availableWidth = availableWidth,
.elisionLines = 1,
});
} else {
p.setFont(font);
p.drawTextLeft(
x,
y,
outerWidth,
(_mutedByMe
? tr::lng_group_call_muted_by_me_status(tr::now)
: _delegate->rowIsMe(peer())
? tr::lng_status_connecting(tr::now)
: (_state == State::WithAccess)
? tr::lng_group_call_blockchain_only_status(tr::now)
: (_state == State::Calling)
? tr::lng_group_call_calling_status(tr::now)
: tr::lng_group_call_invited_status(tr::now)));
}
}
QSize MembersRow::rightActionSize() const {
return _delegate->rowIsNarrow() ? QSize() : QSize(
st::groupCallActiveButton.width,
st::groupCallActiveButton.height);
}
bool MembersRow::rightActionDisabled() const {
return _delegate->rowIsMe(peer())
|| (_state == State::Invited)
|| (_state == State::Calling)
|| !_delegate->rowCanMuteMembers();
}
QMargins MembersRow::rightActionMargins() const {
return QMargins(
0,
0,
st::groupCallMemberButtonSkip,
0);
}
void MembersRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
auto size = rightActionSize();
const auto iconRect = style::rtlrect(
x,
y,
size.width(),
size.height(),
outerWidth);
if (_state == State::Invited
|| _state == State::Calling
|| _state == State::WithAccess) {
_actionRipple = nullptr;
}
if (_actionRipple) {
_actionRipple->paint(
p,
x + st::groupCallActiveButton.rippleAreaPosition.x(),
y + st::groupCallActiveButton.rippleAreaPosition.y(),
outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
paintMuteIcon(p, iconRect);
}
MembersRowDelegate::IconState MembersRow::computeIconState(
MembersRowStyle style) const {
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
const auto active = _activeAnimation.value(
(_state == State::Active) ? 1. : 0.);
const auto muted = _mutedAnimation.value(
(_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.);
return {
.speaking = speaking,
.active = active,
.muted = muted,
.mutedByMe = _mutedByMe,
.raisedHand = (_state == State::RaisedHand),
.invited = (_state == State::Invited),
.calling = (_state == State::Calling),
.style = style,
};
}
void MembersRow::showContextMenu() {
return _delegate->rowShowContextMenu(this);
}
void MembersRow::refreshStatus() {
setCustomStatus(
(_speaking
? tr::lng_group_call_active(tr::now)
: _raisedHandStatus
? tr::lng_group_call_raised_hand_status(tr::now)
: tr::lng_group_call_inactive(tr::now)),
_speaking);
}
void MembersRow::rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
st::groupCallActiveButton.rippleAreaSize,
st::groupCallActiveButton.rippleAreaSize));
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::groupCallActiveButton.ripple,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition);
}
void MembersRow::refreshName(const style::PeerListItem &st) {
PeerListRow::refreshName(st);
//_narrowName = Ui::Text::String();
}
void MembersRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
} // namespace Calls::Group

View File

@@ -0,0 +1,230 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "boxes/peer_list_box.h"
#include "calls/group/calls_group_common.h"
class PeerData;
class Painter;
namespace Data {
struct GroupCallParticipant;
} // namespace Data
namespace Ui {
class RippleAnimation;
struct PeerUserpicView;
} // namespace Ui
namespace Calls::Group {
enum class MembersRowStyle : uchar {
Default,
Narrow,
Video,
};
class MembersRow;
class MembersRowDelegate {
public:
struct IconState {
float64 speaking = 0.;
float64 active = 0.;
float64 muted = 0.;
bool mutedByMe = false;
bool raisedHand = false;
bool invited = false;
bool calling = false;
MembersRowStyle style = MembersRowStyle::Default;
};
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
virtual bool rowCanMuteMembers() = 0;
virtual void rowUpdateRow(not_null<MembersRow*> row) = 0;
virtual void rowScheduleRaisedHandStatusRemove(
not_null<MembersRow*> row) = 0;
virtual void rowPaintIcon(
QPainter &p,
QRect rect,
const IconState &state) = 0;
virtual int rowPaintStatusIcon(
QPainter &p,
int x,
int y,
int outerWidth,
not_null<MembersRow*> row,
const IconState &state) = 0;
virtual bool rowIsNarrow() = 0;
virtual void rowShowContextMenu(not_null<PeerListRow*> row) = 0;
};
class MembersRow final : public PeerListRow {
public:
MembersRow(
not_null<MembersRowDelegate*> delegate,
not_null<PeerData*> participantPeer);
~MembersRow();
enum class State {
Active,
Inactive,
Muted,
RaisedHand,
Invited,
Calling,
WithAccess,
};
void setAbout(const QString &about);
void setSkipLevelUpdate(bool value);
void updateState(const Data::GroupCallParticipant &participant);
void updateStateInvited(bool calling);
void updateStateWithAccess();
void updateLevel(float level);
void updateBlobAnimation(crl::time now);
void clearRaisedHandStatus();
[[nodiscard]] State state() const {
return _state;
}
[[nodiscard]] bool sounding() const {
return _sounding;
}
[[nodiscard]] bool speaking() const {
return _speaking;
}
[[nodiscard]] bool mutedByMe() const {
return _mutedByMe;
}
[[nodiscard]] crl::time speakingLastTime() const {
return _speakingLastTime;
}
[[nodiscard]] int volume() const {
return _volume;
}
[[nodiscard]] uint64 raisedHandRating() const {
return _raisedHandRating;
}
void refreshName(const style::PeerListItem &st) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
QSize rightActionSize() const override;
bool rightActionDisabled() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void paintComplexUserpic(
Painter &p,
int x,
int y,
int outerWidth,
int sizew,
int sizeh,
PanelMode mode,
bool selected = false);
void paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) override;
void paintComplexStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected,
MembersRowStyle style);
void paintMuteIcon(
QPainter &p,
QRect iconRect,
MembersRowStyle style = MembersRowStyle::Default);
[[nodiscard]] MembersRowDelegate::IconState computeIconState(
MembersRowStyle style = MembersRowStyle::Default) const;
void showContextMenu();
private:
struct BlobsAnimation;
struct StatusIcon;
int statusIconWidth(bool skipIcon) const;
int statusIconHeight() const;
void paintStatusIcon(
Painter &p,
int x,
int y,
const style::PeerListItem &st,
const style::font &font,
bool selected,
bool skipIcon);
void refreshStatus() override;
void setSounding(bool sounding);
void setSpeaking(bool speaking);
void setState(State state);
void setVolume(int volume);
void ensureUserpicCache(
Ui::PeerUserpicView &view,
int size);
void paintBlobs(
Painter &p,
int x,
int y,
int sizew,
int sizeh, PanelMode mode);
void paintScaledUserpic(
Painter &p,
Ui::PeerUserpicView &userpic,
int x,
int y,
int outerWidth,
int sizew,
int sizeh,
PanelMode mode);
const not_null<MembersRowDelegate*> _delegate;
State _state = State::Inactive;
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
std::unique_ptr<BlobsAnimation> _blobsAnimation;
std::unique_ptr<StatusIcon> _statusIcon;
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
Ui::Text::String _about;
crl::time _speakingLastTime = 0;
uint64 _raisedHandRating = 0;
int _volume = Group::kDefaultVolume;
bool _sounding : 1 = false;
bool _speaking : 1 = false;
bool _raisedHandStatus : 1 = false;
bool _skipLevelUpdate : 1 = false;
bool _mutedByMe : 1 = false;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,645 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_menu.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_settings.h"
#include "calls/group/calls_group_panel.h"
#include "calls/group/ui/calls_group_recording_box.h"
#include "data/data_peer.h"
#include "data/data_group_call.h"
#include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/menu/menu.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "base/timer_rpl.h"
#include "styles/style_calls.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Calls::Group {
namespace {
class JoinAsAction final : public Ui::Menu::ItemBase {
public:
JoinAsAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
not_null<PeerData*> peer,
Fn<void()> callback);
bool isEnabled() const override;
not_null<QAction*> action() const override;
void handleKeyPress(not_null<QKeyEvent*> e) override;
protected:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
int contentHeight() const override;
private:
void prepare();
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
const not_null<PeerData*> _peer;
Ui::PeerUserpicView _userpicView;
Ui::Text::String _text;
Ui::Text::String _name;
int _textWidth = 0;
int _nameWidth = 0;
const int _height = 0;
};
class RecordingAction final : public Ui::Menu::ItemBase {
public:
RecordingAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
rpl::producer<QString> text,
rpl::producer<TimeId> startAtValues,
Fn<void()> callback);
bool isEnabled() const override;
not_null<QAction*> action() const override;
void handleKeyPress(not_null<QKeyEvent*> e) override;
protected:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
int contentHeight() const override;
private:
void prepare(rpl::producer<QString> text);
void refreshElapsedText();
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
TimeId _startAt = 0;
crl::time _startedAt = 0;
base::Timer _refreshTimer;
Ui::Text::String _text;
int _textWidth = 0;
QString _elapsedText;
const int _smallHeight = 0;
const int _bigHeight = 0;
};
TextParseOptions MenuTextOptions = {
TextParseLinks, // flags
0, // maxw
0, // maxh
Qt::LayoutDirectionAuto, // dir
};
JoinAsAction::JoinAsAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
not_null<PeerData*> peer,
Fn<void()> callback)
: ItemBase(parent, st)
, _dummyAction(new QAction(parent))
, _st(st)
, _peer(peer)
, _height(st::groupCallJoinAsPadding.top()
+ st::groupCallJoinAsPhotoSize
+ st::groupCallJoinAsPadding.bottom()) {
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setClickedCallback(std::move(callback));
paintRequest(
) | rpl::on_next([=] {
Painter p(this);
paint(p);
}, lifetime());
enableMouseSelecting();
prepare();
}
void JoinAsAction::paint(Painter &p) {
const auto selected = isSelected();
const auto height = contentHeight();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), height, _st.itemBg);
}
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
if (isEnabled()) {
paintRipple(p, 0, 0);
}
const auto &padding = st::groupCallJoinAsPadding;
_peer->paintUserpic(
p,
_userpicView,
padding.left(),
padding.top(),
st::groupCallJoinAsPhotoSize);
const auto textLeft = padding.left()
+ st::groupCallJoinAsPhotoSize
+ padding.left();
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_text.drawLeftElided(
p,
textLeft,
st::groupCallJoinAsTextTop,
_textWidth,
width());
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
_name.drawLeftElided(
p,
textLeft,
st::groupCallJoinAsNameTop,
_nameWidth,
width());
}
void JoinAsAction::prepare() {
rpl::combine(
tr::lng_group_call_display_as_header(),
Info::Profile::NameValue(_peer)
) | rpl::on_next([=](QString text, QString name) {
const auto &padding = st::groupCallJoinAsPadding;
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
_name.setMarkedText(_st.itemStyle, { name }, MenuTextOptions);
const auto textWidth = _text.maxWidth();
const auto nameWidth = _name.maxWidth();
const auto textLeft = padding.left()
+ st::groupCallJoinAsPhotoSize
+ padding.left();
const auto w = std::clamp(
(textLeft
+ std::max(textWidth, nameWidth)
+ padding.right()),
_st.widthMin,
_st.widthMax);
setMinWidth(w);
_textWidth = w - textLeft - padding.right();
_nameWidth = w - textLeft - padding.right();
update();
}, lifetime());
}
bool JoinAsAction::isEnabled() const {
return true;
}
not_null<QAction*> JoinAsAction::action() const {
return _dummyAction;
}
QPoint JoinAsAction::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
QImage JoinAsAction::prepareRippleMask() const {
return Ui::RippleAnimation::RectMask(size());
}
int JoinAsAction::contentHeight() const {
return _height;
}
void JoinAsAction::handleKeyPress(not_null<QKeyEvent*> e) {
if (!isSelected()) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
setClicked(Ui::Menu::TriggeredSource::Keyboard);
}
}
RecordingAction::RecordingAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
rpl::producer<QString> text,
rpl::producer<TimeId> startAtValues,
Fn<void()> callback)
: ItemBase(parent, st)
, _dummyAction(new QAction(parent))
, _st(st)
, _refreshTimer([=] { refreshElapsedText(); })
, _smallHeight(st.itemPadding.top()
+ _st.itemStyle.font->height
+ st.itemPadding.bottom())
, _bigHeight(st::groupCallRecordingTimerPadding.top()
+ _st.itemStyle.font->height
+ st::groupCallRecordingTimerFont->height
+ st::groupCallRecordingTimerPadding.bottom()) {
std::move(
startAtValues
) | rpl::on_next([=](TimeId startAt) {
_startAt = startAt;
_startedAt = crl::now();
_refreshTimer.cancel();
refreshElapsedText();
resize(width(), contentHeight());
}, lifetime());
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setClickedCallback(std::move(callback));
paintRequest(
) | rpl::on_next([=] {
Painter p(this);
paint(p);
}, lifetime());
enableMouseSelecting();
prepare(std::move(text));
}
void RecordingAction::paint(Painter &p) {
const auto selected = isSelected();
const auto height = contentHeight();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), height, _st.itemBg);
}
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
if (isEnabled()) {
paintRipple(p, 0, 0);
}
const auto smallTop = st::groupCallRecordingTimerPadding.top();
const auto textTop = _startAt ? smallTop : _st.itemPadding.top();
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_text.drawLeftElided(
p,
_st.itemPadding.left(),
textTop,
_textWidth,
width());
if (_startAt) {
p.setFont(st::groupCallRecordingTimerFont);
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
p.drawTextLeft(
_st.itemPadding.left(),
smallTop + _st.itemStyle.font->height,
width(),
_elapsedText);
}
}
void RecordingAction::refreshElapsedText() {
const auto now = base::unixtime::now();
const auto elapsed = std::max(now - _startAt, 0);
const auto text = !_startAt
? QString()
: (elapsed >= 3600)
? QString("%1:%2:%3"
).arg(elapsed / 3600
).arg((elapsed % 3600) / 60, 2, 10, QChar('0')
).arg(elapsed % 60, 2, 10, QChar('0'))
: QString("%1:%2"
).arg(elapsed / 60
).arg(elapsed % 60, 2, 10, QChar('0'));
if (_elapsedText != text) {
_elapsedText = text;
update();
}
const auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500);
_refreshTimer.callOnce(nextCall);
}
void RecordingAction::prepare(rpl::producer<QString> text) {
refreshElapsedText();
const auto &padding = _st.itemPadding;
const auto textWidth1 = _st.itemStyle.font->width(
tr::lng_group_call_recording_start(tr::now));
const auto textWidth2 = _st.itemStyle.font->width(
tr::lng_group_call_recording_stop(tr::now));
const auto maxWidth = st::groupCallRecordingTimerFont->width("23:59:59");
const auto w = std::clamp(
(padding.left()
+ std::max({ textWidth1, textWidth2, maxWidth })
+ padding.right()),
_st.widthMin,
_st.widthMax);
setMinWidth(w);
std::move(text) | rpl::on_next([=](QString text) {
const auto &padding = _st.itemPadding;
_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);
_textWidth = w - padding.left() - padding.right();
update();
}, lifetime());
}
bool RecordingAction::isEnabled() const {
return true;
}
not_null<QAction*> RecordingAction::action() const {
return _dummyAction;
}
QPoint RecordingAction::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
QImage RecordingAction::prepareRippleMask() const {
return Ui::RippleAnimation::RectMask(size());
}
int RecordingAction::contentHeight() const {
return _startAt ? _bigHeight : _smallHeight;
}
void RecordingAction::handleKeyPress(not_null<QKeyEvent*> e) {
if (!isSelected()) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
setClicked(Ui::Menu::TriggeredSource::Keyboard);
}
}
base::unique_qptr<Ui::Menu::ItemBase> MakeJoinAsAction(
not_null<Ui::Menu::Menu*> menu,
not_null<PeerData*> peer,
Fn<void()> callback) {
return base::make_unique_q<JoinAsAction>(
menu,
menu->st(),
peer,
std::move(callback));
}
base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
not_null<Ui::Menu::Menu*> menu,
rpl::producer<TimeId> startDate,
Fn<void()> callback) {
using namespace rpl::mappers;
return base::make_unique_q<RecordingAction>(
menu,
menu->st(),
rpl::conditional(
rpl::duplicate(startDate) | rpl::map(!!_1),
tr::lng_group_call_recording_stop(),
tr::lng_group_call_recording_start()),
rpl::duplicate(startDate),
std::move(callback));
}
} // namespace
void LeaveBox(
not_null<Ui::GenericBox*> box,
not_null<GroupCall*> call,
bool discardChecked,
BoxContext context) {
const auto conference = call->conference();
const auto livestream = call->peer()->isBroadcast();
const auto scheduled = (call->scheduleDate() != 0);
if (!scheduled) {
box->setTitle(conference
? tr::lng_group_call_leave_title_call()
: livestream
? tr::lng_group_call_leave_title_channel()
: tr::lng_group_call_leave_title());
}
const auto inCall = (context == BoxContext::GroupCallPanel);
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
(scheduled
? (livestream
? tr::lng_group_call_close_sure_channel()
: tr::lng_group_call_close_sure())
: (conference
? tr::lng_group_call_leave_sure_call()
: livestream
? tr::lng_group_call_leave_sure_channel()
: tr::lng_group_call_leave_sure())),
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
scheduled ? st::boxPadding : st::boxRowPadding);
const auto discard = call->canManage()
? box->addRow(object_ptr<Ui::Checkbox>(
box.get(),
(scheduled
? (livestream
? tr::lng_group_call_also_cancel_channel()
: tr::lng_group_call_also_cancel())
: (livestream
? tr::lng_group_call_also_end_channel()
: tr::lng_group_call_also_end())),
discardChecked,
(inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),
(inCall ? st::groupCallCheck : st::defaultCheck)),
style::margins(
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::boxRowPadding.bottom()))
: nullptr;
const auto weak = base::make_weak(call);
auto label = scheduled
? tr::lng_group_call_close()
: tr::lng_group_call_leave();
box->addButton(std::move(label), [=] {
const auto discardCall = (discard && discard->checked());
box->closeBox();
if (!weak) {
return;
} else if (discardCall) {
call->discard();
} else {
call->hangup();
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
object_ptr<Ui::GenericBox> ConfirmBox(Ui::ConfirmBoxArgs &&args) {
auto copy = std::move(args);
copy.labelStyle = &st::groupCallBoxLabel;
return Ui::MakeConfirmBox(std::move(copy));
}
void FillMenu(
not_null<Ui::DropdownMenu*> menu,
not_null<PeerData*> peer,
not_null<GroupCall*> call,
bool wide,
Fn<void()> chooseJoinAs,
Fn<void()> chooseShareScreenSource,
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
const auto weak = base::make_weak(call);
const auto resolveReal = [=] {
if (const auto strong = weak.get()) {
if (const auto real = strong->lookupReal()) {
return real;
}
}
return (Data::GroupCall*)nullptr;
};
const auto real = resolveReal();
if (!real) {
return;
}
const auto conference = call->conference();
const auto addEditJoinAs = call->showChooseJoinAs();
const auto addEditTitle = !conference && call->canManage();
const auto addEditRecording = !conference
&& call->canManage()
&& !real->scheduleDate();
const auto addScreenCast = call->videoIsWorking()
&& !real->scheduleDate();
if (addEditJoinAs) {
menu->addAction(MakeJoinAsAction(
menu->menu(),
call->joinAs(),
chooseJoinAs));
menu->addSeparator();
}
if (addEditTitle) {
const auto livestream = call->peer()->isBroadcast();
const auto text = (livestream
? tr::lng_group_call_edit_title_channel
: tr::lng_group_call_edit_title)(tr::now);
menu->addAction(text, [=] {
const auto done = [=](const QString &title) {
if (const auto strong = weak.get()) {
strong->changeTitle(title);
}
};
if (const auto real = resolveReal()) {
showBox(Box(
EditGroupCallTitleBox,
peer->name(),
real->title(),
livestream,
done));
}
});
}
if (addEditRecording) {
const auto handler = [=] {
const auto real = resolveReal();
if (!real) {
return;
}
const auto type = std::make_shared<RecordingType>();
const auto recordStartDate = real->recordStartDate();
const auto done = [=](QString title) {
if (const auto strong = weak.get()) {
strong->toggleRecording(
!recordStartDate,
title,
(*type) != RecordingType::AudioOnly,
(*type) == RecordingType::VideoPortrait);
}
};
if (recordStartDate) {
showBox(Box(
StopGroupCallRecordingBox,
done));
} else {
const auto typeDone = [=](RecordingType newType) {
*type = newType;
showBox(Box(
AddTitleGroupCallRecordingBox,
real->title(),
done));
};
showBox(Box(StartGroupCallRecordingBox, typeDone));
}
};
menu->addAction(MakeRecordingAction(
menu->menu(),
real->recordStartDateValue(),
handler));
}
if (addScreenCast) {
const auto sharing = call->isSharingScreen();
const auto toggle = [=] {
if (const auto strong = weak.get()) {
if (sharing) {
strong->toggleScreenSharing(std::nullopt);
} else {
chooseShareScreenSource();
}
}
};
menu->addAction(
(call->isSharingScreen()
? tr::lng_group_call_screen_share_stop(tr::now)
: tr::lng_group_call_screen_share_start(tr::now)),
toggle);
}
menu->addAction(tr::lng_group_call_settings(tr::now), [=] {
if (const auto strong = weak.get()) {
showBox(Box(SettingsBox, strong));
}
});
const auto finish = [=] {
if (const auto strong = weak.get()) {
showBox(Box(
LeaveBox,
strong,
true,
BoxContext::GroupCallPanel));
}
};
const auto livestream = real->peer()->isBroadcast();
menu->addAction(MakeAttentionAction(
menu->menu(),
(!call->canManage()
? tr::lng_group_call_leave
: real->scheduleDate()
? (livestream
? tr::lng_group_call_cancel_channel
: tr::lng_group_call_cancel)
: (livestream
? tr::lng_group_call_end_channel
: tr::lng_group_call_end))(tr::now),
finish));
}
base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
not_null<Ui::Menu::Menu*> menu,
const QString &text,
Fn<void()> callback) {
return base::make_unique_q<Ui::Menu::Action>(
menu,
st::groupCallFinishMenu,
Ui::Menu::CreateAction(
menu,
text,
std::move(callback)),
nullptr,
nullptr);
}
} // namespace Calls::Group

View File

@@ -0,0 +1,63 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
#include "ui/boxes/confirm_box.h"
namespace style {
struct FlatLabel;
} // namespace style
namespace Ui {
class DropdownMenu;
class GenericBox;
class BoxContent;
} // namespace Ui
namespace Ui::Menu {
class ItemBase;
class Menu;
} // namespace Ui::Menu
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Calls::Group {
enum class BoxContext {
GroupCallPanel,
MainWindow,
};
void LeaveBox(
not_null<Ui::GenericBox*> box,
not_null<GroupCall*> call,
bool discardChecked,
BoxContext context);
[[nodiscard]] object_ptr<Ui::GenericBox> ConfirmBox(
Ui::ConfirmBoxArgs &&args);
void FillMenu(
not_null<Ui::DropdownMenu*> menu,
not_null<PeerData*> peer,
not_null<GroupCall*> call,
bool wide,
Fn<void()> chooseJoinAs,
Fn<void()> chooseShareScreenSource,
Fn<void(object_ptr<Ui::BoxContent>)> showBox);
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
not_null<Ui::Menu::Menu*> menu,
const QString &text,
Fn<void()> callback);
} // namespace Calls::Group

View File

@@ -0,0 +1,367 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_message_encryption.h"
#include <QtCore/QJsonValue>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
namespace Calls::Group {
namespace {
//[[nodiscard]] MTPJSONValue String(const QByteArray &value) {
// return MTP_jsonString(MTP_bytes(value));
//}
//
//[[nodiscard]] MTPJSONValue Int(int value) {
// return MTP_jsonNumber(MTP_double(value));
//}
//
//[[nodiscard]] MTPJSONObjectValue Value(
// const QByteArray &name,
// const MTPJSONValue &value) {
// return MTP_jsonObjectValue(MTP_bytes(name), value);
//}
//
//[[nodiscard]] MTPJSONValue Object(
// const QByteArray &cons,
// QVector<MTPJSONObjectValue> &&values) {
// values.insert(values.begin(), Value("_", String(cons)));
// return MTP_jsonObject(MTP_vector<MTPJSONObjectValue>(std::move(values)));
//}
//
//[[nodiscard]] MTPJSONValue Array(QVector<MTPJSONValue> &&values) {
// return MTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(values)));
//}
//
//template <typename MTPD>
//[[nodiscard]] MTPJSONValue SimpleEntity(
// const QByteArray &name,
// const MTPD &data) {
// return Object(name, {
// Value("offset", Int(data.voffset().v)),
// Value("length", Int(data.vlength().v)),
// });
//}
//
//[[nodiscard]] MTPJSONValue Entity(const MTPMessageEntity &entity) {
// return entity.match([](const MTPDmessageEntityBold &data) {
// return SimpleEntity("messageEntityBold", data);
// }, [](const MTPDmessageEntityItalic &data) {
// return SimpleEntity("messageEntityItalic", data);
// }, [](const MTPDmessageEntityUnderline &data) {
// return SimpleEntity("messageEntityUnderline", data);
// }, [](const MTPDmessageEntityStrike &data) {
// return SimpleEntity("messageEntityStrike", data);
// }, [](const MTPDmessageEntitySpoiler &data) {
// return SimpleEntity("messageEntitySpoiler", data);
// }, [](const MTPDmessageEntityCustomEmoji &data) {
// return Object("messageEntityCustomEmoji", {
// Value("offset", Int(data.voffset().v)),
// Value("length", Int(data.vlength().v)),
// Value(
// "document_id",
// String(QByteArray::number(int64(data.vdocument_id().v)))),
// });
// }, [](const auto &data) {
// return MTP_jsonNull();
// });
//}
//
//[[nodiscard]] QVector<MTPJSONValue> Entities(
// const QVector<MTPMessageEntity> &list) {
// auto result = QVector<MTPJSONValue>();
// result.reserve(list.size());
// for (const auto &entity : list) {
// if (const auto e = Entity(entity); e.type() != mtpc_jsonNull) {
// result.push_back(e);
// }
// }
// return result;
//}
//
//[[nodiscard]] QByteArray Serialize(const MTPJSONValue &value) {
// auto counter = ::tl::details::LengthCounter();
// value.write(counter);
// auto buffer = mtpBuffer();
// buffer.reserve(counter.length);
// value.write(buffer);
// return QByteArray(
// reinterpret_cast<const char*>(buffer.constData()),
// buffer.size() * sizeof(buffer.front()));
//}
[[nodiscard]] QJsonValue String(const QByteArray &value) {
return QJsonValue(QString::fromUtf8(value));
}
[[nodiscard]] QJsonValue Int(int value) {
return QJsonValue(double(value));
}
struct JsonObjectValue {
const char *name = nullptr;
QJsonValue value;
};
[[nodiscard]] JsonObjectValue Value(
const char *name,
const QJsonValue &value) {
return JsonObjectValue{ name, value };
}
[[nodiscard]] QJsonValue Object(
const char *cons,
QVector<JsonObjectValue> &&values) {
auto result = QJsonObject();
result.insert("_", cons);
for (const auto &value : values) {
result.insert(value.name, value.value);
}
return result;
}
[[nodiscard]] QJsonValue Array(QVector<QJsonValue> &&values) {
auto result = QJsonArray();
for (const auto &value : values) {
result.push_back(value);
}
return result;
}
template <typename MTPD>
[[nodiscard]] QJsonValue SimpleEntity(
const char *name,
const MTPD &data) {
return Object(name, {
Value("offset", Int(data.voffset().v)),
Value("length", Int(data.vlength().v)),
});
}
[[nodiscard]] QJsonValue Entity(const MTPMessageEntity &entity) {
return entity.match([](const MTPDmessageEntityBold &data) {
return SimpleEntity("messageEntityBold", data);
}, [](const MTPDmessageEntityItalic &data) {
return SimpleEntity("messageEntityItalic", data);
}, [](const MTPDmessageEntityUnderline &data) {
return SimpleEntity("messageEntityUnderline", data);
}, [](const MTPDmessageEntityStrike &data) {
return SimpleEntity("messageEntityStrike", data);
}, [](const MTPDmessageEntitySpoiler &data) {
return SimpleEntity("messageEntitySpoiler", data);
}, [](const MTPDmessageEntityCustomEmoji &data) {
return Object("messageEntityCustomEmoji", {
Value("offset", Int(data.voffset().v)),
Value("length", Int(data.vlength().v)),
Value(
"document_id",
String(QByteArray::number(int64(data.vdocument_id().v)))),
});
}, [](const auto &data) {
return QJsonValue(QJsonValue::Null);
});
}
[[nodiscard]] QVector<QJsonValue> Entities(
const QVector<MTPMessageEntity> &list) {
auto result = QVector<QJsonValue>();
result.reserve(list.size());
for (const auto &entity : list) {
if (const auto e = Entity(entity); !e.isNull()) {
result.push_back(e);
}
}
return result;
}
[[nodiscard]] QByteArray Serialize(const QJsonValue &value) {
return QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact);
}
[[nodiscard]] std::optional<QJsonValue> GetValue(
const QJsonObject &object,
const char *name) {
const auto i = object.find(name);
return (i != object.end()) ? *i : std::optional<QJsonValue>();
}
[[nodiscard]] std::optional<int> GetInt(
const QJsonObject &object,
const char *name) {
if (const auto maybeValue = GetValue(object, name)) {
if (maybeValue->isDouble()) {
return int(base::SafeRound(maybeValue->toDouble()));
} else if (maybeValue->isString()) {
auto ok = false;
const auto result = maybeValue->toString().toInt(&ok);
return ok ? result : std::optional<int>();
}
}
return {};
}
[[nodiscard]] std::optional<uint64> GetLong(
const QJsonObject &object,
const char *name) {
if (const auto maybeValue = GetValue(object, name)) {
if (maybeValue->isDouble()) {
const auto value = maybeValue->toDouble();
return (value >= 0.)
? uint64(base::SafeRound(value))
: std::optional<uint64>();
} else if (maybeValue->isString()) {
auto ok = false;
const auto result = maybeValue->toString().toLongLong(&ok);
return ok ? uint64(result) : std::optional<uint64>();
}
}
return {};
}
[[nodiscard]] std::optional<QString> GetString(
const QJsonObject &object,
const char *name) {
const auto maybeValue = GetValue(object, name);
return (maybeValue && maybeValue->isString())
? maybeValue->toString()
: std::optional<QString>();
}
[[nodiscard]] std::optional<QString> GetCons(const QJsonObject &object) {
return GetString(object, "_");
}
[[nodiscard]] bool Unsupported(
const QJsonObject &object,
const QString &cons = QString()) {
const auto maybeMinLayer = GetInt(object, "_min_layer");
const auto layer = int(MTP::details::kCurrentLayer);
if (maybeMinLayer.value_or(layer) > layer) {
LOG(("E2E Error: _min_layer too large: %1 > %2").arg(*maybeMinLayer).arg(layer));
return true;
} else if (!cons.isEmpty() && GetCons(object) != cons) {
LOG(("E2E Error: Expected %1 here.").arg(cons));
return true;
}
return false;
}
[[nodiscard]] std::optional<MTPMessageEntity> GetEntity(
const QString &text,
const QJsonObject &object) {
const auto cons = GetCons(object).value_or(QString());
const auto offset = GetInt(object, "offset").value_or(-1);
const auto length = GetInt(object, "length").value_or(0);
if (Unsupported(object)
|| (offset < 0)
|| (length <= 0)
|| (offset >= text.size())
|| (length > text.size())
|| (offset + length > text.size())) {
return {};
}
const auto simple = [&](const auto &make) {
return make(MTP_int(offset), MTP_int(length));
};
if (cons == "messageEntityBold") {
return simple(MTP_messageEntityBold);
} else if (cons == "messageEntityItalic") {
return simple(MTP_messageEntityItalic);
} else if (cons == "messageEntityUnderline") {
return simple(MTP_messageEntityUnderline);
} else if (cons == "messageEntityStrike") {
return simple(MTP_messageEntityStrike);
} else if (cons == "messageEntitySpoiler") {
return simple(MTP_messageEntitySpoiler);
} else if (cons == "messageEntityCustomEmoji") {
const auto maybeDocumentId = GetLong(object, "document_id");
if (const auto documentId = maybeDocumentId.value_or(0)) {
return MTP_messageEntityCustomEmoji(
MTP_int(offset),
MTP_int(length),
MTP_long(documentId));
}
}
return {};
}
[[nodiscard]] QVector<MTPMessageEntity> GetEntities(
const QString &text,
const QJsonArray &list) {
auto result = QVector<MTPMessageEntity>();
result.reserve(list.size());
for (const auto &entry : list) {
if (const auto entity = GetEntity(text, entry.toObject())) {
result.push_back(*entity);
}
}
return result;
}
} // namespace
QByteArray SerializeMessage(const PreparedMessage &data) {
return Serialize(Object("groupCallMessage", {
Value(
"random_id",
String(QByteArray::number(int64(data.randomId)))),
Value(
"message",
Object("textWithEntities", {
Value("text", String(data.message.data().vtext().v)),
Value(
"entities",
Array(Entities(data.message.data().ventities().v))),
})),
}));
}
std::optional<PreparedMessage> DeserializeMessage(
const QByteArray &data) {
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError
|| !document.isObject()) {
LOG(("E2E Error: Bad json in Calls::Group::DeserializeMessage."));
return {};
}
const auto groupCallMessage = document.object();
if (Unsupported(groupCallMessage, "groupCallMessage")) {
return {};
}
const auto randomId = GetLong(groupCallMessage, "random_id").value_or(0);
if (!randomId) {
return {};
}
const auto message = groupCallMessage["message"].toObject();
if (Unsupported(message, "textWithEntities")) {
return {};
}
const auto maybeText = GetString(message, "text");
if (!maybeText) {
return {};
}
const auto &text = *maybeText;
const auto maybeEntities = GetValue(message, "entities");
if (!maybeEntities || !maybeEntities->isArray()) {
return {};
}
const auto entities = GetEntities(text, maybeEntities->toArray());
return PreparedMessage{
.randomId = randomId,
.message = MTP_textWithEntities(
MTP_string(text),
MTP_vector<MTPMessageEntity>(entities)),
};
}
} // namespace Calls::Group

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Calls::Group {
struct PreparedMessage {
uint64 randomId = 0;
MTPTextWithEntities message;
};
[[nodiscard]] QByteArray SerializeMessage(const PreparedMessage &data);
[[nodiscard]] std::optional<PreparedMessage> DeserializeMessage(
const QByteArray &data);
} // namespace Calls::Group

View File

@@ -0,0 +1,662 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_message_field.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h"
#include "calls/group/calls_group_messages.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/send_button.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/scroll_area.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/userpic_view.h"
#include "styles/style_calls.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.h"
#include "styles/style_media_view.h"
namespace Calls::Group {
namespace {
constexpr auto kErrorLimit = 99;
using Chosen = HistoryView::Reactions::ChosenReaction;
} // namespace
class ReactionPanel final {
public:
ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry);
~ReactionPanel();
[[nodiscard]] rpl::producer<Chosen> chosen() const;
void show();
void hide();
void raise();
void hideIfCollapsed();
void collapse();
private:
struct Hiding;
void create();
void updateShowState();
void fadeOutSelector();
void startAnimation();
const not_null<QWidget*> _outer;
const std::shared_ptr<ChatHelpers::Show> _show;
std::unique_ptr<Ui::RpWidget> _parent;
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
std::vector<std::unique_ptr<Hiding>> _hiding;
rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing;
rpl::variable<float64> _shownValue;
rpl::variable<QRect> _fieldGeometry;
rpl::variable<bool> _expanded;
rpl::variable<bool> _shown = false;
};
struct ReactionPanel::Hiding {
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
}
Ui::RpWidget widget;
Ui::Animations::Simple animation;
QImage frame;
};
ReactionPanel::ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry)
: _outer(outer)
, _show(std::move(show))
, _fieldGeometry(std::move(fieldGeometry)) {
}
ReactionPanel::~ReactionPanel() = default;
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
return _chosen.events();
}
void ReactionPanel::show() {
if (_shown.current()) {
return;
}
create();
if (!_selector) {
return;
}
const auto duration = st::defaultPanelAnimation.heightDuration
* st::defaultPopupMenu.showDuration;
_shown = true;
_showing.start([=] { updateShowState(); }, 0., 1., duration);
updateShowState();
_parent->show();
}
void ReactionPanel::hide() {
if (!_selector) {
return;
}
_selector->beforeDestroy();
if (!anim::Disabled()) {
fadeOutSelector();
}
_shown = false;
_expanded = false;
_showing.stop();
_selector = nullptr;
_parent = nullptr;
}
void ReactionPanel::raise() {
if (_parent) {
_parent->raise();
}
}
void ReactionPanel::hideIfCollapsed() {
if (!_expanded.current()) {
hide();
}
}
void ReactionPanel::collapse() {
if (_expanded.current()) {
hide();
show();
}
}
void ReactionPanel::create() {
auto reactions = Data::LookupPossibleReactions(&_show->session());
if (reactions.recent.empty()) {
return;
}
_parent = std::make_unique<Ui::RpWidget>(_outer);
_parent->show();
_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(event->pos())) {
collapse();
}
}
}
}, _parent->lifetime());
_selector = std::make_unique<HistoryView::Reactions::Selector>(
_parent.get(),
st::storiesReactionsPan,
_show,
std::move(reactions),
TextWithEntities(),
[=](bool fast) { hide(); },
nullptr, // iconFactory
nullptr, // paused
true);
_selector->chosen(
) | rpl::on_next([=](Chosen reaction) {
if (reaction.id.custom() && !_show->session().premium()) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else {
_chosen.fire(std::move(reaction));
hide();
}
}, _selector->lifetime());
const auto desiredWidth = st::storiesReactionsWidth;
const auto maxWidth = desiredWidth * 2;
const auto width = _selector->countWidth(desiredWidth, maxWidth);
const auto margins = _selector->marginsForShadow();
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
width);
const auto full = margins.left() + width + margins.right();
_shownValue = 0.;
rpl::combine(
_fieldGeometry.value(),
_shownValue.value(),
_expanded.value()
) | rpl::on_next([=](QRect field, float64 shown, bool expanded) {
const auto width = margins.left()
+ _selector->countAppearedWidth(shown)
+ margins.right();
const auto available = field.y();
const auto min = st::storiesReactionsBottomSkip
+ st::reactStripHeight;
const auto max = min
+ margins.top()
+ categoriesTop
+ st::storiesReactionsAddedTop;
const auto height = expanded ? std::min(available, max) : min;
const auto top = field.y() - height;
const auto shift = (width / 2);
const auto right = (field.x() + field.width() / 2 + shift);
_parent->setGeometry(QRect((right - width), top, full, height));
const auto innerTop = height
- st::storiesReactionsBottomSkip
- st::reactStripHeight;
const auto maxAdded = innerTop - margins.top() - categoriesTop;
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
_selector->setSpecialExpandTopSkip(added);
_selector->initGeometry(innerTop);
}, _selector->lifetime());
_selector->willExpand(
) | rpl::on_next([=] {
_expanded = true;
const auto raw = _parent.get();
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(
_parent->mapFromGlobal(event->globalPos()))) {
collapse();
}
}
}
return base::EventFilterResult::Continue;
});
}, _selector->lifetime());
_selector->escapes() | rpl::on_next([=] {
collapse();
}, _selector->lifetime());
}
void ReactionPanel::fadeOutSelector() {
const auto geometry = Ui::MapFrom(
_outer,
_parent.get(),
_selector->geometry());
_hiding.push_back(std::make_unique<Hiding>(_outer));
const auto raw = _hiding.back().get();
raw->frame = Ui::GrabWidgetToImage(_selector.get());
raw->widget.setGeometry(geometry);
raw->widget.show();
raw->widget.paintRequest(
) | rpl::on_next([=] {
if (const auto opacity = raw->animation.value(0.)) {
auto p = QPainter(&raw->widget);
p.setOpacity(opacity);
p.drawImage(0, 0, raw->frame);
}
}, raw->widget.lifetime());
Ui::PostponeCall(&raw->widget, [=] {
raw->animation.start([=] {
if (raw->animation.animating()) {
raw->widget.update();
} else {
const auto i = ranges::find(
_hiding,
raw,
&std::unique_ptr<Hiding>::get);
if (i != end(_hiding)) {
_hiding.erase(i);
}
}
}, 1., 0., st::slideWrapDuration);
});
}
void ReactionPanel::updateShowState() {
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
const auto opacity = 1.;
const auto appearing = _showing.animating();
const auto toggling = false;
_shownValue = progress;
_selector->updateShowState(progress, opacity, appearing, toggling);
}
MessageField::MessageField(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
PeerData *peer)
: _parent(parent)
, _show(std::move(show))
, _wrap(std::make_unique<Ui::RpWidget>(_parent))
, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {
createControls(peer);
}
MessageField::~MessageField() = default;
void MessageField::createControls(PeerData *peer) {
setupBackground();
const auto &st = st::storiesComposeControls;
_field = Ui::CreateChild<Ui::InputField>(
_wrap.get(),
st.field,
Ui::InputField::Mode::MultiLine,
tr::lng_message_ph());
_field->setMaxLength(_limit + kErrorLimit);
_field->setMinHeight(
st::historySendSize.height() - 2 * st::historySendPadding);
_field->setMaxHeight(st::historyComposeFieldMaxHeight);
_field->setDocumentMargin(4.);
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
_reactionPanel = std::make_unique<ReactionPanel>(
_parent,
_show,
_wrap->geometryValue());
_fieldFocused = _field->focusedChanges();
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
return field->getLastText().trimmed().isEmpty();
});
rpl::combine(
_fieldFocused.value(),
_fieldEmpty.value()
) | rpl::on_next([=](bool focused, bool empty) {
if (!focused) {
_reactionPanel->hideIfCollapsed();
} else if (empty) {
_reactionPanel->show();
} else {
_reactionPanel->hide();
}
}, _field->lifetime());
_reactionPanel->chosen(
) | rpl::on_next([=](Chosen reaction) {
if (const auto customId = reaction.id.custom()) {
const auto document = _show->session().data().document(customId);
if (const auto sticker = document->sticker()) {
if (const auto alt = sticker->alt; !alt.isEmpty()) {
const auto length = int(alt.size());
const auto data = Data::SerializeCustomEmojiId(customId);
const auto tag = Ui::InputField::CustomEmojiLink(data);
_submitted.fire({ alt, { { 0, length, tag } } });
}
}
} else {
_submitted.fire({ reaction.id.emoji() });
}
_reactionPanel->hide();
}, _field->lifetime());
const auto show = _show;
const auto allow = [=](not_null<DocumentData*> emoji) {
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
return true;
}
return false;
};
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = _field,
.customEmojiPaused = [=] {
return show->paused(ChatHelpers::PauseReason::Layer);
},
.allowPremiumEmoji = allow,
.fieldStyle = &st.files.caption,
.allowMarkdownTags = {
Ui::InputField::kTagBold,
Ui::InputField::kTagItalic,
Ui::InputField::kTagUnderline,
Ui::InputField::kTagStrikeOut,
Ui::InputField::kTagSpoiler,
},
});
Ui::Emoji::SuggestionsController::Init(
_parent,
_field,
&_show->session(),
{
.suggestCustomEmoji = true,
.allowCustomWithoutPremium = allow,
.st = &st.suggestions,
});
_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);
_send->show();
using Selector = ChatHelpers::TabbedSelector;
_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(
_parent,
ChatHelpers::TabbedPanelDescriptor{
.ownedSelector = object_ptr<Selector>(
nullptr,
ChatHelpers::TabbedSelectorDescriptor{
.show = _show,
.st = st.tabbed,
.level = ChatHelpers::PauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
.features = {
.stickersSettings = false,
.openStickerSets = false,
},
}),
});
const auto panel = _emojiPanel.get();
panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
panel->hide();
panel->selector()->setCurrentPeer(peer);
panel->selector()->emojiChosen(
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
}, lifetime());
panel->selector()->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto info = data.document->sticker();
if (info
&& info->setType == Data::StickersType::Emoji
&& !_show->session().premium()) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else {
Data::InsertCustomEmoji(_field, data.document);
}
}, lifetime());
_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);
_emojiToggle->show();
_emojiToggle->installEventFilter(panel);
_emojiToggle->addClickHandler([=] {
panel->toggleAnimated();
});
_width.value(
) | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::on_next([=](int newWidth) {
const auto fieldWidth = newWidth
- st::historySendPadding
- _emojiToggle->width()
- _send->width();
_field->resizeToWidth(fieldWidth);
_field->moveToLeft(
st::historySendPadding,
st::historySendPadding,
newWidth);
updateWrapSize(newWidth);
}, _lifetime);
rpl::combine(
_width.value(),
_field->heightValue()
) | rpl::on_next([=](int width, int height) {
if (width <= 0) {
return;
}
const auto minHeight = st::historySendSize.height()
- 2 * st::historySendPadding;
_send->moveToRight(0, height - minHeight, width);
_emojiToggle->moveToRight(_send->width(), height - minHeight, width);
updateWrapSize();
}, _lifetime);
_field->cancelled() | rpl::on_next([=] {
_closeRequests.fire({});
}, _lifetime);
const auto updateLimitPosition = [=](QSize parent, QSize label) {
const auto skip = st::historySendPadding;
return QPoint(parent.width() - label.width() - skip, skip);
};
Ui::AddLengthLimitLabel(_field, _limit, {
.customParent = _wrap.get(),
.customUpdatePosition = updateLimitPosition,
});
rpl::merge(
_field->submits() | rpl::to_empty,
_send->clicks() | rpl::to_empty
) | rpl::on_next([=] {
auto text = _field->getTextWithTags();
if (text.text.size() <= _limit) {
_submitted.fire(std::move(text));
}
}, _lifetime);
}
void MessageField::updateEmojiPanelGeometry() {
const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
const auto local = _parent->mapFromGlobal(global);
_emojiPanel->moveBottomRight(
local.y(),
local.x() + _emojiToggle->width() * 3);
}
void MessageField::setupBackground() {
_wrap->paintRequest() | rpl::on_next([=] {
const auto radius = st::historySendSize.height() / 2.;
auto p = QPainter(_wrap.get());
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::storiesComposeBg);
p.drawRoundedRect(_wrap->rect(), radius, radius);
}, _lifetime);
}
void MessageField::resizeToWidth(int newWidth) {
_width = newWidth;
if (_wrap->isHidden()) {
Ui::SendPendingMoveResizeEvents(_wrap.get());
}
updateEmojiPanelGeometry();
}
void MessageField::move(int x, int y) {
_wrap->move(x, y);
if (_cache) {
_cache->move(x, y);
}
}
void MessageField::toggle(bool shown) {
if (_shown == shown) {
return;
} else if (shown) {
Assert(_width.current() > 0);
Ui::SendPendingMoveResizeEvents(_wrap.get());
} else if (Ui::InFocusChain(_field)) {
_parent->setFocus();
}
_shown = shown;
if (!anim::Disabled()) {
if (!_cache) {
auto image = Ui::GrabWidgetToImage(_wrap.get());
_cache = std::make_unique<Ui::RpWidget>(_parent);
const auto raw = _cache.get();
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto scale = raw->height() / float64(_wrap->height());
const auto target = _wrap->rect();
const auto center = target.center();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawImage(target, image);
}, raw->lifetime());
raw->show();
raw->move(_wrap->pos());
raw->resize(_wrap->width(), 0);
_wrap->hide();
}
_shownAnimation.start(
[=] { shownAnimationCallback(); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::slideWrapDuration,
anim::easeOutCirc);
}
shownAnimationCallback();
}
void MessageField::raise() {
_wrap->raise();
if (_cache) {
_cache->raise();
}
if (_reactionPanel) {
_reactionPanel->raise();
}
if (_emojiPanel) {
_emojiPanel->raise();
}
}
void MessageField::updateWrapSize(int widthOverride) {
const auto width = widthOverride ? widthOverride : _wrap->width();
const auto height = _field->height() + 2 * st::historySendPadding;
_wrap->resize(width, height);
updateHeight();
}
void MessageField::updateHeight() {
_height = int(base::SafeRound(
_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));
}
void MessageField::shownAnimationCallback() {
updateHeight();
if (_shownAnimation.animating()) {
Assert(_cache != nullptr);
_cache->resize(_cache->width(), _height.current());
_cache->update();
} else if (_shown) {
_cache = nullptr;
_wrap->show();
_field->setFocusFast();
} else {
_closed.fire({});
}
}
int MessageField::height() const {
return _height.current();
}
rpl::producer<int> MessageField::heightValue() const {
return _height.value();
}
rpl::producer<TextWithTags> MessageField::submitted() const {
return _submitted.events();
}
rpl::producer<> MessageField::closeRequests() const {
return _closeRequests.events();
}
rpl::producer<> MessageField::closed() const {
return _closed.events();
}
rpl::lifetime &MessageField::lifetime() {
return _lifetime;
}
} // namespace Calls::Group

View File

@@ -0,0 +1,88 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
struct TextWithTags;
namespace ChatHelpers {
class Show;
class TabbedPanel;
} // namespace ChatHelpers
namespace Ui {
class EmojiButton;
class InputField;
class SendButton;
class RpWidget;
} // namespace Ui
namespace Calls::Group {
class ReactionPanel;
class MessageField final {
public:
MessageField(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
PeerData *peer);
~MessageField();
void resizeToWidth(int newWidth);
void move(int x, int y);
void toggle(bool shown);
void raise();
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<int> heightValue() const;
[[nodiscard]] rpl::producer<TextWithTags> submitted() const;
[[nodiscard]] rpl::producer<> closeRequests() const;
[[nodiscard]] rpl::producer<> closed() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
void createControls(PeerData *peer);
void setupBackground();
void shownAnimationCallback();
void updateEmojiPanelGeometry();
void updateWrapSize(int widthOverride = 0);
void updateHeight();
const not_null<QWidget*> _parent;
const std::shared_ptr<ChatHelpers::Show> _show;
const std::unique_ptr<Ui::RpWidget> _wrap;
int _limit = 0;
Ui::InputField *_field = nullptr;
Ui::SendButton *_send = nullptr;
Ui::EmojiButton *_emojiToggle = nullptr;
std::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;
std::unique_ptr<ReactionPanel> _reactionPanel;
rpl::variable<bool> _fieldFocused;
rpl::variable<bool> _fieldEmpty = true;
rpl::variable<int> _width;
rpl::variable<int> _height;
bool _shown = false;
Ui::Animations::Simple _shownAnimation;
std::unique_ptr<Ui::RpWidget> _cache;
rpl::event_stream<TextWithTags> _submitted;
rpl::event_stream<> _closeRequests;
rpl::event_stream<> _closed;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,731 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_messages.h"
#include "apiwrap.h"
#include "api/api_blocked_peers.h"
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_message_encryption.h"
#include "data/data_channel.h"
#include "data/data_group_call.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "mtproto/sender.h"
#include "ui/text/text_utilities.h"
#include "ui/ui_utility.h"
namespace Calls::Group {
namespace {
constexpr auto kMaxShownVideoStreamMessages = 100;
constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);
[[nodiscard]] StarsTop ParseStarsTop(
not_null<Data::Session*> owner,
const MTPphone_GroupCallStars &stars) {
const auto &data = stars.data();
const auto &list = data.vtop_donors().v;
auto result = StarsTop{ .total = int(data.vtotal_stars().v) };
result.topDonors.reserve(list.size());
for (const auto &entry : list) {
const auto &fields = entry.data();
result.topDonors.push_back({
.peer = (fields.vpeer_id()
? owner->peer(peerFromMTP(*fields.vpeer_id())).get()
: nullptr),
.stars = int(fields.vstars().v),
.my = fields.is_my(),
});
}
return result;
}
[[nodiscard]] TimeId PinFinishDate(
not_null<PeerData*> peer,
TimeId date,
int stars) {
if (!date || !stars) {
return 0;
}
const auto &colorings = peer->session().appConfig().groupCallColorings();
return date + Ui::StarsColoringForCount(colorings, stars).secondsPin;
}
[[nodiscard]] TimeId PinFinishDate(const Message &message) {
return PinFinishDate(message.peer, message.date, message.stars);
}
} // namespace
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
: _call(call)
, _session(&call->peer()->session())
, _api(api)
, _destroyTimer([=] { checkDestroying(); })
, _ttl(_session->appConfig().groupCallMessageTTL())
, _starsStatsTimer([=] { requestStarsStats(); }) {
Ui::PostponeCall(_call, [=] {
_call->real(
) | rpl::on_next([=](not_null<Data::GroupCall*> call) {
_real = call;
if (ready()) {
sendPending();
} else {
Unexpected("Not ready call.");
}
}, _lifetime);
requestStarsStats();
});
}
Messages::~Messages() {
if (_paid.sending > 0) {
finishPaidSending({
.count = int(_paid.sending),
.valid = true,
.shownPeer = _paid.sendingShownPeer,
}, false);
}
}
void Messages::requestStarsStats() {
if (!_call->videoStream()) {
return;
}
_starsStatsTimer.cancel();
_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(
_call->inputCall()
)).done([=](const MTPphone_GroupCallStars &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
_paid.top = ParseStarsTop(owner, result);
_paidChanges.fire({});
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).fail([=](const MTP::Error &error) {
[[maybe_unused]] const auto &type = error.type();
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).send();
}
bool Messages::ready() const {
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
}
void Messages::send(TextWithTags text, int stars) {
if (text.empty() && !stars) {
return;
} else if (!ready()) {
_pending.push_back({ std::move(text), stars });
return;
}
auto prepared = TextWithEntities{
text.text,
TextUtilities::ConvertTextTagsToEntities(text.tags)
};
auto serialized = MTPTextWithEntities(MTP_textWithEntities(
MTP_string(prepared.text),
Api::EntitiesToMTP(
&_real->session(),
prepared.entities,
Api::ConvertOption::SkipLocal)));
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _call->messagesFrom();
const auto creator = _real->creator();
const auto skip = skipMessage(prepared, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.text = std::move(prepared),
.stars = stars,
.admin = (from == _call->peer()) || (creator && from->isSelf()),
.mine = true,
});
}
if (!_call->conference()) {
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as
| (stars ? Flag::f_allow_paid_stars : Flag())),
_call->inputCall(),
MTP_long(randomId),
serialized,
MTP_long(stars),
from->input()
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
failed(randomId, response);
}).send();
} else {
const auto bytes = SerializeMessage({ randomId, serialized });
auto v = std::vector<std::uint8_t>(bytes.size());
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
const auto userId = peerToUser(from->id).bare;
const auto encrypt = _call->e2eEncryptDecrypt();
const auto encrypted = encrypt(v, int64_t(userId), true, 0);
_api->request(MTPphone_SendGroupCallEncryptedMessage(
_call->inputCall(),
MTP_bytes(bytes::make_span(encrypted))
)).done([=](const MTPBool &, const MTP::Response &response) {
sent(randomId, response);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
failed(randomId, response);
}).send();
}
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::setApplyingInitial(bool value) {
_applyingInitial = value;
}
void Messages::received(const MTPDupdateGroupCallMessage &data) {
if (!ready()) {
return;
}
const auto &fields = data.vmessage().data();
received(
fields.vid().v,
fields.vfrom_id(),
fields.vmessage(),
fields.vdate().v,
fields.vpaid_message_stars().value_or_empty(),
fields.is_from_admin());
}
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
if (!ready()) {
return;
}
const auto fromId = data.vfrom_id();
const auto &bytes = data.vencrypted_message().v;
auto v = std::vector<std::uint8_t>(bytes.size());
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
const auto userId = peerToUser(peerFromMTP(fromId)).bare;
const auto decrypt = _call->e2eEncryptDecrypt();
const auto decrypted = decrypt(v, int64_t(userId), false, 0);
const auto deserialized = DeserializeMessage(QByteArray::fromRawData(
reinterpret_cast<const char*>(decrypted.data()),
decrypted.size()));
if (!deserialized) {
LOG(("API Error: Can't parse decrypted message"));
return;
}
const auto realId = ++_conferenceIdAutoIncrement;
const auto randomId = deserialized->randomId;
if (!_conferenceIdByRandomId.emplace(randomId, realId).second) {
// Already received.
return;
}
received(
realId,
fromId,
deserialized->message,
base::unixtime::now(), // date
0, // stars
false,
true); // checkCustomEmoji
}
void Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {
const auto was = _messages.size();
for (const auto &id : data.vmessages().v) {
const auto i = ranges::find(_messages, id.v, &Message::id);
if (i != end(_messages)) {
_messages.erase(i);
}
}
if (_messages.size() < was) {
pushChanges();
}
}
void Messages::sent(const MTPDupdateMessageID &data) {
sent(data.vrandom_id().v, data.vid().v);
}
void Messages::sent(uint64 randomId, const MTP::Response &response) {
const auto realId = ++_conferenceIdAutoIncrement;
_conferenceIdByRandomId.emplace(randomId, realId);
sent(randomId, realId);
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
}
void Messages::sent(uint64 randomId, MsgId realId) {
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j == end(_messages)) {
_skippedIds.emplace(realId);
return;
}
j->id = realId;
crl::on_main(this, [=] {
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = base::unixtime::now();
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
});
_idUpdates.fire({ .localId = localId, .realId = realId });
}
void Messages::received(
MsgId id,
const MTPPeer &from,
const MTPTextWithEntities &message,
TimeId date,
int stars,
bool fromAdmin,
bool checkCustomEmoji) {
const auto peer = _call->peer();
const auto i = ranges::find(_messages, id, &Message::id);
if (i != end(_messages)) {
const auto fromId = peerFromMTP(from);
const auto me1 = peer->session().userPeerId();
const auto me2 = _call->messagesFrom()->id;
if (((fromId == me1) || (fromId == me2)) && !i->date) {
i->date = date;
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
return;
} else if (_skippedIds.contains(id)) {
return;
}
auto allowedEntityTypes = std::vector<EntityType>{
EntityType::Code,
EntityType::Bold,
EntityType::Semibold,
EntityType::Spoiler,
EntityType::StrikeOut,
EntityType::Underline,
EntityType::Italic,
EntityType::CustomEmoji,
};
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
allowedEntityTypes.pop_back();
}
const auto author = peer->owner().peer(peerFromMTP(from));
auto text = Ui::Text::Filtered(
Api::ParseTextWithEntities(&author->session(), message),
allowedEntityTypes);
const auto mine = author->isSelf()
|| (author->isChannel() && author->asChannel()->amCreator());
const auto skip = skipMessage(text, stars);
if (skip) {
_skippedIds.emplace(id);
} else {
// Should check by sendAsPeers() list instead, but it may not be
// loaded here yet.
_messages.push_back({
.id = id,
.date = date,
.pinFinishDate = PinFinishDate(author, date, stars),
.peer = author,
.text = std::move(text),
.stars = stars,
.admin = fromAdmin,
.mine = mine,
});
ranges::sort(_messages, ranges::less(), &Message::id);
}
if (!_applyingInitial) {
addStars(author, stars, mine);
}
if (!skip) {
checkDestroying(true);
}
}
bool Messages::skipMessage(const TextWithEntities &text, int stars) const {
const auto real = _call->lookupReal();
return text.empty()
&& real
&& (stars < real->messagesMinPrice());
}
void Messages::checkDestroying(bool afterChanges) {
auto next = TimeId();
const auto now = base::unixtime::now();
const auto initial = int(_messages.size());
if (_call->videoStream()) {
if (initial > kMaxShownVideoStreamMessages) {
const auto remove = initial - kMaxShownVideoStreamMessages;
auto i = begin(_messages);
for (auto k = 0; k != remove; ++k) {
if (i->date && i->pinFinishDate <= now) {
i = _messages.erase(i);
} else if (!next || next > i->pinFinishDate - now) {
next = i->pinFinishDate - now;
++i;
} else {
++i;
}
}
}
} else for (auto i = begin(_messages); i != end(_messages);) {
const auto date = i->date;
//const auto ttl = i->stars
// ? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)
// : _ttl;
const auto ttl = _ttl;
if (!date) {
if (i->id < 0) {
++i;
} else {
i = _messages.erase(i);
}
} else if (date + ttl <= now) {
i = _messages.erase(i);
} else if (!next || next > date + ttl - now) {
next = date + ttl - now;
++i;
} else {
++i;
}
}
if (!next) {
_destroyTimer.cancel();
} else {
const auto delay = next * crl::time(1000);
if (!_destroyTimer.isActive()
|| (_destroyTimer.remainingTime() > delay)) {
_destroyTimer.callOnce(delay);
}
}
if (afterChanges || (_messages.size() < initial)) {
pushChanges();
}
}
rpl::producer<std::vector<Message>> Messages::listValue() const {
return _changes.events_starting_with_copy(_messages);
}
rpl::producer<MessageIdUpdate> Messages::idUpdates() const {
return _idUpdates.events();
}
void Messages::sendPending() {
Expects(_real != nullptr);
for (auto &pending : base::take(_pending)) {
send(std::move(pending.text), pending.stars);
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::pushChanges() {
if (_changesScheduled) {
return;
}
_changesScheduled = true;
Ui::PostponeCall(this, [=] {
_changesScheduled = false;
_changes.fire_copy(_messages);
});
}
void Messages::failed(uint64 randomId, const MTP::Response &response) {
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j != end(_messages) && !j->date) {
j->date = Api::UnixtimeFromMsgId(response.outerMsgId);
j->stars = 0;
j->failed = true;
checkDestroying(true);
}
}
int Messages::reactionsPaidScheduled() const {
return _paid.scheduled;
}
PeerId Messages::reactionsLocalShownPeer() const {
const auto minePaidShownPeer = [&] {
for (const auto &entry : _paid.top.topDonors) {
if (entry.my) {
return entry.peer ? entry.peer->id : PeerId();
}
}
return _call->messagesFrom()->id;
//const auto api = &_session->api();
//return api->globalPrivacy().paidReactionShownPeerCurrent();
};
return _paid.scheduledFlag
? _paid.scheduledShownPeer
: _paid.sendingFlag
? _paid.sendingShownPeer
: minePaidShownPeer();
}
void Messages::reactionsPaidAdd(int count) {
Expects(count >= 0);
_paid.scheduled += count;
_paid.scheduledFlag = 1;
_paid.scheduledShownPeer = _call->messagesFrom()->id;
if (count > 0) {
_session->credits().lock(CreditsAmount(count));
}
_call->peer()->owner().reactions().schedulePaid(_call);
_paidChanges.fire({});
}
void Messages::reactionsPaidScheduledCancel() {
if (!_paid.scheduledFlag) {
return;
}
if (const auto amount = int(_paid.scheduled)) {
_session->credits().unlock(
CreditsAmount(amount));
}
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
_paidChanges.fire({});
}
Data::PaidReactionSend Messages::startPaidReactionSending() {
_paidSendingPending = false;
if (!_paid.scheduledFlag || !_paid.scheduled) {
return {};
} else if (_paid.sendingFlag || !ready()) {
_paidSendingPending = true;
return {};
}
_paid.sending = _paid.scheduled;
_paid.sendingFlag = _paid.scheduledFlag;
_paid.sendingShownPeer = _paid.scheduledShownPeer;
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
return {
.count = int(_paid.sending),
.valid = true,
.shownPeer = _paid.sendingShownPeer,
};
}
void Messages::finishPaidSending(
Data::PaidReactionSend send,
bool success) {
Expects(send.count == _paid.sending);
Expects(send.valid == (_paid.sendingFlag == 1));
Expects(send.shownPeer == _paid.sendingShownPeer);
_paid.sending = 0;
_paid.sendingFlag = 0;
_paid.sendingShownPeer = 0;
if (const auto amount = send.count) {
if (success) {
const auto from = _session->data().peer(*send.shownPeer);
_session->credits().withdrawLocked(CreditsAmount(amount));
auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsDonor::my);
if (i != end(donors)) {
i->peer = from;
i->stars += amount;
} else {
donors.push_back({
.peer = from,
.stars = amount,
.my = true,
});
}
} else {
_session->credits().unlock(CreditsAmount(amount));
_paidChanges.fire({});
}
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::reactionsPaidSend() {
const auto send = startPaidReactionSending();
if (!send.valid || !send.count) {
return;
}
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _session->data().peer(*send.shownPeer);
const auto stars = int(send.count);
const auto skip = skipMessage({}, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.stars = stars,
.admin = (from == _call->peer()),
.mine = true,
});
}
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),
_call->inputCall(),
MTP_long(randomId),
MTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),
MTP_long(stars),
from->input()
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
finishPaidSending(send, true);
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
finishPaidSending(send, false);
failed(randomId, response);
}).send();
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::undoScheduledPaidOnDestroy() {
_call->peer()->owner().reactions().undoScheduledPaid(_call);
}
Messages::PaidLocalState Messages::starsLocalState() const {
const auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsDonor::my);
const auto local = int(_paid.scheduled);
const auto my = (i != end(donors) ? i->stars : 0) + local;
const auto total = _paid.top.total + local;
return { .total = total, .my = my };
}
void Messages::deleteConfirmed(MessageDeleteRequest request) {
const auto eraseFrom = [&](auto iterator) {
if (iterator != end(_messages)) {
_messages.erase(iterator, end(_messages));
pushChanges();
}
};
const auto peer = _call->peer();
if (const auto from = request.deleteAllFrom) {
using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;
_api->request(MTPphone_DeleteGroupCallParticipantMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
from->input()
)).send();
eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));
} else {
using Flag = MTPphone_DeleteGroupCallMessages::Flag;
_api->request(MTPphone_DeleteGroupCallMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
MTP_vector<MTPint>(1, MTP_int(request.id.bare))
)).send();
eraseFrom(ranges::remove(_messages, request.id, &Message::id));
}
if (const auto ban = request.ban) {
if (const auto channel = peer->asChannel()) {
ban->session().api().chatParticipants().kick(
channel,
ban,
ChatRestrictionsInfo());
} else {
ban->session().api().blockedPeers().block(ban);
}
}
}
void Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {
if (stars <= 0) {
return;
}
_paid.top.total += stars;
const auto i = ranges::find(
_paid.top.topDonors,
from.get(),
&StarsDonor::peer);
if (i != end(_paid.top.topDonors)) {
i->stars += stars;
} else {
_paid.top.topDonors.push_back({
.peer = from,
.stars = stars,
.my = mine,
});
}
ranges::stable_sort(
_paid.top.topDonors,
ranges::greater(),
&StarsDonor::stars);
_paidChanges.fire({ .peer = from, .stars = stars });
}
} // namespace Calls::Group

View File

@@ -0,0 +1,199 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "base/weak_ptr.h"
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Data {
class GroupCall;
struct PaidReactionSend;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace MTP {
class Sender;
struct Response;
} // namespace MTP
namespace Calls::Group {
struct Message {
MsgId id = 0;
TimeId date = 0;
TimeId pinFinishDate = 0;
not_null<PeerData*> peer;
TextWithEntities text;
int stars = 0;
bool failed = false;
bool admin = false;
bool mine = false;
};
struct MessageIdUpdate {
MsgId localId = 0;
MsgId realId = 0;
};
struct MessageDeleteRequest {
MsgId id = 0;
PeerData *deleteAllFrom = nullptr;
PeerData *ban = nullptr;
bool reportSpam = false;
};
struct StarsDonor {
PeerData *peer = nullptr;
int stars = 0;
bool my = false;
friend inline bool operator==(
const StarsDonor &,
const StarsDonor &) = default;
};
struct StarsTop {
std::vector<StarsDonor> topDonors;
int total = 0;
friend inline bool operator==(
const StarsTop &,
const StarsTop &) = default;
};
class Messages final : public base::has_weak_ptr {
public:
Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);
~Messages();
void send(TextWithTags text, int stars);
void setApplyingInitial(bool value);
void received(const MTPDupdateGroupCallMessage &data);
void received(const MTPDupdateGroupCallEncryptedMessage &data);
void deleted(const MTPDupdateDeleteGroupCallMessages &data);
void sent(const MTPDupdateMessageID &data);
[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;
[[nodiscard]] rpl::producer<MessageIdUpdate> idUpdates() const;
[[nodiscard]] int reactionsPaidScheduled() const;
[[nodiscard]] PeerId reactionsLocalShownPeer() const;
void reactionsPaidAdd(int count);
void reactionsPaidScheduledCancel();
void reactionsPaidSend();
void undoScheduledPaidOnDestroy();
struct PaidLocalState {
int total = 0;
int my = 0;
};
[[nodiscard]] PaidLocalState starsLocalState() const;
[[nodiscard]] rpl::producer<StarsDonor> starsValueChanges() const {
return _paidChanges.events();
}
[[nodiscard]] const StarsTop &starsTop() const {
return _paid.top;
}
void requestHiddenShow() {
_hiddenShowRequests.fire({});
}
[[nodiscard]] rpl::producer<> hiddenShowRequested() const {
return _hiddenShowRequests.events();
}
void deleteConfirmed(MessageDeleteRequest request);
private:
struct Pending {
TextWithTags text;
int stars = 0;
};
struct Paid {
StarsTop top;
PeerId scheduledShownPeer = 0;
PeerId sendingShownPeer = 0;
uint32 scheduled : 30 = 0;
uint32 scheduledFlag : 1 = 0;
uint32 scheduledPrivacySet : 1 = 0;
uint32 sending : 30 = 0;
uint32 sendingFlag : 1 = 0;
uint32 sendingPrivacySet : 1 = 0;
};
[[nodiscard]] bool ready() const;
void sendPending();
void pushChanges();
void checkDestroying(bool afterChanges = false);
void received(
MsgId id,
const MTPPeer &from,
const MTPTextWithEntities &message,
TimeId date,
int stars,
bool fromAdmin,
bool checkCustomEmoji = false);
void sent(uint64 randomId, const MTP::Response &response);
void sent(uint64 randomId, MsgId realId);
void failed(uint64 randomId, const MTP::Response &response);
[[nodiscard]] bool skipMessage(
const TextWithEntities &text,
int stars) const;
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
void finishPaidSending(
Data::PaidReactionSend send,
bool success);
void addStars(not_null<PeerData*> from, int stars, bool mine);
void requestStarsStats();
const not_null<GroupCall*> _call;
const not_null<Main::Session*> _session;
const not_null<MTP::Sender*> _api;
MsgId _conferenceIdAutoIncrement = 0;
base::flat_map<uint64, MsgId> _conferenceIdByRandomId;
base::flat_map<uint64, MsgId> _sendingIdByRandomId;
Data::GroupCall *_real = nullptr;
std::vector<Pending> _pending;
base::Timer _destroyTimer;
std::vector<Message> _messages;
base::flat_set<MsgId> _skippedIds;
rpl::event_stream<std::vector<Message>> _changes;
rpl::event_stream<MessageIdUpdate> _idUpdates;
bool _applyingInitial = false;
mtpRequestId _starsTopRequestId = 0;
Paid _paid;
rpl::event_stream<StarsDonor> _paidChanges;
bool _paidSendingPending = false;
TimeId _ttl = 0;
bool _changesScheduled = false;
rpl::event_stream<> _hiddenShowRequests;
base::Timer _starsStatsTimer;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/text/custom_emoji_helper.h"
#include "ui/round_rect.h"
struct TextWithTags;
namespace ChatHelpers {
class Show;
class TabbedPanel;
} // namespace ChatHelpers
namespace Data {
struct ReactionId;
} // namespace Data
namespace Ui {
class ElasticScroll;
class EmojiButton;
class InputField;
class SendButton;
class PopupMenu;
class RpWidget;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring;
} // namespace Calls::Group::Ui
namespace Calls::Group {
struct Message;
struct MessageIdUpdate;
struct MessageDeleteRequest;
enum class MessagesMode {
GroupCall,
VideoStream,
};
class MessagesUi final {
public:
MessagesUi(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
MessagesMode mode,
rpl::producer<std::vector<Message>> messages,
rpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> canManageValue,
rpl::producer<bool> shown);
~MessagesUi();
void move(int left, int bottom, int width, int availableHeight);
void raise();
[[nodiscard]] rpl::producer<> hiddenShowRequested() const;
[[nodiscard]] rpl::producer<MessageDeleteRequest> deleteRequests() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct MessageView;
struct PinnedView;
struct PayedBg {
explicit PayedBg(const Ui::StarsColoring &coloring);
style::owned_color light;
style::owned_color dark;
Ui::RoundRect pinnedLight;
Ui::RoundRect pinnedDark;
Ui::RoundRect messageLight;
Ui::RoundRect priceDark;
Ui::RoundRect badgeDark;
};
void setupBadges();
void setupList(
rpl::producer<std::vector<Message>> messages,
rpl::producer<bool> shown);
void showList(const std::vector<Message> &list);
void handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates);
void toggleMessage(MessageView &entry, bool shown);
void setContentFailed(MessageView &entry);
void setContent(MessageView &entry);
void setContent(PinnedView &entry);
void updateMessageSize(MessageView &entry);
bool updateMessageHeight(MessageView &entry);
void updatePinnedSize(PinnedView &entry);
bool updatePinnedWidth(PinnedView &entry);
void animateMessageSent(MessageView &entry);
void repaintMessage(MsgId id);
void highlightMessage(MsgId id);
void startHighlight(MsgId id);
void recountHeights(std::vector<MessageView>::iterator i, int top);
void appendMessage(const Message &data);
void checkReactionContent(
MessageView &entry,
const TextWithEntities &text);
void startReactionAnimation(MessageView &entry);
void updateReactionPosition(MessageView &entry);
void removeReaction(not_null<Ui::RpWidget*> widget);
void setupMessagesWidget();
void togglePinned(PinnedView &entry, bool shown);
void repaintPinned(MsgId id);
void recountWidths(std::vector<PinnedView>::iterator i, int left);
void appendPinned(const Message &data, TimeId now);
void setupPinnedWidget();
void applyGeometry();
void applyGeometryToPinned();
void updateGeometries();
[[nodiscard]] int countPinnedScrollSkip(const PinnedView &entry) const;
void setPinnedScrollSkip(int skip);
void updateTopFade();
void updateBottomFade();
void updateLeftFade();
void updateRightFade();
void receiveSomeMouseEvents();
void receiveAllMouseEvents();
void handleClick(const MessageView &entry, QPoint point);
void showContextMenu(const MessageView &entry, QPoint globalPoint);
[[nodiscard]] int donorPlace(not_null<PeerData*> peer) const;
[[nodiscard]] TextWithEntities nameText(
not_null<PeerData*> peer,
int place);
const not_null<QWidget*> _parent;
const std::shared_ptr<ChatHelpers::Show> _show;
const MessagesMode _mode;
std::unique_ptr<Ui::ElasticScroll> _scroll;
Ui::Animations::Simple _scrollToAnimation;
Ui::RpWidget *_messages = nullptr;
QImage _canvas;
std::unique_ptr<Ui::ElasticScroll> _pinnedScroll;
Ui::RpWidget *_pinned = nullptr;
QImage _pinnedCanvas;
int _pinnedScrollSkip = 0;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::variable<bool> _canManage;
rpl::event_stream<> _hiddenShowRequested;
rpl::event_stream<MessageDeleteRequest> _deleteRequests;
std::optional<std::vector<Message>> _hidden;
std::vector<MessageView> _views;
style::complex_color _messageBg;
Ui::RoundRect _messageBgRect;
MsgId _delayedHighlightId = 0;
MsgId _highlightId = 0;
Ui::Animations::Simple _highlightAnimation;
std::vector<PinnedView> _pinnedViews;
base::flat_map<uint64, std::unique_ptr<PayedBg>> _bgs;
QPoint _reactionBasePosition;
rpl::lifetime _effectsLifetime;
Ui::Text::String _liveBadge;
Ui::Text::String _adminBadge;
Ui::Text::CustomEmojiHelper _crownHelper;
base::flat_map<int, QString> _crownEmojiDataCache;
rpl::variable<std::vector<not_null<PeerData*>>> _topDonors;
//Ui::Animations::Simple _topFadeAnimation;
//Ui::Animations::Simple _bottomFadeAnimation;
//Ui::Animations::Simple _leftFadeAnimation;
//Ui::Animations::Simple _rightFadeAnimation;
int _fadeHeight = 0;
int _fadeWidth = 0;
bool _topFadeShown = false;
bool _bottomFadeShown = false;
bool _leftFadeShown = false;
bool _rightFadeShown = false;
bool _streamMode = false;
int _left = 0;
int _bottom = 0;
int _width = 0;
int _availableHeight = 0;
MsgId _revealedSpoilerId = 0;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,281 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/effects/animations.h"
class Image;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
class PhotoMedia;
class GroupCall;
} // namespace Data
namespace Ui {
class Show;
class BoxContent;
class AbstractButton;
class ImportantTooltip;
class DropdownMenu;
class CallButton;
class CallMuteButton;
class IconButton;
class FlatLabel;
class RpWidget;
class RpWindow;
template <typename Widget>
class FadeWrap;
template <typename Widget>
class PaddingWrap;
class ScrollArea;
class GenericBox;
class GroupCallScheduledLeft;
struct CallButtonColors;
} // namespace Ui
namespace Ui::Toast {
class Instance;
struct Config;
} // namespace Ui::Toast
namespace style {
struct CallSignalBars;
struct CallBodyLayout;
} // namespace style
namespace Calls {
struct InviteRequest;
struct ConferencePanelMigration;
class Window;
} // namespace Calls
namespace Calls::Group {
class Toasts;
class Members;
class Viewport;
enum class PanelMode;
enum class StickedTooltip;
class MicLevelTester;
class MessageField;
class MessagesUi;
class Panel final
: public base::has_weak_ptr
, private Ui::DesktopCapture::ChooseSourceDelegate {
public:
explicit Panel(not_null<GroupCall*> call);
Panel(not_null<GroupCall*> call, ConferencePanelMigration info);
~Panel();
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
[[nodiscard]] not_null<GroupCall*> call() const;
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const;
void migrationShowShareLink();
void migrationInviteUsers(std::vector<InviteRequest> users);
void minimize();
void toggleFullScreen();
void toggleFullScreen(bool fullscreen);
void close();
void showAndActivate();
void closeBeforeDestroy();
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
[[nodiscard]] not_null<Window*> callWindow() const;
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
rpl::lifetime &lifetime();
private:
using State = GroupCall::State;
struct ControlsBackgroundNarrow;
enum class NiceTooltipType {
Normal,
Sticked,
};
enum class StickedTooltipHide {
Unavailable,
Activated,
Discarded,
};
[[nodiscard]] PanelMode mode() const;
void paint(QRect clip);
void initWindow();
void initWidget();
void initControls();
void initShareAction();
void initLayout(ConferencePanelMigration info);
void initGeometry(ConferencePanelMigration info);
void setupScheduledLabels(rpl::producer<TimeId> date);
void setupMembers();
void setupVideo(not_null<Viewport*> viewport);
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
[[nodiscard]] rpl::producer<QString> titleText();
bool handleClose();
void startScheduledNow();
void trackControls(bool track, bool force = false);
void raiseControls();
void enlargeVideo();
void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime);
void trackControlOver(not_null<Ui::RpWidget*> control, bool over);
void showNiceTooltip(
not_null<Ui::RpWidget*> control,
NiceTooltipType type = NiceTooltipType::Normal);
void showStickedTooltip();
void hideStickedTooltip(StickedTooltipHide hide);
void hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide);
void hideNiceTooltip();
bool updateMode();
void updateControlsGeometry();
void updateButtonsGeometry();
void updateTooltipGeometry();
void updateButtonsStyles();
void updateMembersGeometry();
void refreshControlsBackground();
void refreshTitleBackground();
void setupControlsBackgroundWide();
void setupControlsBackgroundNarrow();
void showControls();
void createMessageButton();
void refreshLeftButton();
void refreshVideoButtons(
std::optional<bool> overrideWideMode = std::nullopt);
void refreshTopButton();
void createPinOnTop();
void setupEmptyRtmp();
void toggleWideControls(bool shown);
void updateWideControlsVisibility();
[[nodiscard]] bool videoButtonInNarrowMode() const;
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
void toggleMessageTyping();
[[nodiscard]] rpl::producer<Ui::CallButtonColors> toggleableOverrides(
rpl::producer<bool> active);
void endCall();
void showMainMenu();
void chooseJoinAs();
void chooseShareScreenSource();
void screenSharingPrivacyRequest();
void addMembers();
void kickParticipant(not_null<PeerData*> participantPeer);
void kickParticipantSure(not_null<PeerData*> participantPeer);
[[nodiscard]] QRect computeTitleRect() const;
void refreshTitle();
void refreshTitleGeometry();
void refreshTitleColors();
void setupRealCallViewers();
void subscribeToChanges(not_null<Data::GroupCall*> real);
void migrate(not_null<ChannelData*> channel);
void subscribeToPeerChanges();
QWidget *chooseSourceParent() override;
QString chooseSourceActiveDeviceId() override;
bool chooseSourceActiveWithAudio() override;
bool chooseSourceWithAudioSupported() override;
rpl::lifetime &chooseSourceInstanceLifetime() override;
void chooseSourceAccepted(
const QString &deviceId,
bool withAudio) override;
void chooseSourceStop() override;
const not_null<GroupCall*> _call;
not_null<PeerData*> _peer;
std::shared_ptr<Window> _window;
rpl::variable<PanelMode> _mode;
rpl::variable<bool> _fullScreenOrMaximized = false;
bool _unpinnedMaximized = false;
bool _rtmpFull = false;
rpl::lifetime _callLifetime;
object_ptr<Ui::RpWidget> _titleBackground = { nullptr };
object_ptr<Ui::FlatLabel> _title = { nullptr };
object_ptr<Ui::FlatLabel> _titleSeparator = { nullptr };
object_ptr<Ui::FlatLabel> _viewers = { nullptr };
object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
object_ptr<Ui::AbstractButton> _recordingMark = { nullptr };
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
object_ptr<Ui::IconButton> _pinOnTop = { nullptr };
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
rpl::variable<bool> _wideMenuShown = false;
rpl::variable<bool> _messageTyping = false;
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
object_ptr<Members> _members = { nullptr };
std::unique_ptr<Viewport> _viewport;
rpl::lifetime _trackControlsOverStateLifetime;
rpl::lifetime _trackControlsMenuLifetime;
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
object_ptr<Ui::RpWidget> _countdown = { nullptr };
std::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
object_ptr<Ui::RpWidget> _emptyRtmp = { nullptr };
ChooseJoinAsProcess _joinAsProcess;
std::optional<QRect> _lastSmallGeometry;
std::optional<QRect> _lastLargeGeometry;
bool _lastLargeMaximized = false;
bool _showWideControls = false;
bool _trackControls = false;
bool _wideControlsShown = false;
Ui::Animations::Simple _wideControlsAnimation;
object_ptr<Ui::RpWidget> _controlsBackgroundWide = { nullptr };
std::unique_ptr<ControlsBackgroundNarrow> _controlsBackgroundNarrow;
object_ptr<Ui::CallButton> _settings = { nullptr };
object_ptr<Ui::CallButton> _wideMenu = { nullptr };
object_ptr<Ui::CallButton> _callShare = { nullptr };
object_ptr<Ui::CallButton> _video = { nullptr };
object_ptr<Ui::CallButton> _screenShare = { nullptr };
object_ptr<Ui::CallButton> _message = { nullptr };
std::unique_ptr<Ui::CallMuteButton> _mute;
object_ptr<Ui::CallButton> _hangup;
object_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };
QPointer<Ui::IconButton> _stickedTooltipClose;
QPointer<Ui::RpWidget> _niceTooltipControl;
StickedTooltips _stickedTooltipsShown;
Fn<void()> _callShareLinkCallback;
std::shared_ptr<ChatHelpers::Show> _cachedShow;
std::unique_ptr<MessageField> _messageField;
std::unique_ptr<MessagesUi> _messages;
const std::unique_ptr<Toasts> _toasts;
std::unique_ptr<MicLevelTester> _micLevelTester;
style::complex_color _controlsBackgroundColor;
base::Timer _hideControlsTimer;
rpl::lifetime _hideControlsTimerLifetime;
rpl::lifetime _peerLifetime;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,356 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_rtmp.h"
#include "apiwrap.h"
#include "calls/group/calls_group_common.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "styles/style_boxes.h"
#include "styles/style_calls.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QGuiApplication>
#include <QStyle>
namespace Calls::Group {
namespace {
constexpr auto kPasswordCharAmount = 24;
void StartWithBox(
not_null<Ui::GenericBox*> box,
Fn<void()> done,
Fn<void()> revoke,
std::shared_ptr<Ui::Show> show,
rpl::producer<RtmpInfo> &&data) {
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto state = box->lifetime().make_state<State>();
{
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"rtmp"_q,
.sizeOverride = st::normalBoxLottieSize,
},
{});
box->verticalLayout()->add(std::move(icon.widget), {}, style::al_top);
box->setShowFinishedCallback([animate = icon.animate] {
animate(anim::repeat::loop);
});
Ui::AddSkip(box->verticalLayout());
}
StartRtmpProcess::FillRtmpRows(
box->verticalLayout(),
true,
std::move(show),
std::move(data),
&st::boxLabel,
&st::groupCallRtmpShowButton,
&st::defaultSubsectionTitle,
&st::attentionBoxButton,
&st::defaultPopupMenu);
box->setTitle(tr::lng_group_call_rtmp_title());
Ui::AddDividerText(
box->verticalLayout(),
tr::lng_group_call_rtmp_info());
box->addButton(tr::lng_group_call_rtmp_start(), done);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->setWidth(st::boxWideWidth);
{
const auto top = box->addTopButton(st::infoTopBarMenu);
top->setClickedCallback([=] {
state->menu = base::make_unique_q<Ui::PopupMenu>(
top,
st::popupMenuWithIcons);
state->menu->addAction(
tr::lng_group_invite_context_revoke(tr::now),
revoke,
&st::menuIconRemove);
state->menu->setForcedOrigin(
Ui::PanelAnimation::Origin::TopRight);
top->setForceRippled(true);
const auto raw = state->menu.get();
raw->setDestroyedCallback([=] {
if ((state->menu == raw) && top) {
top->setForceRippled(false);
}
});
state->menu->popup(top->mapToGlobal(top->rect().center()));
return true;
});
}
}
} // namespace
StartRtmpProcess::~StartRtmpProcess() {
close();
}
void StartRtmpProcess::start(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
Fn<void(JoinInfo)> done) {
Expects(done != nullptr);
const auto session = &peer->session();
if (_request) {
if (_request->peer == peer) {
_request->show = std::move(show);
_request->done = std::move(done);
return;
}
session->api().request(_request->id).cancel();
_request = nullptr;
}
_request = std::make_unique<RtmpRequest>(
RtmpRequest{
.peer = peer,
.show = std::move(show),
.done = std::move(done),
});
session->account().sessionChanges(
) | rpl::on_next([=] {
_request = nullptr;
}, _request->lifetime);
requestUrl(false);
}
void StartRtmpProcess::close() {
if (_request) {
_request->peer->session().api().request(_request->id).cancel();
if (const auto strong = _request->box.get()) {
strong->closeBox();
}
_request = nullptr;
}
}
void StartRtmpProcess::requestUrl(bool revoke) {
const auto session = &_request->peer->session();
_request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl(
MTP_flags(0),
_request->peer->input(),
MTP_bool(revoke)
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
auto data = result.match([&](
const MTPDphone_groupCallStreamRtmpUrl &data) {
return RtmpInfo{ .url = qs(data.vurl()), .key = qs(data.vkey()) };
});
processUrl(std::move(data));
}).fail([=] {
_request->show->showToast(Lang::Hard::ServerError());
}).send();
}
void StartRtmpProcess::processUrl(RtmpInfo data) {
if (!_request->box) {
createBox();
}
_request->data = std::move(data);
}
void StartRtmpProcess::finish(JoinInfo info) {
info.rtmpInfo = _request->data.current();
_request->done(std::move(info));
}
void StartRtmpProcess::createBox() {
auto done = [=] {
const auto peer = _request->peer;
const auto joinAs = (peer->isChat() && peer->asChat()->amCreator())
? peer
: (peer->isChannel() && peer->asChannel()->amCreator())
? peer
: peer->session().user();
finish({ .peer = peer, .joinAs = joinAs, .rtmp = true });
};
auto revoke = [=] {
const auto guard = base::make_weak(&_request->guard);
_request->show->showBox(Ui::MakeConfirmBox({
.text = tr::lng_group_call_rtmp_revoke_sure(),
.confirmed = crl::guard(guard, [=](Fn<void()> &&close) {
requestUrl(true);
close();
}),
.confirmText = tr::lng_group_invite_context_revoke(),
}));
};
auto object = Box(
StartWithBox,
std::move(done),
std::move(revoke),
_request->show,
_request->data.value());
object->boxClosing(
) | rpl::on_next([=] {
_request = nullptr;
}, _request->lifetime);
_request->box = base::make_weak(object.data());
_request->show->showBox(std::move(object));
}
void StartRtmpProcess::FillRtmpRows(
not_null<Ui::VerticalLayout*> container,
bool divider,
std::shared_ptr<Ui::Show> show,
rpl::producer<RtmpInfo> &&data,
const style::FlatLabel *labelStyle,
const style::IconButton *showButtonStyle,
const style::FlatLabel *subsectionTitleStyle,
const style::RoundButton *attentionButtonStyle,
const style::PopupMenu *popupMenuStyle) {
struct State {
rpl::variable<QString> key;
rpl::variable<QString> url;
bool warned = false;
};
const auto &rowPadding = st::boxRowPadding;
const auto state = container->lifetime().make_state<State>();
state->key = rpl::duplicate(
data
) | rpl::map([=](const auto &d) { return d.key; });
state->url = std::move(
data
) | rpl::map([=](const auto &d) { return d.url; });
const auto showToast = [=](const QString &text) {
show->showToast(text);
};
const auto addButton = [&](
bool key,
rpl::producer<QString> &&text) {
auto wrap = object_ptr<Ui::RpWidget>(container);
auto button = Ui::CreateChild<Ui::RoundButton>(
wrap.data(),
rpl::duplicate(text),
st::groupCallRtmpCopyButton);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->setClickedCallback(key
? Fn<void()>([=] {
QGuiApplication::clipboard()->setText(state->key.current());
showToast(tr::lng_group_call_rtmp_key_copied(tr::now));
})
: Fn<void()>([=] {
QGuiApplication::clipboard()->setText(state->url.current());
showToast(tr::lng_group_call_rtmp_url_copied(tr::now));
}));
Ui::AddSkip(container, st::groupCallRtmpCopyButtonTopSkip);
const auto weak = container->add(std::move(wrap), rowPadding);
Ui::AddSkip(container, st::groupCallRtmpCopyButtonBottomSkip);
button->heightValue(
) | rpl::on_next([=](int height) {
weak->resize(weak->width(), height);
}, container->lifetime());
return weak;
};
const auto addLabel = [&](v::text::data &&text) {
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
v::text::take_marked(std::move(text)),
*labelStyle,
*popupMenuStyle),
st::boxRowPadding + QMargins(0, 0, showButtonStyle->width, 0));
label->setSelectable(true);
label->setBreakEverywhere(true);
return label;
};
// Server URL.
Ui::AddSubsectionTitle(
container,
tr::lng_group_call_rtmp_url_subtitle(),
st::groupCallRtmpSubsectionTitleAddPadding,
subsectionTitleStyle);
auto urlLabelContent = state->url.value();
addLabel(std::move(urlLabelContent));
Ui::AddSkip(container, st::groupCallRtmpUrlSkip);
addButton(false, tr::lng_group_call_rtmp_url_copy());
//
if (divider) {
Ui::AddDivider(container);
}
// Stream Key.
Ui::AddSkip(container, st::groupCallRtmpKeySubsectionTitleSkip);
Ui::AddSubsectionTitle(
container,
tr::lng_group_call_rtmp_key_subtitle(),
st::groupCallRtmpSubsectionTitleAddPadding,
subsectionTitleStyle);
auto keyLabelContent = state->key.value(
) | rpl::map([](const QString &key) {
const auto size = int(key.size());
auto result = TextWithEntities{ key };
if (size > 0) {
result.entities.push_back({ EntityType::Spoiler, 0, size });
}
return result;
}) | rpl::after_next([=] {
container->resizeToWidth(container->widthNoMargins());
});
const auto streamKeyLabel = addLabel(std::move(keyLabelContent));
streamKeyLabel->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button == Qt::LeftButton) {
show->showBox(Ui::MakeConfirmBox({
.text = tr::lng_group_call_rtmp_key_warning(
tr::rich),
.confirmed = [=](Fn<void()> &&close) {
handler->onClick({});
close();
},
.confirmText = tr::lng_from_request_understand(),
.cancelText = tr::lng_cancel(),
.confirmStyle = attentionButtonStyle,
.labelStyle = labelStyle,
}));
}
return false;
});
addButton(true, tr::lng_group_call_rtmp_key_copy());
//
}
} // namespace Calls::Group

View File

@@ -0,0 +1,75 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/object_ptr.h"
#include "calls/group/calls_group_common.h"
class PeerData;
namespace Ui {
class Show;
class BoxContent;
class VerticalLayout;
} // namespace Ui
namespace style {
struct FlatLabel;
struct RoundButton;
struct IconButton;
struct PopupMenu;
} // namespace style
namespace Calls::Group {
struct JoinInfo;
class StartRtmpProcess final {
public:
StartRtmpProcess() = default;
~StartRtmpProcess();
void start(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
Fn<void(JoinInfo)> done);
void close();
static void FillRtmpRows(
not_null<Ui::VerticalLayout*> container,
bool divider,
std::shared_ptr<Ui::Show> show,
rpl::producer<RtmpInfo> &&data,
const style::FlatLabel *labelStyle,
const style::IconButton *showButtonStyle,
const style::FlatLabel *subsectionTitleStyle,
const style::RoundButton *attentionButtonStyle,
const style::PopupMenu *popupMenuStyle);
private:
void requestUrl(bool revoke);
void processUrl(RtmpInfo data);
void createBox();
void finish(JoinInfo info);
struct RtmpRequest {
not_null<PeerData*> peer;
rpl::variable<RtmpInfo> data;
std::shared_ptr<Ui::Show> show;
Fn<void(JoinInfo)> done;
base::has_weak_ptr guard;
base::weak_qptr<Ui::BoxContent> box;
rpl::lifetime lifetime;
mtpRequestId id = 0;
};
std::unique_ptr<RtmpRequest> _request;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,988 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_settings.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_menu.h" // LeaveBox.
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/calls_volume_item.h"
#include "calls/calls_instance.h"
#include "ui/widgets/level_meter.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "lang/lang_keys.h"
#include "boxes/share_box.h"
#include "history/view/history_view_schedule_box.h"
#include "history/history_item_helpers.h" // GetErrorForSending.
#include "history/history.h"
#include "data/data_histories.h"
#include "data/data_session.h"
#include "base/timer_rpl.h"
#include "base/event_filter.h"
#include "base/global_shortcuts.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_group_call.h"
#include "data/data_user.h"
#include "calls/group/calls_group_rtmp.h"
#include "ui/toast/toast.h"
#include "data/data_changes.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "webrtc/webrtc_audio_input_tester.h"
#include "webrtc/webrtc_device_resolver.h"
#include "settings/settings_calls.h"
#include "settings/settings_credits_graphics.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "api/api_invite_links.h"
#include "styles/style_layers.h"
#include "styles/style_calls.h"
#include "styles/style_settings.h"
#include <QtGui/QGuiApplication>
namespace Calls::Group {
namespace {
constexpr auto kDelaysCount = 201;
constexpr auto kMicrophoneTooltipAfterLoudCount = 3;
constexpr auto kDropLoudAfterQuietCount = 5;
constexpr auto kMicrophoneTooltipLevelThreshold = 0.2;
constexpr auto kMicrophoneTooltipCheckInterval = crl::time(500);
#ifdef Q_OS_MAC
constexpr auto kCheckAccessibilityInterval = crl::time(500);
#endif // Q_OS_MAC
void SaveCallJoinMuted(
not_null<PeerData*> peer,
CallId callId,
bool joinMuted) {
const auto call = peer->groupCall();
if (!call
|| call->id() != callId
|| peer->isUser()
|| !peer->canManageGroupCall()
|| !call->canChangeJoinMuted()
|| call->joinMuted() == joinMuted) {
return;
}
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
call->setJoinMutedLocally(joinMuted);
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
MTP_flags(Flag::f_join_muted),
call->input(),
MTP_bool(joinMuted),
MTPBool(), // messages_enabled
MTPlong() // send_paid_messages_stars
)).send();
}
void SaveCallMessagesEnabled(
not_null<PeerData*> peer,
CallId callId,
bool messagesEnabled) {
const auto call = peer->groupCall();
if (!call
|| call->id() != callId
|| !peer->canManageGroupCall()
|| !call->canChangeMessagesEnabled()
|| call->messagesEnabled() == messagesEnabled) {
return;
}
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
call->setMessagesEnabledLocally(messagesEnabled);
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
MTP_flags(Flag::f_messages_enabled),
call->input(),
MTPBool(), // join_muted
MTP_bool(messagesEnabled),
MTPlong() // send_paid_messages_stars
)).send();
}
[[nodiscard]] crl::time DelayByIndex(int index) {
return index * crl::time(10);
}
[[nodiscard]] QString FormatDelay(crl::time delay) {
return (delay < crl::time(1000))
? tr::lng_group_call_ptt_delay_ms(
tr::now,
lt_amount,
QString::number(delay))
: tr::lng_group_call_ptt_delay_s(
tr::now,
lt_amount,
QString::number(delay / 1000., 'f', 2));
}
object_ptr<ShareBox> ShareInviteLinkBox(
not_null<PeerData*> peer,
const QString &linkSpeaker,
const QString &linkListener,
std::shared_ptr<Ui::Show> show) {
const auto sending = std::make_shared<bool>();
const auto box = std::make_shared<base::weak_qptr<ShareBox>>();
auto bottom = linkSpeaker.isEmpty()
? nullptr
: object_ptr<Ui::PaddingWrap<Ui::Checkbox>>(
nullptr,
object_ptr<Ui::Checkbox>(
nullptr,
tr::lng_group_call_share_speaker(tr::now),
true,
st::groupCallCheckbox),
st::groupCallShareMutedMargin);
const auto speakerCheckbox = bottom ? bottom->entity() : nullptr;
const auto currentLink = [=] {
return (!speakerCheckbox || !speakerCheckbox->checked())
? linkListener
: linkSpeaker;
};
auto copyCallback = [=] {
QGuiApplication::clipboard()->setText(currentLink());
show->showToast(tr::lng_group_invite_copied(tr::now));
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
if (*sending || result.empty()) {
return;
}
const auto error = GetErrorForSending(
result,
{ .text = &comment });
if (error.error) {
if (const auto weak = *box) {
weak->getDelegate()->show(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
const auto link = currentLink();
if (!comment.text.isEmpty()) {
comment.text = link + "\n" + comment.text;
const auto add = link.size() + 1;
for (auto &tag : comment.tags) {
tag.offset += add;
}
} else {
comment.text = link;
}
auto &api = peer->session().api();
for (const auto thread : result) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
message.textWithTags = comment;
message.action.clearDraft = false;
api.sendMessage(std::move(message));
}
if (*box) {
(*box)->closeBox();
}
show->showToast(tr::lng_share_done(tr::now));
};
auto filterCallback = [](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
return Data::CanSend(thread, ChatRestriction::SendOther);
};
const auto st = ::Settings::DarkCreditsEntryBoxStyle();
auto result = Box<ShareBox>(ShareBox::Descriptor{
.session = &peer->session(),
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.bottomWidget = std::move(bottom),
.copyLinkText = rpl::conditional(
(speakerCheckbox
? speakerCheckbox->checkedValue()
: rpl::single(false)),
tr::lng_group_call_copy_speaker_link(),
tr::lng_group_call_copy_listener_link()),
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
});
*box = result.data();
return result;
}
} // namespace
void SettingsBox(
not_null<Ui::GenericBox*> box,
not_null<GroupCall*> call) {
using namespace Settings;
const auto weakCall = base::make_weak(call);
const auto weakBox = base::make_weak(box);
struct State {
std::unique_ptr<Webrtc::DeviceResolver> deviceId;
std::unique_ptr<Webrtc::AudioInputTester> micTester;
Ui::LevelMeter *micTestLevel = nullptr;
float micLevel = 0.;
Ui::Animations::Simple micLevelAnimation;
base::Timer levelUpdateTimer;
bool generatingLink = false;
};
const auto peer = call->peer();
const auto state = box->lifetime().make_state<State>();
const auto real = call->conference()
? call->lookupReal()
: peer->groupCall();
const auto rtmp = call->rtmp();
const auto id = call->id();
const auto goodReal = (real && real->id() == id);
const auto layout = box->verticalLayout();
const auto &settings = Core::App().settings();
const auto joinMuted = !call->conference()
&& goodReal
&& real->joinMuted();
const auto messagesEnabled = goodReal && real->messagesEnabled();
const auto canChangeJoinMuted = !rtmp
&& goodReal
&& real->canChangeJoinMuted();
const auto canChangeMessagesEnabled = !rtmp
&& goodReal
&& real->canChangeMessagesEnabled();
const auto addCheck = canChangeJoinMuted && peer->canManageGroupCall();
const auto addMessages = canChangeMessagesEnabled
&& (call->conference() || peer->canManageGroupCall());
const auto addDivider = [&] {
layout->add(object_ptr<Ui::BoxContentDivider>(
layout,
st::boxDividerHeight,
st::groupCallDividerBar));
};
if (addCheck || addMessages) {
Ui::AddSkip(layout);
}
const auto muteJoined = addCheck
? layout->add(object_ptr<Ui::SettingsButton>(
layout,
tr::lng_group_call_new_muted(),
st::groupCallSettingsButton))->toggleOn(rpl::single(joinMuted))
: nullptr;
const auto enableMessages = addMessages
? layout->add(object_ptr<Ui::SettingsButton>(
layout,
tr::lng_group_call_enable_messages(),
st::groupCallSettingsButton))->toggleOn(
rpl::single(messagesEnabled))
: nullptr;
if (addCheck || addMessages) {
Ui::AddSkip(layout);
}
auto playbackIdWithFallback = Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callPlaybackDeviceIdValue(),
Core::App().settings().playbackDeviceIdValue());
AddButtonWithLabel(
layout,
tr::lng_group_call_speakers(),
PlaybackDeviceNameValue(rpl::duplicate(playbackIdWithFallback)),
st::groupCallSettingsButton
)->addClickHandler([=] {
box->getDelegate()->show(ChoosePlaybackDeviceBox(
rpl::duplicate(playbackIdWithFallback),
crl::guard(box, [=](const QString &id) {
Core::App().settings().setCallPlaybackDeviceId(id);
Core::App().saveSettingsDelayed();
}),
&st::groupCallCheckbox,
&st::groupCallRadio));
});
if (!rtmp) {
auto captureIdWithFallback = Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callCaptureDeviceIdValue(),
Core::App().settings().captureDeviceIdValue());
AddButtonWithLabel(
layout,
tr::lng_group_call_microphone(),
CaptureDeviceNameValue(rpl::duplicate(captureIdWithFallback)),
st::groupCallSettingsButton
)->addClickHandler([=] {
box->getDelegate()->show(ChooseCaptureDeviceBox(
rpl::duplicate(captureIdWithFallback),
crl::guard(box, [=](const QString &id) {
Core::App().settings().setCallCaptureDeviceId(id);
Core::App().saveSettingsDelayed();
}),
&st::groupCallCheckbox,
&st::groupCallRadio));
});
state->micTestLevel = box->addRow(
object_ptr<Ui::LevelMeter>(
box.get(),
st::groupCallLevelMeter),
st::settingsLevelMeterPadding);
state->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));
state->levelUpdateTimer.setCallback([=] {
const auto was = state->micLevel;
state->micLevel = state->micTester->getAndResetLevel();
state->micLevelAnimation.start([=] {
state->micTestLevel->setValue(
state->micLevelAnimation.value(state->micLevel));
}, was, state->micLevel, kMicTestAnimationDuration);
});
Ui::AddSkip(layout);
//Ui::AddDivider(layout);
//Ui::AddSkip(layout);
layout->add(object_ptr<Ui::SettingsButton>(
layout,
tr::lng_group_call_noise_suppression(),
st::groupCallSettingsButton
))->toggleOn(rpl::single(
settings.groupCallNoiseSuppression()
))->toggledChanges(
) | rpl::on_next([=](bool enabled) {
Core::App().settings().setGroupCallNoiseSuppression(enabled);
call->setNoiseSuppression(enabled);
Core::App().saveSettingsDelayed();
}, layout->lifetime());
using GlobalShortcut = base::GlobalShortcut;
struct PushToTalkState {
rpl::variable<QString> recordText = tr::lng_group_call_ptt_shortcut();
rpl::variable<QString> shortcutText;
rpl::event_stream<bool> pushToTalkToggles;
std::shared_ptr<base::GlobalShortcutManager> manager;
GlobalShortcut shortcut;
crl::time delay = 0;
bool recording = false;
};
if (base::GlobalShortcutsAvailable()) {
const auto state = box->lifetime().make_state<PushToTalkState>();
if (!base::GlobalShortcutsAllowed()) {
Core::App().settings().setGroupCallPushToTalk(false);
}
const auto tryFillFromManager = [=] {
state->shortcut = state->manager
? state->manager->shortcutFromSerialized(
Core::App().settings().groupCallPushToTalkShortcut())
: nullptr;
state->shortcutText = state->shortcut
? state->shortcut->toDisplayString()
: QString();
};
state->manager = settings.groupCallPushToTalk()
? call->ensureGlobalShortcutManager()
: nullptr;
tryFillFromManager();
state->delay = settings.groupCallPushToTalkDelay();
const auto pushToTalk = layout->add(
object_ptr<Ui::SettingsButton>(
layout,
tr::lng_group_call_push_to_talk(),
st::groupCallSettingsButton
))->toggleOn(rpl::single(
settings.groupCallPushToTalk()
) | rpl::then(state->pushToTalkToggles.events()));
const auto pushToTalkWrap = layout->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
layout,
object_ptr<Ui::VerticalLayout>(layout)));
const auto pushToTalkInner = pushToTalkWrap->entity();
const auto recording = pushToTalkInner->add(
object_ptr<Ui::SettingsButton>(
pushToTalkInner,
state->recordText.value(),
st::groupCallSettingsButton));
CreateRightLabel(
recording,
state->shortcutText.value(),
st::groupCallSettingsButton,
state->recordText.value());
const auto applyAndSave = [=] {
call->applyGlobalShortcutChanges();
Core::App().saveSettingsDelayed();
};
const auto showPrivacyRequest = [=] {
#ifdef Q_OS_MAC
if (!Platform::IsMac10_14OrGreater()) {
return;
}
const auto requestInputMonitoring = Platform::IsMac10_15OrGreater();
box->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
rpl::combine(
tr::lng_group_call_mac_access(),
(requestInputMonitoring
? tr::lng_group_call_mac_input()
: tr::lng_group_call_mac_accessibility())
) | rpl::map([](QString a, QString b) {
auto result = tr::rich(a);
result.append("\n\n").append(tr::rich(b));
return result;
}),
st::groupCallBoxLabel),
style::margins(
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom()));
box->addButton(tr::lng_group_call_mac_settings(), [=] {
if (requestInputMonitoring) {
Platform::OpenInputMonitoringPrivacySettings();
} else {
Platform::OpenAccessibilityPrivacySettings();
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
if (!requestInputMonitoring) {
// Accessibility is enabled without app restart, so short-poll it.
base::timer_each(
kCheckAccessibilityInterval
) | rpl::filter([] {
return base::GlobalShortcutsAllowed();
}) | rpl::on_next([=] {
box->closeBox();
}, box->lifetime());
}
}));
#endif // Q_OS_MAC
};
const auto ensureManager = [=] {
if (state->manager) {
return true;
} else if (base::GlobalShortcutsAllowed()) {
state->manager = call->ensureGlobalShortcutManager();
tryFillFromManager();
return true;
}
showPrivacyRequest();
return false;
};
const auto stopRecording = [=] {
state->recording = false;
state->recordText = tr::lng_group_call_ptt_shortcut();
state->shortcutText = state->shortcut
? state->shortcut->toDisplayString()
: QString();
recording->setColorOverride(std::nullopt);
if (state->manager) {
state->manager->stopRecording();
}
};
const auto startRecording = [=] {
if (!ensureManager()) {
state->pushToTalkToggles.fire(false);
pushToTalkWrap->hide(anim::type::instant);
return;
}
state->recording = true;
state->recordText = tr::lng_group_call_ptt_recording();
recording->setColorOverride(
st::groupCallSettingsAttentionButton.textFg->c);
auto progress = crl::guard(box, [=](GlobalShortcut shortcut) {
state->shortcutText = shortcut->toDisplayString();
});
auto done = crl::guard(box, [=](GlobalShortcut shortcut) {
state->shortcut = shortcut;
Core::App().settings().setGroupCallPushToTalkShortcut(shortcut
? shortcut->serialize()
: QByteArray());
applyAndSave();
stopRecording();
});
state->manager->startRecording(std::move(progress), std::move(done));
};
recording->addClickHandler([=] {
if (state->recording) {
stopRecording();
} else {
startRecording();
}
});
const auto label = pushToTalkInner->add(
object_ptr<Ui::LabelSimple>(
pushToTalkInner,
st::groupCallDelayLabel),
st::groupCallDelayLabelMargin);
const auto value = std::clamp(
state->delay,
crl::time(0),
DelayByIndex(kDelaysCount - 1));
const auto callback = [=](crl::time delay) {
state->delay = delay;
label->setText(tr::lng_group_call_ptt_delay(
tr::now,
lt_delay,
FormatDelay(delay)));
if (Core::App().settings().groupCallPushToTalkDelay() != delay) {
Core::App().settings().setGroupCallPushToTalkDelay(delay);
applyAndSave();
}
};
callback(value);
const auto slider = pushToTalkInner->add(
object_ptr<Ui::MediaSlider>(
pushToTalkInner,
st::groupCallDelaySlider),
st::groupCallDelayMargin);
slider->resize(st::groupCallDelaySlider.seekSize);
slider->setPseudoDiscrete(
kDelaysCount,
DelayByIndex,
value,
callback);
pushToTalkWrap->toggle(
settings.groupCallPushToTalk(),
anim::type::instant);
pushToTalk->toggledChanges(
) | rpl::on_next([=](bool toggled) {
if (!toggled) {
stopRecording();
} else if (!ensureManager()) {
state->pushToTalkToggles.fire(false);
pushToTalkWrap->hide(anim::type::instant);
return;
}
Core::App().settings().setGroupCallPushToTalk(toggled);
applyAndSave();
pushToTalkWrap->toggle(toggled, anim::type::normal);
}, pushToTalk->lifetime());
auto boxKeyFilter = [=](not_null<QEvent*> e) {
return (e->type() == QEvent::KeyPress && state->recording)
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
};
box->lifetime().make_state<base::unique_qptr<QObject>>(
base::install_event_filter(box, std::move(boxKeyFilter)));
}
Ui::AddSkip(layout);
//Ui::AddDivider(layout);
//Ui::AddSkip(layout);
}
auto shareLink = Fn<void()>();
if (peer->isChannel()
&& peer->asChannel()->hasUsername()
&& goodReal) {
const auto showBox = crl::guard(box, [=](
object_ptr<Ui::BoxContent> next) {
box->getDelegate()->show(std::move(next));
});
const auto showToast = crl::guard(box, [=](QString text) {
box->showToast(text);
});
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
peer,
box->uiShow());
shareLink = std::move(shareLinkCallback);
box->lifetime().add(std::move(shareLinkLifetime));
} else {
const auto lookupLink = [=] {
if (const auto group = peer->asMegagroup()) {
return group->hasUsername()
? group->session().createInternalLinkFull(
group->username())
: group->inviteLink();
} else if (const auto chat = peer->asChat()) {
return chat->inviteLink();
}
return QString();
};
const auto canCreateLink = [&] {
if (const auto chat = peer->asChat()) {
return chat->canHaveInviteLink();
} else if (const auto group = peer->asMegagroup()) {
return group->canHaveInviteLink();
}
return false;
};
const auto alreadyHasLink = !lookupLink().isEmpty();
if (alreadyHasLink || canCreateLink()) {
if (!alreadyHasLink) {
// Request invite link.
peer->session().api().requestFullPeer(peer);
}
const auto copyLink = [=] {
const auto link = lookupLink();
if (link.isEmpty()) {
return false;
}
QGuiApplication::clipboard()->setText(link);
if (weakBox) {
box->showToast(
tr::lng_create_channel_link_copied(tr::now));
}
return true;
};
shareLink = [=] {
if (!copyLink() && !state->generatingLink) {
state->generatingLink = true;
peer->session().api().inviteLinks().create({
peer,
crl::guard(layout, [=](auto&&) { copyLink(); })
});
}
};
}
}
if (shareLink) {
layout->add(object_ptr<Ui::SettingsButton>(
layout,
tr::lng_group_call_share(),
st::groupCallSettingsButton
))->addClickHandler(std::move(shareLink));
}
if (rtmp && !call->rtmpInfo().url.isEmpty()) {
Ui::AddSkip(layout);
addDivider();
Ui::AddSkip(layout);
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
mtpRequestId requestId;
rpl::event_stream<RtmpInfo> data;
};
const auto top = box->addTopButton(st::groupCallMenuToggle);
const auto state = top->lifetime().make_state<State>();
const auto revokeSure = [=] {
const auto session = &peer->session();
state->requestId = session->api().request(
MTPphone_GetGroupCallStreamRtmpUrl(
MTP_flags(0),
peer->input(),
MTP_bool(true)
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
auto data = result.match([&](
const MTPDphone_groupCallStreamRtmpUrl &data) {
return RtmpInfo{
.url = qs(data.vurl()),
.key = qs(data.vkey()),
};
});
if (const auto call = weakCall.get()) {
call->setRtmpInfo(data);
}
if (!top) {
return;
}
state->requestId = 0;
state->data.fire(std::move(data));
}).fail([=] {
state->requestId = 0;
}).send();
};
const auto revoke = [=] {
if (state->requestId || !top) {
return;
}
box->getDelegate()->show(Ui::MakeConfirmBox({
.text = tr::lng_group_call_rtmp_revoke_sure(),
.confirmed = [=](Fn<void()> &&close) {
revokeSure();
close();
},
.confirmText = tr::lng_group_invite_context_revoke(),
.labelStyle = &st::groupCallBoxLabel,
}));
};
top->setClickedCallback([=] {
state->menu = base::make_unique_q<Ui::PopupMenu>(
box,
st::groupCallPopupMenu);
state->menu->addAction(
tr::lng_group_call_rtmp_revoke(tr::now),
revoke);
state->menu->setForcedOrigin(
Ui::PanelAnimation::Origin::TopRight);
top->setForceRippled(true);
const auto raw = state->menu.get();
raw->setDestroyedCallback([=] {
if ((state->menu == raw) && top) {
top->setForceRippled(false);
}
});
state->menu->popup(
top->mapToGlobal(QPoint(top->width() / 2, top->height())));
return true;
});
StartRtmpProcess::FillRtmpRows(
layout,
false,
box->uiShow(),
state->data.events(),
&st::groupCallBoxLabel,
&st::groupCallSettingsRtmpShowButton,
&st::groupCallSubsectionTitle,
&st::groupCallAttentionBoxButton,
&st::groupCallPopupMenu);
state->data.fire(call->rtmpInfo());
addDivider();
Ui::AddSkip(layout);
}
if (rtmp) {
const auto volumeItem = layout->add(
object_ptr<MenuVolumeItem>(
layout,
st::groupCallVolumeSettings,
st::groupCallVolumeSettingsSlider,
call->otherParticipantStateValue(
) | rpl::filter([=](const Group::ParticipantState &data) {
return data.peer == peer;
}),
call->rtmpVolume(),
Group::kMaxVolume,
false,
st::groupCallVolumeSettingsPadding));
const auto toggleMute = crl::guard(layout, [=](bool m, bool local) {
if (call) {
call->toggleMute({
.peer = peer,
.mute = m,
.locallyOnly = local,
});
}
});
const auto changeVolume = crl::guard(layout, [=](int v, bool local) {
if (call) {
call->changeVolume({
.peer = peer,
.volume = std::clamp(v, 1, Group::kMaxVolume),
.locallyOnly = local,
});
}
});
volumeItem->toggleMuteLocallyRequests(
) | rpl::on_next([=](bool muted) {
toggleMute(muted, true);
}, volumeItem->lifetime());
volumeItem->changeVolumeLocallyRequests(
) | rpl::on_next([=](int volume) {
changeVolume(volume, true);
}, volumeItem->lifetime());
}
if (call->canManage()) {
layout->add(object_ptr<Ui::SettingsButton>(
layout,
(peer->isBroadcast()
? tr::lng_group_call_end_channel()
: tr::lng_group_call_end()),
st::groupCallSettingsAttentionButton
))->addClickHandler([=] {
if (const auto call = weakCall.get()) {
box->getDelegate()->show(Box(
LeaveBox,
call,
true,
BoxContext::GroupCallPanel));
box->closeBox();
}
});
}
if (!rtmp) {
box->setShowFinishedCallback([=] {
// Means we finished showing the box.
crl::on_main(box, [=] {
state->deviceId = std::make_unique<Webrtc::DeviceResolver>(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Capture,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callCaptureDeviceIdValue(),
Core::App().settings().captureDeviceIdValue()));
state->micTester = std::make_unique<Webrtc::AudioInputTester>(
state->deviceId->value());
state->levelUpdateTimer.callEach(kMicTestUpdateInterval);
});
});
}
box->setTitle(tr::lng_group_call_settings_title());
box->boxClosing(
) | rpl::on_next([=] {
if (canChangeJoinMuted
&& muteJoined
&& muteJoined->toggled() != joinMuted) {
SaveCallJoinMuted(peer, id, muteJoined->toggled());
}
if (canChangeMessagesEnabled
&& enableMessages
&& enableMessages->toggled() != messagesEnabled) {
const auto value = enableMessages->toggled();
if (!call->conference()) {
SaveCallMessagesEnabled(peer, id, value);
} else if (const auto real = call->lookupReal()) {
real->setMessagesEnabledLocally(value);
}
}
}, box->lifetime());
box->addButton(tr::lng_box_done(), [=] {
box->closeBox();
});
}
std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show) {
auto lifetime = rpl::lifetime();
struct State {
State(not_null<Main::Session*> session) : session(session) {
}
~State() {
session->api().request(linkListenerRequestId).cancel();
session->api().request(linkSpeakerRequestId).cancel();
}
not_null<Main::Session*> session;
std::optional<QString> linkSpeaker;
QString linkListener;
mtpRequestId linkListenerRequestId = 0;
mtpRequestId linkSpeakerRequestId = 0;
bool generatingLink = false;
};
const auto state = lifetime.make_state<State>(&peer->session());
if (peer->isUser() || !peer->canManageGroupCall()) {
state->linkSpeaker = QString();
}
const auto shareReady = [=] {
if (!state->linkSpeaker.has_value()
|| state->linkListener.isEmpty()) {
return false;
}
show->showBox(ShareInviteLinkBox(
peer,
*state->linkSpeaker,
state->linkListener,
show));
return true;
};
auto callback = [=] {
const auto real = peer->migrateToOrMe()->groupCall();
if (shareReady() || state->generatingLink || !real) {
return;
}
state->generatingLink = true;
state->linkListenerRequestId = peer->session().api().request(
MTPphone_ExportGroupCallInvite(
MTP_flags(0),
real->input()
)
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
state->linkListenerRequestId = 0;
result.match([&](
const MTPDphone_exportedGroupCallInvite &data) {
state->linkListener = qs(data.vlink());
shareReady();
});
}).send();
if (real->rtmp()) {
state->linkSpeaker = QString();
state->linkSpeakerRequestId = 0;
shareReady();
} else if (!state->linkSpeaker.has_value()) {
using Flag = MTPphone_ExportGroupCallInvite::Flag;
state->linkSpeakerRequestId = peer->session().api().request(
MTPphone_ExportGroupCallInvite(
MTP_flags(Flag::f_can_self_unmute),
real->input())
).done([=](const MTPphone_ExportedGroupCallInvite &result) {
state->linkSpeakerRequestId = 0;
result.match([&](
const MTPDphone_exportedGroupCallInvite &data) {
state->linkSpeaker = qs(data.vlink());
shareReady();
});
}).fail([=] {
state->linkSpeakerRequestId = 0;
state->linkSpeaker = QString();
shareReady();
}).send();
}
};
return { std::move(callback), std::move(lifetime) };
}
MicLevelTester::MicLevelTester(Fn<void()> show)
: _show(std::move(show))
, _timer([=] { check(); })
, _deviceId(std::make_unique<Webrtc::DeviceResolver>(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Capture,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callCaptureDeviceIdValue(),
Core::App().settings().captureDeviceIdValue())))
, _tester(std::make_unique<Webrtc::AudioInputTester>(_deviceId->value())) {
_timer.callEach(kMicrophoneTooltipCheckInterval);
}
bool MicLevelTester::showTooltip() const {
return (_loudCount >= kMicrophoneTooltipAfterLoudCount);
}
void MicLevelTester::check() {
const auto level = _tester->getAndResetLevel();
if (level >= kMicrophoneTooltipLevelThreshold) {
_quietCount = 0;
if (++_loudCount >= kMicrophoneTooltipAfterLoudCount) {
_show();
}
} else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) {
_quietCount = 0;
_loudCount = 0;
}
}
} // namespace Calls::Group

View File

@@ -0,0 +1,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/layers/generic_box.h"
namespace Webrtc {
class AudioInputTester;
class DeviceResolver;
} // namespace Webrtc
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Calls::Group {
void SettingsBox(
not_null<Ui::GenericBox*> box,
not_null<GroupCall*> call);
[[nodiscard]] std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show);
class MicLevelTester final {
public:
explicit MicLevelTester(Fn<void()> show);
[[nodiscard]] bool showTooltip() const;
private:
void check();
Fn<void()> _show;
base::Timer _timer;
std::unique_ptr<Webrtc::DeviceResolver> _deviceId;
std::unique_ptr<Webrtc::AudioInputTester> _tester;
int _loudCount = 0;
int _quietCount = 0;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,147 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_stars_box.h"
#include "boxes/send_credits_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
#include "window/window_session_controller.h"
namespace Calls::Group {
namespace {
constexpr auto kMaxStarsFallback = 10'000;
constexpr auto kDefaultStars = 10;
} // namespace
int MaxVideoStreamStarsCount(not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return std::max(
appConfig->get<int>(
u"stars_groupcall_message_amount_max"_q,
kMaxStarsFallback),
2);
}
void VideoStreamStarsBox(
not_null<Ui::GenericBox*> box,
VideoStreamStarsBoxArgs &&args) {
args.show->session().credits().load();
const auto admin = args.admin;
const auto sending = args.sending;
auto submitText = [=](rpl::producer<int> amount) {
auto nice = std::move(amount) | rpl::map([=](int count) {
return Ui::CreditsEmojiSmall().append(
Lang::FormatCountDecimal(count));
});
return admin
? tr::lng_box_ok(tr::marked)
: (sending
? tr::lng_paid_reaction_button
: tr::lng_paid_comment_button)(
lt_stars,
std::move(nice),
tr::rich);
};
const auto &show = args.show;
const auto session = &show->session();
const auto max = std::max(args.min, MaxVideoStreamStarsCount(session));
const auto chosen = std::clamp(
args.current ? args.current : kDefaultStars,
args.min,
max);
auto top = std::vector<Ui::PaidReactionTop>();
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
const auto peer = entry.peer;
const auto name = peer
? peer->shortName()
: tr::lng_paid_react_anonymous(tr::now);
const auto open = [=] {
if (const auto controller = show->resolveWindow()) {
controller->showPeerInfo(peer);
}
};
top.push_back({
.name = name,
.photo = (peer
? Ui::MakeUserpicThumbnail(peer)
: Ui::MakeHiddenAuthorThumbnail()),
.barePeerId = peer ? uint64(peer->id.value) : 0,
.count = int(entry.count),
.click = peer ? open : Fn<void()>(),
.my = (entry.my == 1),
});
};
top.reserve(args.top.size() + 1);
for (const auto &entry : args.top) {
add(entry);
}
auto myAdded = base::flat_set<uint64>();
const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);
if (i != end(top)) {
myAdded.emplace(i->barePeerId);
}
const auto myCount = uint32((i != end(top)) ? i->count : 0);
const auto myAdd = [&](not_null<PeerData*> peer) {
const auto barePeerId = uint64(peer->id.value);
if (!myAdded.emplace(barePeerId).second) {
return;
}
add(Data::MessageReactionsTopPaid{
.peer = peer,
.count = myCount,
.my = true,
});
};
myAdd(session->user());
ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
const auto weak = base::make_weak(box);
Ui::PaidReactionsBox(box, {
.min = args.min,
.chosen = chosen,
.max = max,
.top = std::move(top),
.session = &show->session(),
.name = args.name,
.submit = std::move(submitText),
.colorings = show->session().appConfig().groupCallColorings(),
.balanceValue = session->credits().balanceValue(),
.send = [=, save = args.save](int count, uint64 barePeerId) {
if (!admin) {
save(count);
}
if (const auto strong = weak.get()) {
strong->closeBox();
}
},
.videoStreamChoosing = !sending,
.videoStreamSending = sending,
.videoStreamAdmin = admin,
.dark = true,
});
}
object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
VideoStreamStarsBoxArgs &&args) {
return Box(VideoStreamStarsBox, std::move(args));
}
} // namespace Calls::Group

View File

@@ -0,0 +1,54 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct MessageReactionsTopPaid;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class BoxContent;
class GenericBox;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring;
} // namespace Calls::Group::Ui
namespace Calls::Group {
[[nodiscard]] int MaxVideoStreamStarsCount(not_null<Main::Session*> session);
struct VideoStreamStarsBoxArgs {
std::shared_ptr<ChatHelpers::Show> show;
std::vector<Data::MessageReactionsTopPaid> top;
int min = 0;
int current = 0;
bool sending = false;
bool admin = false;
Fn<void(int)> save;
QString name;
};
void VideoStreamStarsBox(
not_null<Ui::GenericBox*> box,
VideoStreamStarsBoxArgs &&args);
[[nodiscard]] object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
VideoStreamStarsBoxArgs &&args);
} // namespace Calls::Group

View File

@@ -0,0 +1,183 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_toasts.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_panel.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_peer.h"
#include "data/data_group_call.h"
#include "ui/layers/show.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "lang/lang_keys.h"
namespace Calls::Group {
namespace {
constexpr auto kErrorDuration = 2 * crl::time(1000);
using State = GroupCall::State;
} // namespace
Toasts::Toasts(not_null<Panel*> panel)
: _panel(panel)
, _call(panel->call()) {
setup();
}
void Toasts::setup() {
setupJoinAsChanged();
setupTitleChanged();
setupRequestedToSpeak();
setupAllowedToSpeak();
setupPinnedVideo();
setupError();
}
void Toasts::setupJoinAsChanged() {
_call->rejoinEvents(
) | rpl::filter([](RejoinEvent event) {
return (event.wasJoinAs != event.nowJoinAs);
}) | rpl::map([=] {
return _call->stateValue() | rpl::filter([](State state) {
return (state == State::Joined);
}) | rpl::take(1);
}) | rpl::flatten_latest() | rpl::on_next([=] {
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
? tr::lng_group_call_join_as_changed_channel
: tr::lng_group_call_join_as_changed)(
tr::now,
lt_name,
tr::bold(_call->joinAs()->name()),
tr::marked));
}, _lifetime);
}
void Toasts::setupTitleChanged() {
_call->titleChanged(
) | rpl::filter([=] {
return (_call->lookupReal() != nullptr);
}) | rpl::map([=] {
const auto peer = _call->peer();
return peer->groupCall()->title().isEmpty()
? peer->name()
: peer->groupCall()->title();
}) | rpl::on_next([=](const QString &title) {
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
? tr::lng_group_call_title_changed_channel
: tr::lng_group_call_title_changed)(
tr::now,
lt_title,
tr::bold(title),
tr::marked));
}, _lifetime);
}
void Toasts::setupAllowedToSpeak() {
_call->allowedToSpeakNotifications(
) | rpl::on_next([=] {
if (_panel->isActive()) {
_panel->uiShow()->showToast(
tr::lng_group_call_can_speak_here(tr::now));
} else {
const auto real = _call->lookupReal();
const auto name = (real && !real->title().isEmpty())
? real->title()
: _call->peer()->name();
Ui::Toast::Show({
.text = tr::lng_group_call_can_speak(
tr::now,
lt_chat,
tr::bold(name),
tr::marked),
});
}
}, _lifetime);
}
void Toasts::setupPinnedVideo() {
_call->videoEndpointPinnedValue(
) | rpl::map([=](bool pinned) {
return pinned
? _call->videoEndpointLargeValue()
: rpl::single(_call->videoEndpointLarge());
}) | rpl::flatten_latest(
) | rpl::filter([=] {
return (_call->shownVideoTracks().size() > 1);
}) | rpl::on_next([=](const VideoEndpoint &endpoint) {
const auto pinned = _call->videoEndpointPinned();
const auto peer = endpoint.peer;
if (!peer) {
return;
}
const auto text = [&] {
const auto me = (peer == _call->joinAs());
const auto camera = (endpoint.type == VideoEndpointType::Camera);
if (me) {
const auto key = camera
? (pinned
? tr::lng_group_call_pinned_camera_me
: tr::lng_group_call_unpinned_camera_me)
: (pinned
? tr::lng_group_call_pinned_screen_me
: tr::lng_group_call_unpinned_screen_me);
return key(tr::now);
}
const auto key = camera
? (pinned
? tr::lng_group_call_pinned_camera
: tr::lng_group_call_unpinned_camera)
: (pinned
? tr::lng_group_call_pinned_screen
: tr::lng_group_call_unpinned_screen);
return key(tr::now, lt_user, peer->shortName());
}();
_panel->uiShow()->showToast(text);
}, _lifetime);
}
void Toasts::setupRequestedToSpeak() {
_call->mutedValue(
) | rpl::combine_previous(
) | rpl::on_next([=](MuteState was, MuteState now) {
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
_panel->uiShow()->showToast(
tr::lng_group_call_tooltip_raised_hand(tr::now));
}
}, _lifetime);
}
void Toasts::setupError() {
_call->errors(
) | rpl::on_next([=](Error error) {
const auto key = [&] {
switch (error) {
case Error::NoCamera: return tr::lng_call_error_no_camera;
case Error::CameraFailed:
return tr::lng_group_call_failed_camera;
case Error::ScreenFailed:
return tr::lng_group_call_failed_screen;
case Error::MutedNoCamera:
return tr::lng_group_call_muted_no_camera;
case Error::MutedNoScreen:
return tr::lng_group_call_muted_no_screen;
case Error::DisabledNoCamera:
return tr::lng_group_call_chat_no_camera;
case Error::DisabledNoScreen:
return tr::lng_group_call_chat_no_screen;
}
Unexpected("Error in Calls::Group::Toasts::setupErrorToasts.");
}();
_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);
}, _lifetime);
}
} // namespace Calls::Group

View File

@@ -0,0 +1,38 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Calls::Group {
class Panel;
class Toasts final {
public:
explicit Toasts(not_null<Panel*> panel);
private:
void setup();
void setupJoinAsChanged();
void setupTitleChanged();
void setupRequestedToSpeak();
void setupAllowedToSpeak();
void setupPinnedVideo();
void setupError();
const not_null<Panel*> _panel;
const not_null<GroupCall*> _call;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,234 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
class Painter;
class QOpenGLFunctions;
namespace Ui {
class AbstractButton;
class RpWidgetWrap;
} // namespace Ui
namespace Ui::GL {
enum class Backend;
struct Capabilities;
struct ChosenRenderer;
class Renderer;
} // namespace Ui::GL
namespace Calls {
class GroupCall;
struct VideoEndpoint;
struct VideoQualityRequest;
} // namespace Calls
namespace Webrtc {
class VideoTrack;
} // namespace Webrtc
namespace Calls::Group {
class MembersRow;
enum class PanelMode;
enum class VideoQuality;
struct VideoTileTrack {
Webrtc::VideoTrack *track = nullptr;
MembersRow *row = nullptr;
rpl::variable<QSize> trackSize;
[[nodiscard]] explicit operator bool() const {
return track != nullptr;
}
};
[[nodiscard]] inline bool operator==(
VideoTileTrack a,
VideoTileTrack b) noexcept {
return (a.track == b.track) && (a.row == b.row);
}
[[nodiscard]] inline bool operator!=(
VideoTileTrack a,
VideoTileTrack b) noexcept {
return !(a == b);
}
class Viewport final {
public:
Viewport(
not_null<QWidget*> parent,
PanelMode mode,
Ui::GL::Backend backend,
Ui::RpWidgetWrap *borrowedRp = nullptr,
bool borrowedOpenGL = false);
~Viewport();
[[nodiscard]] not_null<QWidget*> widget() const;
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
void setMode(PanelMode mode, not_null<QWidget*> parent);
void setControlsShown(float64 shown);
void setCursorShown(bool shown);
void setGeometry(bool fullscreen, QRect geometry);
void resizeToWidth(int newWidth);
void setScrollTop(int scrollTop);
void add(
const VideoEndpoint &endpoint,
VideoTileTrack track,
rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned,
bool self);
void remove(const VideoEndpoint &endpoint);
void showLarge(const VideoEndpoint &endpoint);
[[nodiscard]] bool requireARGB32() const;
[[nodiscard]] int fullHeight() const;
[[nodiscard]] rpl::producer<int> fullHeightValue() const;
[[nodiscard]] rpl::producer<bool> pinToggled() const;
[[nodiscard]] rpl::producer<VideoEndpoint> clicks() const;
[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;
[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;
void ensureBorrowedRenderer(QOpenGLFunctions &f);
void ensureBorrowedCleared(QOpenGLFunctions *f);
void borrowedPaint(QOpenGLFunctions &f);
void ensureBorrowedRenderer();
void ensureBorrowedCleared();
void borrowedPaint(Painter &p, const QRegion &clip);
[[nodiscard]] QPoint borrowedOrigin() const;
[[nodiscard]] rpl::lifetime &lifetime();
static constexpr auto kShadowMaxAlpha = 80;
private:
struct Textures;
class VideoTile;
class RendererSW;
class RendererGL;
using TileId = quintptr;
struct Geometry {
VideoTile *tile = nullptr;
QSize size;
QRect rows;
QRect columns;
};
struct Layout {
std::vector<Geometry> list;
QSize outer;
bool useColumns = false;
};
struct TileAnimation {
QSize from;
QSize to;
float64 ratio = -1.;
};
struct Selection {
enum class Element {
None,
Tile,
PinButton,
BackButton,
};
VideoTile *tile = nullptr;
Element element = Element::None;
inline bool operator==(Selection other) const {
return (tile == other.tile) && (element == other.element);
}
};
void setup();
[[nodiscard]] bool wide() const;
[[nodiscard]] bool videoStream() const;
void updateCursor();
void updateTilesGeometry();
void updateTilesGeometry(int outerWidth);
void updateTilesGeometryWide(int outerWidth, int outerHeight);
void updateTilesGeometryNarrow(int outerWidth);
void updateTilesGeometryColumn(int outerWidth);
void setTileGeometry(not_null<VideoTile*> tile, QRect geometry);
void refreshHasTwoOrMore();
void updateTopControlsVisibility();
void prepareLargeChangeAnimation();
void startLargeChangeAnimation();
void updateTilesAnimated();
[[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const;
[[nodiscard]] Layout applyLarge(Layout layout) const;
void setSelected(Selection value);
void setPressed(Selection value);
void handleMousePress(QPoint position, Qt::MouseButton button);
void handleMouseRelease(QPoint position, Qt::MouseButton button);
void handleMouseMove(QPoint position);
void updateSelected(QPoint position);
void updateSelected();
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
Ui::GL::Backend backend);
[[nodiscard]] std::unique_ptr<Ui::GL::Renderer> makeRenderer();
void updateMyWidgetPart();
PanelMode _mode = PanelMode();
bool _opengl = false;
const std::unique_ptr<Ui::RpWidgetWrap> _content;
std::vector<std::unique_ptr<VideoTile>> _tiles;
std::vector<not_null<VideoTile*>> _tilesForOrder;
rpl::variable<int> _fullHeight = 0;
bool _hasTwoOrMore = false;
bool _fullscreen = false;
bool _cursorHidden = false;
int _scrollTop = 0;
QImage _shadow;
rpl::event_stream<VideoEndpoint> _clicks;
rpl::event_stream<bool> _pinToggles;
rpl::event_stream<VideoQualityRequest> _qualityRequests;
float64 _controlsShownRatio = 1.;
VideoTile *_large = nullptr;
Fn<void()> _updateLargeScheduled;
Ui::Animations::Simple _largeChangeAnimation;
Layout _startTilesLayout;
Layout _finishTilesLayout;
Selection _selected;
Selection _pressed;
rpl::variable<bool> _mouseInside = false;
Ui::RpWidgetWrap * const _borrowed = nullptr;
QRect _borrowedGeometry;
std::unique_ptr<Ui::GL::Renderer> _borrowedRenderer;
QMetaObject::Connection _borrowedConnection;
rpl::lifetime _lifetime;
};
[[nodiscard]] QImage GenerateShadow(
int height,
int topAlpha,
int bottomAlpha,
QColor color = QColor(0, 0, 0));
[[nodiscard]] rpl::producer<QString> MuteButtonTooltip(
not_null<GroupCall*> call);
} // namespace Calls::Group

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "calls/group/calls_group_viewport.h"
#include "ui/round_rect.h"
#include "ui/effects/animations.h"
#include "ui/effects/cross_line.h"
#include "ui/gl/gl_primitives.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/gl_image.h"
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
namespace Webrtc {
struct FrameWithInfo;
} // namespace Webrtc
namespace Calls::Group {
class Viewport::RendererGL final : public Ui::GL::Renderer {
public:
explicit RendererGL(not_null<Viewport*> owner);
void init(QOpenGLFunctions &f) override;
void deinit(QOpenGLFunctions *f) override;
void paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f) override;
std::optional<QColor> clearColor() override;
private:
struct TileData {
quintptr id = 0;
not_null<PeerData*> peer;
Ui::GL::Textures<5> textures;
Ui::GL::Framebuffers<2> framebuffers;
Ui::Animations::Simple outlined;
Ui::Animations::Simple paused;
QImage userpicFrame;
QRect nameRect;
int nameVersion = 0;
mutable int trackIndex = -1;
mutable QSize rgbaSize;
mutable QSize textureSize;
mutable QSize textureChromaSize;
mutable QSize textureBlurSize;
bool stale = false;
bool pause = false;
bool outline = false;
};
struct Program {
std::optional<QOpenGLShaderProgram> argb32;
std::optional<QOpenGLShaderProgram> yuv420;
};
void setDefaultViewport(QOpenGLFunctions &f);
void paintTile(
QOpenGLFunctions &f,
GLuint defaultFramebufferObject,
not_null<VideoTile*> tile,
TileData &nameData);
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
[[nodiscard]] Ui::GL::Rect transformRect(
const Ui::GL::Rect &raster) const;
void ensureARGB32Program();
void ensureButtonsImage();
void prepareObjects(
QOpenGLFunctions &f,
TileData &tileData,
QSize blurSize);
void bindFrame(
QOpenGLFunctions &f,
const Webrtc::FrameWithInfo &data,
TileData &tileData,
Program &program);
void drawDownscalePass(
QOpenGLFunctions &f,
TileData &tileData);
void drawFirstBlurPass(
QOpenGLFunctions &f,
TileData &tileData,
QSize blurSize);
void validateDatas();
void validateNoiseTexture(
QOpenGLFunctions &f,
GLuint defaultFramebufferObject);
void validateOutlineAnimation(
not_null<VideoTile*> tile,
TileData &data);
void validatePausedAnimation(
not_null<VideoTile*> tile,
TileData &data);
void validateUserpicFrame(
not_null<VideoTile*> tile,
TileData &tileData);
void uploadTexture(
QOpenGLFunctions &f,
GLint internalformat,
GLint format,
QSize size,
QSize hasSize,
int stride,
const void *data) const;
[[nodiscard]] bool isExpanded(
not_null<VideoTile*> tile,
QSize unscaled,
QSize tileSize) const;
[[nodiscard]] float64 countExpandRatio(
not_null<VideoTile*> tile,
QSize unscaled,
const TileAnimation &animation) const;
const not_null<Viewport*> _owner;
GLfloat _factor = 1.;
int _ifactor = 1;
QSize _viewport;
bool _rgbaFrame = false;
bool _userpicFrame;
std::optional<QOpenGLBuffer> _frameBuffer;
Program _downscaleProgram;
std::optional<QOpenGLShaderProgram> _blurProgram;
Program _frameProgram;
std::optional<QOpenGLShaderProgram> _imageProgram;
Ui::GL::Textures<1> _noiseTexture;
Ui::GL::Framebuffers<1> _noiseFramebuffer;
QOpenGLShader *_downscaleVertexShader = nullptr;
QOpenGLShader *_frameVertexShader = nullptr;
Ui::GL::Image _buttons;
QRect _pinOn;
QRect _pinOff;
QRect _back;
QRect _muteOn;
QRect _muteOff;
QRect _paused;
Ui::GL::Image _names;
QRect _pausedTextRect;
std::vector<TileData> _tileData;
std::vector<int> _tileDataIndices;
Ui::CrossLineAnimation _pinIcon;
Ui::CrossLineAnimation _muteIcon;
Ui::RoundRect _pinBackground;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,347 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_viewport_raster.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_viewport_tile.h"
#include "calls/group/calls_group_members_row.h"
#include "data/data_peer.h"
#include "media/view/media_view_pip.h"
#include "webrtc/webrtc_video_track.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_calls.h"
#include "styles/palette.h"
namespace Calls::Group {
namespace {
constexpr auto kBlurRadius = 15;
} // namespace
Viewport::RendererSW::RendererSW(not_null<Viewport*> owner)
: _owner(owner)
, _pinIcon(st::groupCallVideoTile.pin)
, _pinBackground(
(st::groupCallVideoTile.pinPadding.top()
+ st::groupCallVideoTile.pin.icon.height()
+ st::groupCallVideoTile.pinPadding.bottom()) / 2,
st::radialBg) {
}
void Viewport::RendererSW::paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) {
auto bg = clip;
auto hq = PainterHighQualityEnabler(p);
const auto bounding = clip.boundingRect();
for (auto &[tile, tileData] : _tileData) {
tileData.stale = true;
}
for (const auto &tile : _owner->_tiles) {
if (!tile->visible()) {
continue;
}
paintTile(p, tile.get(), bounding, bg);
}
if (_owner->borrowedOrigin().isNull()) {
const auto fullscreen = _owner->_fullscreen;
const auto color = fullscreen
? QColor(0, 0, 0)
: st::groupCallBg->c;
for (const auto &rect : bg) {
p.fillRect(rect, color);
}
}
for (auto i = _tileData.begin(); i != _tileData.end();) {
if (i->second.stale) {
i = _tileData.erase(i);
} else {
++i;
}
}
}
void Viewport::RendererSW::validateUserpicFrame(
not_null<VideoTile*> tile,
TileData &data) {
if (!_userpicFrame) {
data.userpicFrame = QImage();
return;
} else if (!data.userpicFrame.isNull()) {
return;
}
const auto size = tile->trackOrUserpicSize();
data.userpicFrame = Images::BlurLargeImage(
PeerData::GenerateUserpicImage(
tile->peer(),
tile->row()->ensureUserpicView(),
size.width(),
0),
kBlurRadius);
}
void Viewport::RendererSW::paintTile(
Painter &p,
not_null<VideoTile*> tile,
const QRect &clip,
QRegion &bg) {
const auto track = tile->track();
const auto markGuard = gsl::finally([&] {
tile->track()->markFrameShown();
});
const auto data = track->frameWithInfo(true);
auto &tileData = _tileData[tile];
tileData.stale = false;
_userpicFrame = (data.format == Webrtc::FrameFormat::None);
_pausedFrame = (track->state() == Webrtc::VideoState::Paused);
validateUserpicFrame(tile, tileData);
if (_userpicFrame || !_pausedFrame) {
tileData.blurredFrame = QImage();
} else if (tileData.blurredFrame.isNull()) {
tileData.blurredFrame = Images::BlurLargeImage(
data.original.scaled(
VideoTile::PausedVideoSize(),
Qt::KeepAspectRatio).mirrored(tile->mirror(), false),
kBlurRadius);
}
const auto &image = _userpicFrame
? tileData.userpicFrame
: _pausedFrame
? tileData.blurredFrame
: data.original.mirrored(tile->mirror(), false);
const auto frameRotation = _userpicFrame ? 0 : data.rotation;
Assert(!image.isNull());
const auto background = _owner->_fullscreen
? QColor(0, 0, 0)
: st::groupCallMembersBg->c;
const auto fill = [&](QRect rect) {
const auto intersected = rect.intersected(clip);
if (!intersected.isEmpty()) {
p.fillRect(intersected, background);
bg -= intersected;
}
};
using namespace Media::View;
const auto geometry = tile->geometry().translated(
_owner->borrowedOrigin());
const auto x = geometry.x();
const auto y = geometry.y();
const auto width = geometry.width();
const auto height = geometry.height();
const auto scaled = FlipSizeByRotation(
image.size(),
frameRotation
).scaled(QSize(width, height), Qt::KeepAspectRatio);
const auto left = (width - scaled.width()) / 2;
const auto top = (height - scaled.height()) / 2;
const auto target = QRect(QPoint(x + left, y + top), scaled);
if (UsePainterRotation(frameRotation)) {
if (frameRotation) {
p.save();
p.rotate(frameRotation);
}
p.drawImage(RotatedRect(target, frameRotation), image);
if (frameRotation) {
p.restore();
}
} else if (frameRotation) {
p.drawImage(target, RotateFrameImage(image, frameRotation));
} else {
p.drawImage(target, image);
}
bg -= target;
if (left > 0) {
fill({ x, y, left, height });
}
if (const auto right = left + scaled.width(); right < width) {
fill({ x + right, y, width - right, height });
}
if (top > 0) {
fill({ x, y, width, top });
}
if (const auto bottom = top + scaled.height(); bottom < height) {
fill({ x, y + bottom, width, height - bottom });
}
paintTileControls(p, x, y, width, height, tile);
paintTileOutline(p, x, y, width, height, tile);
}
void Viewport::RendererSW::paintTileOutline(
Painter &p,
int x,
int y,
int width,
int height,
not_null<VideoTile*> tile) {
if (!tile->row()->speaking()) {
return;
}
const auto outline = st::groupCallOutline;
const auto &color = st::groupCallMemberActiveIcon;
p.setPen(Qt::NoPen);
p.fillRect(x, y, outline, height - outline, color);
p.fillRect(x + outline, y, width - outline, outline, color);
p.fillRect(
x + width - outline,
y + outline,
outline,
height - outline,
color);
p.fillRect(x, y + height - outline, width - outline, outline, color);
}
void Viewport::RendererSW::paintTileControls(
Painter &p,
int x,
int y,
int width,
int height,
not_null<VideoTile*> tile) {
p.setClipRect(x, y, width, height);
const auto guard = gsl::finally([&] { p.setClipping(false); });
const auto wide = _owner->wide();
if (wide) {
// Pin.
const auto pinInner = tile->pinInner();
VideoTile::PaintPinButton(
p,
tile->pinned(),
x + pinInner.x(),
y + pinInner.y(),
_owner->widget()->width(),
&_pinBackground,
&_pinIcon);
// Back.
const auto backInner = tile->backInner();
VideoTile::PaintBackButton(
p,
x + backInner.x(),
y + backInner.y(),
_owner->widget()->width(),
&_pinBackground);
}
const auto &st = st::groupCallVideoTile;
const auto nameTop = y + (height
- st.namePosition.y()
- st::semiboldFont->height);
if (_pausedFrame) {
p.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha));
const auto middle = (st::groupCallVideoPlaceholderHeight
- st::groupCallPaused.height()) / 2;
const auto pausedSpace = (nameTop - y)
- st::groupCallPaused.height()
- st::semiboldFont->height;
const auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop;
const auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop
- st::groupCallVideoPlaceholderIconTop;
const auto pauseIconTop = !_owner->wide()
? (y + (height - st::groupCallPaused.height()) / 2)
: (pausedSpace < 3 * st::semiboldFont->height)
? (pausedSpace / 3)
: std::min(
y + (height / 2) - pauseIconSkip,
(nameTop
- st::semiboldFont->height * 3
- st::groupCallPaused.height()));
const auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height)
? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)
: std::min(
pauseIconTop + pauseTextSkip,
nameTop - st::semiboldFont->height * 2);
st::groupCallPaused.paint(
p,
x + (width - st::groupCallPaused.width()) / 2,
pauseIconTop,
width);
if (_owner->wide()) {
p.drawText(
QRect(x, pauseTextTop, width, y + height - pauseTextTop),
tr::lng_group_call_video_paused(tr::now),
style::al_top);
}
}
const auto shown = _owner->_controlsShownRatio;
if (shown == 0.) {
return;
}
const auto fullShift = st.namePosition.y() + st::normalFont->height;
const auto shift = anim::interpolate(fullShift, 0, shown);
// Shadow.
if (_shadow.isNull()) {
_shadow = Images::GenerateShadow(
st.shadowHeight,
0,
kShadowMaxAlpha);
}
const auto shadowRect = QRect(
x,
y + (height - anim::interpolate(0, st.shadowHeight, shown)),
width,
st.shadowHeight);
const auto shadowFill = shadowRect.intersected({ x, y, width, height });
if (shadowFill.isEmpty()) {
return;
}
const auto factor = style::DevicePixelRatio();
if (!_pausedFrame) {
p.drawImage(
shadowFill,
_shadow,
QRect(
0,
(shadowFill.y() - shadowRect.y()) * factor,
_shadow.width(),
shadowFill.height() * factor));
}
const auto row = tile->row();
row->lazyInitialize(st::groupCallMembersListItem);
// Mute.
const auto &icon = st::groupCallVideoCrossLine.icon;
const auto iconLeft = x + width - st.iconPosition.x() - icon.width();
const auto iconTop = y + (height
- st.iconPosition.y()
- icon.height()
+ shift);
row->paintMuteIcon(
p,
{ iconLeft, iconTop, icon.width(), icon.height() },
MembersRowStyle::Video);
// Name.
p.setPen(st::groupCallVideoTextFg);
const auto hasWidth = width
- st.iconPosition.x() - icon.width()
- st.namePosition.x();
const auto nameLeft = x + st.namePosition.x();
row->name().drawLeftElided(
p,
nameLeft,
nameTop + shift,
hasWidth,
width);
}
} // namespace Calls::Group

View File

@@ -0,0 +1,67 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "calls/group/calls_group_viewport.h"
#include "ui/round_rect.h"
#include "ui/effects/cross_line.h"
#include "ui/gl/gl_surface.h"
#include "ui/text/text.h"
namespace Calls::Group {
class Viewport::RendererSW final : public Ui::GL::Renderer {
public:
explicit RendererSW(not_null<Viewport*> owner);
void paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) override;
private:
struct TileData {
QImage userpicFrame;
QImage blurredFrame;
bool stale = false;
};
void paintTile(
Painter &p,
not_null<VideoTile*> tile,
const QRect &clip,
QRegion &bg);
void paintTileOutline(
Painter &p,
int x,
int y,
int width,
int height,
not_null<VideoTile*> tile);
void paintTileControls(
Painter &p,
int x,
int y,
int width,
int height,
not_null<VideoTile*> tile);
void validateUserpicFrame(
not_null<VideoTile*> tile,
TileData &data);
const not_null<Viewport*> _owner;
QImage _shadow;
bool _userpicFrame = false;
bool _pausedFrame = false;
base::flat_map<not_null<VideoTile*>, TileData> _tileData;
Ui::CrossLineAnimation _pinIcon;
Ui::RoundRect _pinBackground;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,278 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_viewport_tile.h"
#include "calls/group/calls_group_members_row.h"
#include "webrtc/webrtc_video_track.h"
#include "lang/lang_keys.h"
#include "ui/round_rect.h"
#include "ui/painter.h"
#include "ui/effects/cross_line.h"
#include "styles/style_calls.h"
#include <QtGui/QOpenGLFunctions>
namespace Calls::Group {
namespace {
constexpr auto kPausedVideoSize = 90;
} // namespace
Viewport::VideoTile::VideoTile(
const VideoEndpoint &endpoint,
VideoTileTrack track,
rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned,
Fn<void()> update,
bool self)
: _endpoint(endpoint)
, _update(std::move(update))
, _track(std::move(track))
, _peer(_track.row->peer())
, _trackSize(std::move(trackSize))
, _rtmp(endpoint.rtmp())
, _self(self) {
Expects(_track.track != nullptr);
using namespace rpl::mappers;
_track.track->stateValue(
) | rpl::filter(
_1 == Webrtc::VideoState::Paused
) | rpl::take(1) | rpl::on_next([=] {
_wasPaused = true;
}, _lifetime);
setup(std::move(pinned));
}
bool Viewport::VideoTile::mirror() const {
return _self && (_endpoint.type == VideoEndpointType::Camera);
}
QRect Viewport::VideoTile::pinOuter() const {
return _pinOuter;
}
QRect Viewport::VideoTile::pinInner() const {
return _pinInner.translated(0, -topControlsSlide());
}
QRect Viewport::VideoTile::backOuter() const {
return _backOuter;
}
QRect Viewport::VideoTile::backInner() const {
return _backInner.translated(0, -topControlsSlide());
}
int Viewport::VideoTile::topControlsSlide() const {
return anim::interpolate(
st::groupCallVideoTile.pinPosition.y() + _pinInner.height(),
0,
_topControlsShownAnimation.value(_topControlsShown ? 1. : 0.));
}
QSize Viewport::VideoTile::PausedVideoSize() {
return QSize(kPausedVideoSize, kPausedVideoSize);
}
QSize Viewport::VideoTile::trackOrUserpicSize() const {
if (const auto size = trackSize(); !size.isEmpty()) {
return size;
}
return _wasPaused ? PausedVideoSize() : QSize();
}
bool Viewport::VideoTile::screencast() const {
return (_endpoint.type == VideoEndpointType::Screen);
}
void Viewport::VideoTile::setGeometry(
QRect geometry,
TileAnimation animation) {
_hidden = false;
_geometry = geometry;
_animation = animation;
updateTopControlsPosition();
}
void Viewport::VideoTile::hide() {
_hidden = true;
_quality = std::nullopt;
}
void Viewport::VideoTile::toggleTopControlsShown(bool shown) {
if (_topControlsShown == shown) {
return;
}
_topControlsShown = shown;
_topControlsShownAnimation.start(
_update,
shown ? 0. : 1.,
shown ? 1. : 0.,
st::slideWrapDuration);
}
bool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) {
if (_hidden) {
_quality = std::nullopt;
return false;
} else if (_quality && *_quality == quality) {
return false;
}
_quality = quality;
return true;
}
QSize Viewport::VideoTile::PinInnerSize(bool pinned) {
const auto &st = st::groupCallVideoTile;
const auto &icon = st::groupCallVideoTile.pin.icon;
const auto innerWidth = icon.width()
+ st.pinTextPosition.x()
+ st::semiboldFont->width(pinned
? tr::lng_pinned_unpin(tr::now)
: tr::lng_pinned_pin(tr::now));
const auto innerHeight = icon.height();
const auto buttonWidth = st.pinPadding.left()
+ innerWidth
+ st.pinPadding.right();
const auto buttonHeight = st.pinPadding.top()
+ innerHeight
+ st.pinPadding.bottom();
return { buttonWidth, buttonHeight };
}
void Viewport::VideoTile::PaintPinButton(
Painter &p,
bool pinned,
int x,
int y,
int outerWidth,
not_null<Ui::RoundRect*> background,
not_null<Ui::CrossLineAnimation*> icon) {
const auto &st = st::groupCallVideoTile;
const auto rect = QRect(QPoint(x, y), PinInnerSize(pinned));
background->paint(p, rect);
icon->paint(
p,
rect.marginsRemoved(st.pinPadding).topLeft(),
pinned ? 1. : 0.);
p.setPen(st::groupCallVideoTextFg);
p.setFont(st::semiboldFont);
p.drawTextLeft(
(x
+ st.pinPadding.left()
+ st::groupCallVideoTile.pin.icon.width()
+ st.pinTextPosition.x()),
(y
+ st.pinPadding.top()
+ st.pinTextPosition.y()),
outerWidth,
(pinned
? tr::lng_pinned_unpin(tr::now)
: tr::lng_pinned_pin(tr::now)));
}
QSize Viewport::VideoTile::BackInnerSize() {
const auto &st = st::groupCallVideoTile;
const auto &icon = st::groupCallVideoTile.back;
const auto innerWidth = icon.width()
+ st.pinTextPosition.x()
+ st::semiboldFont->width(tr::lng_create_group_back(tr::now));
const auto innerHeight = icon.height();
const auto buttonWidth = st.pinPadding.left()
+ innerWidth
+ st.pinPadding.right();
const auto buttonHeight = st.pinPadding.top()
+ innerHeight
+ st.pinPadding.bottom();
return { buttonWidth, buttonHeight };
}
void Viewport::VideoTile::PaintBackButton(
Painter &p,
int x,
int y,
int outerWidth,
not_null<Ui::RoundRect*> background) {
const auto &st = st::groupCallVideoTile;
const auto rect = QRect(QPoint(x, y), BackInnerSize());
background->paint(p, rect);
st.back.paint(
p,
rect.marginsRemoved(st.pinPadding).topLeft(),
outerWidth);
p.setPen(st::groupCallVideoTextFg);
p.setFont(st::semiboldFont);
p.drawTextLeft(
(x
+ st.pinPadding.left()
+ st::groupCallVideoTile.pin.icon.width()
+ st.pinTextPosition.x()),
(y
+ st.pinPadding.top()
+ st.pinTextPosition.y()),
outerWidth,
tr::lng_create_group_back(tr::now));
}
void Viewport::VideoTile::updateTopControlsSize() {
const auto &st = st::groupCallVideoTile;
const auto pinSize = PinInnerSize(_pinned);
const auto pinWidth = st.pinPosition.x() * 2 + pinSize.width();
const auto pinHeight = st.pinPosition.y() * 2 + pinSize.height();
_pinInner = QRect(QPoint(), pinSize);
_pinOuter = QRect(0, 0, pinWidth, pinHeight);
const auto backSize = BackInnerSize();
const auto backWidth = st.pinPosition.x() * 2 + backSize.width();
const auto backHeight = st.pinPosition.y() * 2 + backSize.height();
_backInner = QRect(QPoint(), backSize);
_backOuter = QRect(0, 0, backWidth, backHeight);
}
void Viewport::VideoTile::updateTopControlsPosition() {
const auto &st = st::groupCallVideoTile;
_pinInner = QRect(
_geometry.width() - st.pinPosition.x() - _pinInner.width(),
st.pinPosition.y(),
_pinInner.width(),
_pinInner.height());
_pinOuter = QRect(
_geometry.width() - _pinOuter.width(),
0,
_pinOuter.width(),
_pinOuter.height());
_backInner = QRect(st.pinPosition, _backInner.size());
}
void Viewport::VideoTile::setup(rpl::producer<bool> pinned) {
std::move(
pinned
) | rpl::filter([=](bool pinned) {
return (_pinned != pinned);
}) | rpl::on_next([=](bool pinned) {
_pinned = pinned;
updateTopControlsSize();
if (!_hidden) {
updateTopControlsPosition();
_update();
}
}, _lifetime);
_track.track->renderNextFrame(
) | rpl::on_next(_update, _lifetime);
updateTopControlsSize();
}
} // namespace Calls::Group

View File

@@ -0,0 +1,142 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "calls/group/calls_group_viewport.h"
#include "calls/group/calls_group_call.h"
#include "ui/effects/animations.h"
class Painter;
class QOpenGLFunctions;
namespace Ui {
class CrossLineAnimation;
class RoundRect;
} // namespace Ui
namespace Calls::Group {
class Viewport::VideoTile final {
public:
VideoTile(
const VideoEndpoint &endpoint,
VideoTileTrack track,
rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned,
Fn<void()> update,
bool self);
[[nodiscard]] not_null<Webrtc::VideoTrack*> track() const {
return _track.track;
}
[[nodiscard]] not_null<MembersRow*> row() const {
return _track.row;
}
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
[[nodiscard]] bool rtmp() const {
return _rtmp;
}
[[nodiscard]] QRect geometry() const {
return _geometry;
}
[[nodiscard]] TileAnimation animation() const {
return _animation;
}
[[nodiscard]] bool pinned() const {
return _pinned;
}
[[nodiscard]] bool hidden() const {
return _hidden;
}
[[nodiscard]] bool visible() const {
return !_hidden && !_geometry.isEmpty();
}
[[nodiscard]] bool self() const {
return _self;
}
[[nodiscard]] bool mirror() const;
[[nodiscard]] QRect pinOuter() const;
[[nodiscard]] QRect pinInner() const;
[[nodiscard]] QRect backOuter() const;
[[nodiscard]] QRect backInner() const;
[[nodiscard]] const VideoEndpoint &endpoint() const {
return _endpoint;
}
[[nodiscard]] QSize trackSize() const {
return _trackSize.current();
}
[[nodiscard]] rpl::producer<QSize> trackSizeValue() const {
return _trackSize.value();
}
[[nodiscard]] QSize trackOrUserpicSize() const;
[[nodiscard]] static QSize PausedVideoSize();
[[nodiscard]] bool screencast() const;
void setGeometry(
QRect geometry,
TileAnimation animation = TileAnimation());
void hide();
void toggleTopControlsShown(bool shown);
bool updateRequestedQuality(VideoQuality quality);
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
[[nodiscard]] static QSize PinInnerSize(bool pinned);
static void PaintPinButton(
Painter &p,
bool pinned,
int x,
int y,
int outerWidth,
not_null<Ui::RoundRect*> background,
not_null<Ui::CrossLineAnimation*> icon);
[[nodiscard]] static QSize BackInnerSize();
static void PaintBackButton(
Painter &p,
int x,
int y,
int outerWidth,
not_null<Ui::RoundRect*> background);
private:
void setup(rpl::producer<bool> pinned);
[[nodiscard]] int topControlsSlide() const;
void updateTopControlsSize();
void updateTopControlsPosition();
const VideoEndpoint _endpoint;
const Fn<void()> _update;
const VideoTileTrack _track;
const not_null<PeerData*> _peer;
QRect _geometry;
TileAnimation _animation;
rpl::variable<QSize> _trackSize;
QRect _pinOuter;
QRect _pinInner;
QRect _backOuter;
QRect _backInner;
Ui::Animations::Simple _topControlsShownAnimation;
bool _wasPaused = false;
bool _topControlsShown = false;
bool _pinned = false;
bool _hidden = true;
bool _rtmp = false;
bool _self = false;
std::optional<VideoQuality> _quality;
rpl::lifetime _lifetime;
};
} // namespace Calls::Group

View File

@@ -0,0 +1,301 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_volume_item.h"
#include "calls/group/calls_group_common.h"
#include "ui/color_int_conversion.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/cross_line.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/rect.h"
#include "styles/style_calls.h"
#include "ui/paint/arcs.h"
namespace Calls {
namespace {
constexpr auto kMaxVolumePercent = 200;
const auto kSpeakerThreshold = std::vector<float>{
10.0f / kMaxVolumePercent,
50.0f / kMaxVolumePercent,
150.0f / kMaxVolumePercent };
constexpr auto kVolumeStickedValues
= std::array<std::pair<float64, float64>, 7>{{
{ 25. / kMaxVolumePercent, 2. / kMaxVolumePercent },
{ 50. / kMaxVolumePercent, 2. / kMaxVolumePercent },
{ 75. / kMaxVolumePercent, 2. / kMaxVolumePercent },
{ 100. / kMaxVolumePercent, 10. / kMaxVolumePercent },
{ 125. / kMaxVolumePercent, 2. / kMaxVolumePercent },
{ 150. / kMaxVolumePercent, 2. / kMaxVolumePercent },
{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },
}};
} // namespace
MenuVolumeItem::MenuVolumeItem(
not_null<RpWidget*> parent,
const style::Menu &st,
const style::MediaSlider &stSlider,
rpl::producer<Group::ParticipantState> participantState,
int startVolume,
int maxVolume,
bool muted,
const QMargins &padding)
: Ui::Menu::ItemBase(parent, st)
, _maxVolume(maxVolume)
, _cloudMuted(muted)
, _localMuted(muted)
, _slider(base::make_unique_q<Ui::MediaSlider>(this, stSlider))
, _dummyAction(new QAction(parent))
, _st(st)
, _stCross(st::groupCallMuteCrossLine)
, _padding(padding)
, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))
, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
st::groupCallSpeakerArcsAnimation,
kSpeakerThreshold,
_localMuted ? 0. : (startVolume / float(maxVolume)),
Ui::Paint::ArcsAnimation::Direction::Right)) {
initResizeHook(parent->sizeValue());
enableMouseSelecting();
enableMouseSelecting(_slider.get());
_slider->setAlwaysDisplayMarker(true);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
const auto geometry = QRect(QPoint(), size);
_itemRect = geometry - _padding;
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
_arcPosition = _speakerRect.center()
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
_slider->setGeometry(
st::groupCallMenuVolumeMargin.left(),
_speakerRect.y(),
(geometry.width()
- st::groupCallMenuVolumeMargin.left()
- st::groupCallMenuVolumeMargin.right()),
_speakerRect.height());
}, lifetime());
setCloudVolume(startVolume);
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(this);
const auto volume = _localMuted
? 0
: base::SafeRound(_slider->value() * kMaxVolumePercent);
const auto muteProgress
= _crossLineAnimation.value((!volume) ? 1. : 0.);
const auto selected = isSelected();
p.fillRect(clip, selected ? st.itemBgOver : st.itemBg);
const auto mutePen = anim::color(
unmuteColor(),
muteColor(),
muteProgress);
_crossLineMute->paint(
p,
_speakerRect.topLeft(),
muteProgress,
(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
{
p.translate(_arcPosition);
_arcs->paint(p);
}
}, lifetime());
_slider->setChangeProgressCallback([=](float64 value) {
const auto newMuted = (value == 0);
if (_localMuted != newMuted) {
_localMuted = newMuted;
_toggleMuteLocallyRequests.fire_copy(newMuted);
_crossLineAnimation.start(
[=] { update(_speakerRect); },
_localMuted ? 0. : 1.,
_localMuted ? 1. : 0.,
st::callPanelDuration);
}
if (value > 0) {
_changeVolumeLocallyRequests.fire(value * _maxVolume);
}
_arcs->setValue(value);
updateSliderColor(value);
});
const auto returnVolume = [=] {
_changeVolumeLocallyRequests.fire_copy(_cloudVolume);
};
_slider->setChangeFinishedCallback([=](float64 value) {
const auto newVolume = base::SafeRound(value * _maxVolume);
const auto muted = (value == 0);
if (!_cloudMuted && muted) {
returnVolume();
_localMuted = true;
_toggleMuteRequests.fire(true);
}
if (_cloudMuted && muted) {
returnVolume();
}
if (_cloudMuted && !muted) {
_waitingForUpdateVolume = true;
_localMuted = false;
_toggleMuteRequests.fire(false);
}
if (!_cloudMuted && !muted) {
_changeVolumeRequests.fire_copy(newVolume);
}
updateSliderColor(value);
});
std::move(
participantState
) | rpl::on_next([=](const Group::ParticipantState &state) {
const auto newMuted = state.mutedByMe;
const auto newVolume = state.volume.value_or(0);
_cloudMuted = _localMuted = newMuted;
if (!newVolume) {
return;
}
if (_waitingForUpdateVolume) {
const auto localVolume
= base::SafeRound(_slider->value() * _maxVolume);
if ((localVolume != newVolume)
&& (_cloudVolume == newVolume)) {
_changeVolumeRequests.fire(int(localVolume));
}
} else {
setCloudVolume(newVolume);
}
_waitingForUpdateVolume = false;
}, lifetime());
_slider->setAdjustCallback([=](float64 value) {
for (const auto &snap : kVolumeStickedValues) {
if (value > (snap.first - snap.second)
&& value < (snap.first + snap.second)) {
return snap.first;
}
}
return value;
});
initArcsAnimation();
}
void MenuVolumeItem::initArcsAnimation() {
const auto lastTime = lifetime().make_state<int>(0);
_arcsAnimation.init([=](crl::time now) {
_arcs->update(now);
update(_speakerRect);
});
_arcs->startUpdateRequests(
) | rpl::on_next([=] {
if (!_arcsAnimation.animating()) {
*lastTime = crl::now();
_arcsAnimation.start();
}
}, lifetime());
_arcs->stopUpdateRequests(
) | rpl::on_next([=] {
_arcsAnimation.stop();
}, lifetime());
}
QColor MenuVolumeItem::unmuteColor() const {
return (isSelected()
? _st.itemFgOver
: isEnabled()
? _st.itemFg
: _st.itemFgDisabled)->c;
}
QColor MenuVolumeItem::muteColor() const {
return (isSelected()
? st::attentionButtonFgOver
: st::attentionButtonFg)->c;
}
void MenuVolumeItem::setCloudVolume(int volume) {
if (_cloudVolume == volume) {
return;
}
_cloudVolume = volume;
if (!_slider->isChanging()) {
setSliderVolume(_cloudMuted ? 0. : volume);
}
}
void MenuVolumeItem::setSliderVolume(int volume) {
const auto value = float64(volume) / _maxVolume;
_slider->setValue(value);
updateSliderColor(value);
}
void MenuVolumeItem::updateSliderColor(float64 value) {
value = std::clamp(value, 0., 1.);
const auto colors = std::array<QColor, 4>{ {
Ui::ColorFromSerialized(0xF66464),
Ui::ColorFromSerialized(0xD0B738),
Ui::ColorFromSerialized(0x24CD80),
Ui::ColorFromSerialized(0x3BBCEC),
} };
_slider->setColorOverrides({
.activeFg = (value < 0.25)
? anim::color(colors[0], colors[1], value / 0.25)
: (value < 0.5)
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
});
}
not_null<QAction*> MenuVolumeItem::action() const {
return _dummyAction;
}
bool MenuVolumeItem::isEnabled() const {
return true;
}
int MenuVolumeItem::contentHeight() const {
return rect::m::sum::v(_padding) + _stCross.icon.height();
}
rpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {
return _toggleMuteRequests.events();
}
rpl::producer<bool> MenuVolumeItem::toggleMuteLocallyRequests() const {
return _toggleMuteLocallyRequests.events();
}
rpl::producer<int> MenuVolumeItem::changeVolumeRequests() const {
return _changeVolumeRequests.events();
}
rpl::producer<int> MenuVolumeItem::changeVolumeLocallyRequests() const {
return _changeVolumeLocallyRequests.events();
}
} // namespace Calls

View File

@@ -0,0 +1,90 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/widgets/menu/menu_item_base.h"
namespace Ui {
class CrossLineAnimation;
class MediaSlider;
namespace Paint {
class ArcsAnimation;
} // namespace Paint
} // namespace Ui
namespace Calls {
namespace Group {
struct MuteRequest;
struct VolumeRequest;
struct ParticipantState;
} // namespace Group
class MenuVolumeItem final : public Ui::Menu::ItemBase {
public:
MenuVolumeItem(
not_null<RpWidget*> parent,
const style::Menu &st,
const style::MediaSlider &stSlider,
rpl::producer<Group::ParticipantState> participantState,
int startVolume,
int maxVolume,
bool muted,
const QMargins &padding);
not_null<QAction*> action() const override;
bool isEnabled() const override;
[[nodiscard]] rpl::producer<bool> toggleMuteRequests() const;
[[nodiscard]] rpl::producer<bool> toggleMuteLocallyRequests() const;
[[nodiscard]] rpl::producer<int> changeVolumeRequests() const;
[[nodiscard]] rpl::producer<int> changeVolumeLocallyRequests() const;
protected:
int contentHeight() const override;
private:
void initArcsAnimation();
void setCloudVolume(int volume);
void setSliderVolume(int volume);
void updateSliderColor(float64 value);
QColor unmuteColor() const;
QColor muteColor() const;
const int _maxVolume;
int _cloudVolume = 0;
bool _waitingForUpdateVolume = false;
bool _cloudMuted = false;
bool _localMuted = false;
QRect _itemRect;
QRect _speakerRect;
QPoint _arcPosition;
const base::unique_qptr<Ui::MediaSlider> _slider;
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
const style::CrossLineAnimation &_stCross;
const QMargins &_padding;
const std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;
Ui::Animations::Simple _crossLineAnimation;
const std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;
Ui::Animations::Basic _arcsAnimation;
rpl::event_stream<bool> _toggleMuteRequests;
rpl::event_stream<bool> _toggleMuteLocallyRequests;
rpl::event_stream<int> _changeVolumeRequests;
rpl::event_stream<int> _changeVolumeLocallyRequests;
};
} // namespace Calls

View File

@@ -0,0 +1,370 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/ui/calls_group_recording_box.h"
#include "lang/lang_keys.h"
#include "ui/effects/animations.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "styles/style_basic.h"
#include "styles/style_calls.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QSvgRenderer>
namespace Calls::Group {
namespace {
constexpr auto kRoundRadius = 9;
constexpr auto kMaxGroupCallLength = 40;
constexpr auto kSwitchDuration = 200;
class GraphicButton final : public Ui::AbstractButton {
public:
GraphicButton(
not_null<Ui::RpWidget*> parent,
const QString &filename,
int selectWidth = 0);
void setToggled(bool value);
protected:
void paintEvent(QPaintEvent *e);
private:
const style::margins _margins;
QSvgRenderer _renderer;
Ui::RoundRect _roundRect;
Ui::RoundRect _roundRectSelect;
Ui::Animations::Simple _animation;
bool _toggled = false;
};
class RecordingInfo final : public Ui::RpWidget {
public:
RecordingInfo(not_null<Ui::RpWidget*> parent);
void prepareAudio();
void prepareVideo();
RecordingType type() const;
private:
void setLabel(const QString &text);
const object_ptr<Ui::VerticalLayout> _container;
RecordingType _type = RecordingType::AudioOnly;
};
class Switcher final : public Ui::RpWidget {
public:
Switcher(
not_null<Ui::RpWidget*> parent,
rpl::producer<bool> &&toggled);
RecordingType type() const;
private:
const object_ptr<Ui::BoxContentDivider> _background;
const object_ptr<RecordingInfo> _audio;
const object_ptr<RecordingInfo> _video;
bool _toggled = false;
Ui::Animations::Simple _animation;
};
GraphicButton::GraphicButton(
not_null<Ui::RpWidget*> parent,
const QString &filename,
int selectWidth)
: AbstractButton(parent)
, _margins(selectWidth, selectWidth, selectWidth, selectWidth)
, _renderer(u":/gui/recording/%1.svg"_q.arg(filename))
, _roundRect(kRoundRadius, st::groupCallMembersBg)
, _roundRectSelect(kRoundRadius, st::groupCallActiveFg) {
const auto size = style::ConvertScale(_renderer.defaultSize());
resize((QRect(QPoint(), size) + _margins).size());
}
void GraphicButton::setToggled(bool value) {
if (_toggled == value) {
return;
}
_toggled = value;
_animation.start(
[=] { update(); },
_toggled ? 0. : 1.,
_toggled ? 1. : 0.,
st::universalDuration);
}
void GraphicButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto progress = _animation.value(_toggled ? 1. : 0.);
p.setOpacity(progress);
_roundRectSelect.paint(p, rect());
p.setOpacity(1.);
const auto r = rect() - _margins;
_roundRect.paint(p, r);
_renderer.render(&p, r);
}
RecordingInfo::RecordingInfo(not_null<Ui::RpWidget*> parent)
: RpWidget(parent)
, _container(this) {
sizeValue(
) | rpl::on_next([=](const QSize &size) {
_container->resizeToWidth(size.width());
}, _container->lifetime());
}
void RecordingInfo::prepareAudio() {
_type = RecordingType::AudioOnly;
setLabel(tr::lng_group_call_recording_start_audio_subtitle(tr::now));
const auto wrap = _container->add(
object_ptr<Ui::RpWidget>(_container),
style::margins(0, st::groupCallRecordingAudioSkip, 0, 0));
const auto audioIcon = Ui::CreateChild<GraphicButton>(
wrap,
"info_audio");
wrap->resize(width(), audioIcon->height());
audioIcon->setAttribute(Qt::WA_TransparentForMouseEvents);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
audioIcon->moveToLeft((size.width() - audioIcon->width()) / 2, 0);
}, lifetime());
}
void RecordingInfo::prepareVideo() {
setLabel(tr::lng_group_call_recording_start_video_subtitle(tr::now));
const auto wrap = _container->add(
object_ptr<Ui::RpWidget>(_container),
style::margins());
const auto landscapeIcon = Ui::CreateChild<GraphicButton>(
wrap,
"info_video_landscape",
st::groupCallRecordingSelectWidth);
const auto portraitIcon = Ui::CreateChild<GraphicButton>(
wrap,
"info_video_portrait",
st::groupCallRecordingSelectWidth);
wrap->resize(width(), portraitIcon->height());
landscapeIcon->setToggled(true);
_type = RecordingType::VideoLandscape;
const auto icons = std::vector<GraphicButton*>{
landscapeIcon,
portraitIcon,
};
const auto types = std::map<GraphicButton*, RecordingType>{
{ landscapeIcon, RecordingType::VideoLandscape },
{ portraitIcon, RecordingType::VideoPortrait },
};
for (const auto icon : icons) {
icon->clicks(
) | rpl::on_next([=] {
for (const auto &i : icons) {
i->setToggled(icon == i);
}
_type = types.at(icon);
}, lifetime());
}
wrap->sizeValue(
) | rpl::on_next([=](const QSize &size) {
const auto wHalf = size.width() / icons.size();
for (auto i = 0; i < icons.size(); i++) {
const auto &icon = icons[i];
icon->moveToLeft(
wHalf * i + (wHalf - icon->width()) / 2,
(size.height() - icon->height()) / 2);
}
}, lifetime());
}
void RecordingInfo::setLabel(const QString &text) {
_container->add(
object_ptr<Ui::FlatLabel>(
_container,
text,
st::groupCallRecordingSubLabel),
st::groupCallRecordingSubLabelMargins,
style::al_top);
}
RecordingType RecordingInfo::type() const {
return _type;
}
Switcher::Switcher(
not_null<Ui::RpWidget*> parent,
rpl::producer<bool> &&toggled)
: RpWidget(parent)
, _background(
this,
st::groupCallRecordingInfoHeight,
st::groupCallDividerBar)
, _audio(this)
, _video(this) {
_audio->prepareAudio();
_video->prepareVideo();
resize(0, st::groupCallRecordingInfoHeight);
const auto updatePositions = [=](float64 progress) {
_audio->moveToLeft(-width() * progress, 0);
_video->moveToLeft(_audio->x() + _audio->width(), 0);
};
sizeValue(
) | rpl::on_next([=](const QSize &size) {
_audio->resize(size.width(), size.height());
_video->resize(size.width(), size.height());
updatePositions(_toggled ? 1. : 0.);
_background->lower();
_background->setGeometry(QRect(QPoint(), size));
}, lifetime());
std::move(
toggled
) | rpl::on_next([=](bool toggled) {
_toggled = toggled;
_animation.start(
updatePositions,
toggled ? 0. : 1.,
toggled ? 1. : 0.,
kSwitchDuration);
}, lifetime());
}
RecordingType Switcher::type() const {
return _toggled ? _video->type() : _audio->type();
}
} // namespace
void EditGroupCallTitleBox(
not_null<Ui::GenericBox*> box,
const QString &placeholder,
const QString &title,
bool livestream,
Fn<void(QString)> done) {
box->setTitle(livestream
? tr::lng_group_call_edit_title_channel()
: tr::lng_group_call_edit_title());
const auto input = box->addRow(object_ptr<Ui::InputField>(
box,
st::groupCallField,
rpl::single(placeholder),
title));
input->setMaxLength(kMaxGroupCallLength);
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto submit = [=] {
const auto result = input->getLastText().trimmed();
box->closeBox();
done(result);
};
input->submits() | rpl::on_next(submit, input->lifetime());
box->addButton(tr::lng_settings_save(), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void StartGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
Fn<void(RecordingType)> done) {
box->setTitle(tr::lng_group_call_recording_start());
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_group_call_recording_start_sure(),
st::groupCallBoxLabel));
const auto checkbox = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_group_call_recording_start_checkbox(),
false,
st::groupCallCheckbox),
style::margins(
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
const auto switcher = box->addRow(
object_ptr<Switcher>(box, checkbox->checkedChanges()),
st::groupCallRecordingInfoMargins);
box->addButton(tr::lng_continue(), [=] {
const auto type = switcher->type();
box->closeBox();
done(type);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void AddTitleGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
const QString &title,
Fn<void(QString)> done) {
box->setTitle(tr::lng_group_call_recording_start_title());
const auto input = box->addRow(object_ptr<Ui::InputField>(
box,
st::groupCallField,
tr::lng_group_call_recording_start_field(),
title));
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto submit = [=] {
const auto result = input->getLastText().trimmed();
box->closeBox();
done(result);
};
input->submits() | rpl::on_next(submit, input->lifetime());
box->addButton(tr::lng_group_call_recording_start_button(), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void StopGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
Fn<void(QString)> done) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_group_call_recording_stop_sure(),
st::groupCallBoxLabel),
style::margins(
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom()));
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
done(QString());
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Calls::Group

View File

@@ -0,0 +1,42 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Calls::Group {
enum class RecordingType {
AudioOnly,
VideoLandscape,
VideoPortrait,
};
void EditGroupCallTitleBox(
not_null<Ui::GenericBox*> box,
const QString &placeholder,
const QString &title,
bool livestream,
Fn<void(QString)> done);
void StartGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
Fn<void(RecordingType)> done);
void AddTitleGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
const QString &title,
Fn<void(QString)> done);
void StopGroupCallRecordingBox(
not_null<Ui::GenericBox*> box,
Fn<void(QString)> done);
} // namespace Calls::Group

View File

@@ -0,0 +1,139 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/ui/calls_group_scheduled_labels.h"
#include "ui/rp_widget.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "base/timer_rpl.h"
#include "styles/style_calls.h"
#include <QtCore/QDateTime>
#include <QtCore/QLocale>
namespace Calls::Group::Ui {
rpl::producer<QString> StartsWhenText(rpl::producer<TimeId> date) {
return std::move(
date
) | rpl::map([](TimeId date) -> rpl::producer<QString> {
const auto parsedDate = base::unixtime::parse(date);
const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0));
const auto previousDay = QDateTime(
parsedDate.date().addDays(-1),
QTime(0, 0));
const auto now = QDateTime::currentDateTime();
const auto kDay = int64(24 * 60 * 60);
const auto tillTomorrow = int64(now.secsTo(previousDay));
const auto tillToday = tillTomorrow + kDay;
const auto tillAfter = tillToday + kDay;
const auto time = QLocale().toString(
parsedDate.time(),
QLocale::ShortFormat);
auto exact = tr::lng_group_call_starts_short_date(
lt_date,
rpl::single(langDayOfMonthFull(dateDay.date())),
lt_time,
rpl::single(time)
) | rpl::type_erased;
auto tomorrow = tr::lng_group_call_starts_short_tomorrow(
lt_time,
rpl::single(time));
auto today = tr::lng_group_call_starts_short_today(
lt_time,
rpl::single(time));
auto todayAndAfter = rpl::single(
std::move(today)
) | rpl::then(base::timer_once(
std::min(tillAfter, kDay) * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(exact);
})) | rpl::flatten_latest() | rpl::type_erased;
auto tomorrowAndAfter = rpl::single(
std::move(tomorrow)
) | rpl::then(base::timer_once(
std::min(tillToday, kDay) * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(todayAndAfter);
})) | rpl::flatten_latest() | rpl::type_erased;
auto full = rpl::single(
rpl::duplicate(exact)
) | rpl::then(base::timer_once(
tillTomorrow * crl::time(1000)
) | rpl::map([=] {
return rpl::duplicate(tomorrowAndAfter);
})) | rpl::flatten_latest() | rpl::type_erased;
if (tillTomorrow > 0) {
return full;
} else if (tillToday > 0) {
return tomorrowAndAfter;
} else if (tillAfter > 0) {
return todayAndAfter;
} else {
return exact;
}
}) | rpl::flatten_latest();
}
object_ptr<Ui::RpWidget> CreateGradientLabel(
QWidget *parent,
rpl::producer<QString> text) {
struct State {
QBrush brush;
QPainterPath path;
};
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto state = raw->lifetime().make_state<State>();
std::move(
text
) | rpl::on_next([=](const QString &text) {
state->path = QPainterPath();
const auto &font = st::groupCallCountdownFont;
state->path.addText(0, font->ascent, font->f, text);
const auto width = font->width(text);
raw->resize(width, font->height);
auto gradient = QLinearGradient(QPoint(width, 0), QPoint());
gradient.setStops(QGradientStops{
{ 0.0, st::groupCallForceMutedBar1->c },
{ .7, st::groupCallForceMutedBar2->c },
{ 1.0, st::groupCallForceMutedBar3->c }
});
state->brush = QBrush(std::move(gradient));
raw->update();
}, raw->lifetime());
raw->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto skip = st::groupCallWidth / 20;
const auto available = parent->width() - 2 * skip;
const auto full = raw->width();
if (available > 0 && full > available) {
const auto scale = available / float64(full);
const auto shift = raw->rect().center();
p.translate(shift);
p.scale(scale, scale);
p.translate(-shift);
}
p.setPen(Qt::NoPen);
p.setBrush(state->brush);
p.drawPath(state->path);
}, raw->lifetime());
return result;
}
} // namespace Calls::Group::Ui

View File

@@ -0,0 +1,27 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
[[nodiscard]] rpl::producer<QString> StartsWhenText(
rpl::producer<TimeId> date);
[[nodiscard]] object_ptr<RpWidget> CreateGradientLabel(
QWidget *parent,
rpl::producer<QString> text);
} // namespace Calls::Group::Ui

View File

@@ -0,0 +1,122 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "base/object_ptr.h"
#include "lang/lang_keys.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/widgets/labels.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace Calls::Group::Ui {
StarsColoring StarsColoringForCount(
const std::vector<StarsColoring> &colorings,
int stars) {
for (auto i = begin(colorings), e = end(colorings); i != e; ++i) {
if (i->fromStars > stars) {
Assert(i != begin(colorings));
return *(std::prev(i));
}
}
return colorings.back();
}
int StarsRequiredForMessage(
const std::vector<StarsColoring> &colorings,
const TextWithTags &text) {
Expects(!colorings.empty());
auto emojis = 0;
auto outLength = 0;
auto view = QStringView(text.text);
const auto length = int(view.size());
while (!view.isEmpty()) {
if (Emoji::Find(view, &outLength)) {
view = view.mid(outLength);
++emojis;
} else {
view = view.mid(1);
}
}
for (const auto &entry : colorings) {
if (emojis <= entry.emojiLimit && length <= entry.charactersMax) {
return entry.fromStars;
}
}
return colorings.back().fromStars + 1;
}
object_ptr<RpWidget> VideoStreamStarsLevel(
not_null<RpWidget*> box,
const std::vector<StarsColoring> &colorings,
rpl::producer<int> starsValue) {
struct State {
rpl::variable<int> stars;
rpl::variable<StarsColoring> coloring;
};
const auto state = box->lifetime().make_state<State>();
state->stars = std::move(starsValue);
state->coloring = state->stars.value(
) | rpl::map([=](int stars) {
return StarsColoringForCount(colorings, stars);
});
auto pinTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
const auto seconds = value.secondsPin;
return (seconds >= 3600)
? tr::lng_hours_tiny(tr::now, lt_count, seconds / 3600)
: (seconds >= 60)
? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)
: tr::lng_seconds_tiny(tr::now, lt_count, seconds);
});
auto limitTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return QString::number(value.charactersMax);
});
auto limitSubtext = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return tr::lng_paid_comment_limit_about(
tr::now,
lt_count,
value.charactersMax);
});
auto emojiTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return QString::number(value.emojiLimit);
});
auto emojiSubtext = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return tr::lng_paid_comment_emoji_about(
tr::now,
lt_count,
value.emojiLimit);
});
return MakeStarSelectInfoBlocks(box, {
{
.title = std::move(pinTitle) | rpl::map(tr::marked),
.subtext = tr::lng_paid_comment_pin_about(),
},
{
.title = std::move(limitTitle) | rpl::map(tr::marked),
.subtext = std::move(limitSubtext),
},
{
.title = std::move(emojiTitle) | rpl::map(tr::marked),
.subtext = std::move(emojiSubtext),
},
}, {}, true);
}
} // namespace Calls::Group::Ui

View File

@@ -0,0 +1,47 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring {
int bgLight = 0;
int bgDark = 0;
int fromStars = 0;
TimeId secondsPin = 0;
int charactersMax = 0;
int emojiLimit = 0;
friend inline auto operator<=>(
const StarsColoring &,
const StarsColoring &) = default;
friend inline bool operator==(
const StarsColoring &,
const StarsColoring &) = default;
};
[[nodiscard]] StarsColoring StarsColoringForCount(
const std::vector<StarsColoring> &colorings,
int stars);
[[nodiscard]] int StarsRequiredForMessage(
const std::vector<StarsColoring> &colorings,
const TextWithTags &text);
[[nodiscard]] object_ptr<Ui::RpWidget> VideoStreamStarsLevel(
not_null<Ui::RpWidget*> box,
const std::vector<StarsColoring> &colorings,
rpl::producer<int> starsValue);
} // namespace Calls::Group::Ui

View File

@@ -0,0 +1,618 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/widgets/rp_window.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/effects/ripple_animation.h"
#include "ui/image/image.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/painter.h"
#include "base/platform/base_platform_info.h"
#include "webrtc/webrtc_video_track.h"
#include "lang/lang_keys.h"
#include "styles/style_calls.h"
#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
#include <QtGui/QWindow>
namespace Calls::Group::Ui::DesktopCapture {
namespace {
constexpr auto kColumns = 3;
constexpr auto kRows = 2;
struct Preview {
explicit Preview(tgcalls::DesktopCaptureSource source);
tgcalls::DesktopCaptureSourceHelper helper;
Webrtc::VideoTrack track;
rpl::lifetime lifetime;
};
class SourceButton final : public RippleButton {
public:
using RippleButton::RippleButton;
private:
QImage prepareRippleMask() const override;
};
QImage SourceButton::prepareRippleMask() const {
return RippleAnimation::RoundRectMask(size(), st::roundRadiusLarge);
}
class Source final {
public:
Source(
not_null<QWidget*> parent,
tgcalls::DesktopCaptureSource source,
const QString &title);
void setGeometry(QRect geometry);
void clearHelper();
[[nodiscard]] rpl::producer<> activations() const;
void setActive(bool active);
[[nodiscard]] QString deviceIdKey() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
void paint();
void setupPreview();
SourceButton _widget;
FlatLabel _label;
Ui::RoundRect _selectedRect;
Ui::RoundRect _activeRect;
tgcalls::DesktopCaptureSource _source;
std::unique_ptr<Preview> _preview;
rpl::event_stream<> _activations;
QImage _frame;
bool _active = false;
};
class ChooseSourceProcess final {
public:
static void Start(not_null<ChooseSourceDelegate*> delegate);
explicit ChooseSourceProcess(not_null<ChooseSourceDelegate*> delegate);
void activate();
private:
void setupPanel();
void setupSources();
void setupGeometryWithParent(not_null<QWidget*> parent);
void fillSources();
void setupSourcesGeometry();
void updateButtonsVisibility();
void destroy();
static base::flat_map<
not_null<ChooseSourceDelegate*>,
std::unique_ptr<ChooseSourceProcess>> &Map();
const not_null<ChooseSourceDelegate*> _delegate;
const std::unique_ptr<RpWindow> _window;
const std::unique_ptr<ScrollArea> _scroll;
const not_null<RpWidget*> _inner;
const not_null<RpWidget*> _bottom;
const not_null<RoundButton*> _submit;
const not_null<RoundButton*> _finish;
const not_null<Checkbox*> _withAudio;
QSize _fixedSize;
std::vector<std::unique_ptr<Source>> _sources;
Source *_selected = nullptr;
QString _selectedId;
};
[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() {
const auto factor = style::DevicePixelRatio();
const auto size = st::desktopCaptureSourceSize * factor;
return {
.aspectSize = { size.width(), size.height() },
.fps = 1,
.captureMouse = false,
};
}
Preview::Preview(tgcalls::DesktopCaptureSource source)
: helper(source, SourceData())
, track(Webrtc::VideoState::Active) {
helper.setOutput(track.sink());
helper.start();
}
Source::Source(
not_null<QWidget*> parent,
tgcalls::DesktopCaptureSource source,
const QString &title)
: _widget(parent, st::groupCallRipple)
, _label(&_widget, title, st::desktopCaptureLabel)
, _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver)
, _activeRect(ImageRoundRadius::Large, st::groupCallMuted1)
, _source(source) {
_widget.paintRequest(
) | rpl::on_next([=] {
paint();
}, _widget.lifetime());
_label.setAttribute(Qt::WA_TransparentForMouseEvents);
_widget.sizeValue(
) | rpl::on_next([=](QSize size) {
const auto padding = st::desktopCapturePadding;
_label.resizeToNaturalWidth(
size.width() - padding.left() - padding.right());
_label.move(
(size.width() - _label.width()) / 2,
size.height() - _label.height() - st::desktopCaptureLabelBottom);
}, _label.lifetime());
_widget.setClickedCallback([=] {
setActive(true);
});
}
rpl::producer<> Source::activations() const {
return _activations.events();
}
QString Source::deviceIdKey() const {
return QString::fromStdString(_source.deviceIdKey());
}
void Source::setActive(bool active) {
if (_active != active) {
_active = active;
_widget.update();
if (active) {
_activations.fire({});
}
}
}
void Source::setGeometry(QRect geometry) {
_widget.setGeometry(geometry);
}
void Source::clearHelper() {
_preview = nullptr;
}
void Source::paint() {
auto p = QPainter(&_widget);
if (_frame.isNull() && !_preview) {
setupPreview();
}
if (_active) {
_activeRect.paint(p, _widget.rect());
} else if (_widget.isOver() || _widget.isDown()) {
_selectedRect.paint(p, _widget.rect());
}
_widget.paintRipple(
p,
{ 0, 0 },
_active ? &st::shadowFg->c : nullptr);
const auto size = _preview ? _preview->track.frameSize() : QSize();
const auto factor = style::DevicePixelRatio();
const auto padding = st::desktopCapturePadding;
const auto rect = _widget.rect();
const auto inner = rect.marginsRemoved(padding);
if (!size.isEmpty()) {
const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio);
const auto request = Webrtc::FrameRequest{
.resize = scaled * factor,
.outer = scaled * factor,
};
_frame = _preview->track.frame(request);
_preview->track.markFrameShown();
}
if (!_frame.isNull()) {
clearHelper();
const auto size = _frame.size() / factor;
const auto x = inner.x() + (inner.width() - size.width()) / 2;
const auto y = inner.y() + (inner.height() - size.height()) / 2;
auto hq = PainterHighQualityEnabler(p);
p.drawImage(QRect(x, y, size.width(), size.height()), _frame);
}
}
void Source::setupPreview() {
_preview = std::make_unique<Preview>(_source);
_preview->track.renderNextFrame(
) | rpl::on_next([=] {
if (_preview->track.frameSize().isEmpty()) {
_preview->track.markFrameShown();
}
_widget.update();
}, _preview->lifetime);
}
rpl::lifetime &Source::lifetime() {
return _widget.lifetime();
}
ChooseSourceProcess::ChooseSourceProcess(
not_null<ChooseSourceDelegate*> delegate)
: _delegate(delegate)
, _window(std::make_unique<RpWindow>())
, _scroll(std::make_unique<ScrollArea>(_window->body()))
, _inner(_scroll->setOwnedWidget(object_ptr<RpWidget>(_scroll.get())))
, _bottom(CreateChild<RpWidget>(_window->body().get()))
, _submit(
CreateChild<RoundButton>(
_bottom.get(),
tr::lng_group_call_screen_share_start(),
st::desktopCaptureSubmit))
, _finish(
CreateChild<RoundButton>(
_bottom.get(),
tr::lng_group_call_screen_share_stop(),
st::desktopCaptureFinish))
, _withAudio(
CreateChild<Checkbox>(
_bottom.get(),
tr::lng_group_call_screen_share_audio(tr::now),
false,
st::desktopCaptureWithAudio)) {
setupPanel();
setupSources();
activate();
}
void ChooseSourceProcess::Start(not_null<ChooseSourceDelegate*> delegate) {
auto &map = Map();
auto i = map.find(delegate);
if (i == end(map)) {
i = map.emplace(delegate, nullptr).first;
delegate->chooseSourceInstanceLifetime().add([=] {
Map().erase(delegate);
});
}
if (!i->second) {
i->second = std::make_unique<ChooseSourceProcess>(delegate);
} else {
i->second->activate();
}
}
void ChooseSourceProcess::activate() {
if (_window->windowState() & Qt::WindowMinimized) {
_window->showNormal();
} else {
_window->show();
}
_window->raise();
_window->activateWindow();
}
[[nodiscard]] base::flat_map<
not_null<ChooseSourceDelegate*>,
std::unique_ptr<ChooseSourceProcess>> &ChooseSourceProcess::Map() {
static auto result = base::flat_map<
not_null<ChooseSourceDelegate*>,
std::unique_ptr<ChooseSourceProcess>>();
return result;
}
void ChooseSourceProcess::setupPanel() {
#ifndef Q_OS_LINUX
//_window->setAttribute(Qt::WA_OpaquePaintEvent);
#endif // Q_OS_LINUX
//_window->setAttribute(Qt::WA_NoSystemBackground);
_window->setWindowIcon(QIcon(
QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
_window->setTitleStyle(st::desktopCaptureSourceTitle);
const auto skips = st::desktopCaptureSourceSkips;
const auto margins = st::desktopCaptureMargins;
const auto padding = st::desktopCapturePadding;
const auto bottomSkip = margins.right() + padding.right();
const auto bottomHeight = 2 * bottomSkip
+ st::desktopCaptureCancel.height;
const auto width = margins.left()
+ kColumns * st::desktopCaptureSourceSize.width()
+ (kColumns - 1) * skips.width()
+ margins.right();
const auto height = margins.top()
+ kRows * st::desktopCaptureSourceSize.height()
+ (kRows - 1) * skips.height()
+ (st::desktopCaptureSourceSize.height() / 2)
+ bottomHeight;
_fixedSize = QSize(width, height);
_window->setStaysOnTop(true);
_window->body()->paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg);
}, _window->lifetime());
_bottom->setGeometry(0, height - bottomHeight, width, bottomHeight);
_submit->setClickedCallback([=] {
if (_selectedId.isEmpty()) {
return;
}
const auto weak = base::make_weak(_window.get());
_delegate->chooseSourceAccepted(
_selectedId,
!_withAudio->isHidden() && _withAudio->checked());
if (const auto strong = weak.get()) {
strong->close();
}
});
_finish->setClickedCallback([=] {
const auto weak = base::make_weak(_window.get());
_delegate->chooseSourceStop();
if (const auto strong = weak.get()) {
strong->close();
}
});
const auto cancel = CreateChild<RoundButton>(
_bottom.get(),
tr::lng_cancel(),
st::desktopCaptureCancel);
cancel->setClickedCallback([=] {
_window->close();
});
rpl::combine(
_submit->widthValue(),
_submit->shownValue(),
_finish->widthValue(),
_finish->shownValue(),
cancel->widthValue()
) | rpl::on_next([=](
int submitWidth,
bool submitShown,
int finishWidth,
bool finishShown,
int cancelWidth) {
_finish->moveToRight(bottomSkip, bottomSkip);
_submit->moveToRight(bottomSkip, bottomSkip);
cancel->moveToRight(
bottomSkip * 2 + (submitShown ? submitWidth : finishWidth),
bottomSkip);
}, _bottom->lifetime());
_withAudio->widthValue(
) | rpl::on_next([=](int width) {
const auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2;
_withAudio->moveToLeft(bottomSkip, top);
}, _withAudio->lifetime());
_withAudio->setChecked(_delegate->chooseSourceActiveWithAudio());
_withAudio->checkedChanges(
) | rpl::on_next([=] {
updateButtonsVisibility();
}, _withAudio->lifetime());
const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty();
_finish->setVisible(sharing);
_submit->setVisible(!sharing);
_window->body()->sizeValue(
) | rpl::on_next([=](QSize size) {
_scroll->setGeometry(
0,
0,
size.width(),
size.height() - _bottom->height());
}, _scroll->lifetime());
_scroll->widthValue(
) | rpl::on_next([=](int width) {
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
const auto innerHeight = margins.top()
+ rows * st::desktopCaptureSourceSize.height()
+ (rows - 1) * skips.height()
+ margins.bottom();
_inner->resize(width, innerHeight);
}, _inner->lifetime());
if (const auto parent = _delegate->chooseSourceParent()) {
setupGeometryWithParent(parent);
}
_window->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return e->type() == QEvent::Close;
}) | rpl::on_next([=] {
destroy();
}, _window->lifetime());
}
void ChooseSourceProcess::setupSources() {
fillSources();
setupSourcesGeometry();
}
void ChooseSourceProcess::fillSources() {
using Type = tgcalls::DesktopCaptureType;
auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen);
auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window);
_withAudio->setVisible(_delegate->chooseSourceWithAudioSupported());
auto screenIndex = 0;
auto windowIndex = 0;
auto firstScreenSelected = false;
const auto active = _delegate->chooseSourceActiveDeviceId();
const auto append = [&](const tgcalls::DesktopCaptureSource &source) {
const auto firstScreen = !source.isWindow() && !screenIndex;
const auto title = !source.isWindow()
? tr::lng_group_call_screen_title(
tr::now,
lt_index,
QString::number(++screenIndex))
: !source.title().empty()
? QString::fromStdString(source.title())
: "Window " + QString::number(++windowIndex);
const auto id = source.deviceIdKey();
_sources.push_back(std::make_unique<Source>(_inner, source, title));
const auto raw = _sources.back().get();
if (!active.isEmpty() && active.toStdString() == id) {
_selected = raw;
raw->setActive(true);
} else if (active.isEmpty() && firstScreen) {
_selected = raw;
raw->setActive(true);
firstScreenSelected = true;
}
_sources.back()->activations(
) | rpl::filter([=] {
return (_selected != raw);
}) | rpl::on_next([=]{
if (_selected) {
_selected->setActive(false);
}
_selected = raw;
updateButtonsVisibility();
}, raw->lifetime());
};
for (const auto &source : screensManager.sources()) {
append(source);
}
for (const auto &source : windowsManager.sources()) {
append(source);
}
if (firstScreenSelected) {
updateButtonsVisibility();
}
}
void ChooseSourceProcess::updateButtonsVisibility() {
const auto selectedId = _selected
? _selected->deviceIdKey()
: QString();
if (selectedId == _delegate->chooseSourceActiveDeviceId()
&& (!_delegate->chooseSourceWithAudioSupported()
|| (_withAudio->checked()
== _delegate->chooseSourceActiveWithAudio()))) {
_selectedId = QString();
_finish->setVisible(true);
_submit->setVisible(false);
} else {
_selectedId = selectedId;
_finish->setVisible(false);
_submit->setVisible(true);
}
}
void ChooseSourceProcess::setupSourcesGeometry() {
if (_sources.empty()) {
destroy();
return;
}
_inner->widthValue(
) | rpl::on_next([=](int width) {
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
const auto margins = st::desktopCaptureMargins;
const auto skips = st::desktopCaptureSourceSkips;
const auto single = (width
- margins.left()
- margins.right()
- (kColumns - 1) * skips.width()) / kColumns;
const auto height = st::desktopCaptureSourceSize.height();
auto top = margins.top();
auto index = 0;
for (auto row = 0; row != rows; ++row) {
auto left = margins.left();
for (auto column = 0; column != kColumns; ++column) {
_sources[index]->setGeometry({ left, top, single, height });
if (++index == _sources.size()) {
break;
}
left += single + skips.width();
}
if (index >= _sources.size()) {
break;
}
top += height + skips.height();
}
}, _inner->lifetime());
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue()
) | rpl::on_next([=](int scrollTop, int scrollHeight) {
const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
const auto margins = st::desktopCaptureMargins;
const auto skips = st::desktopCaptureSourceSkips;
const auto height = st::desktopCaptureSourceSize.height();
auto top = margins.top();
auto index = 0;
for (auto row = 0; row != rows; ++row) {
const auto hidden = (top + height <= scrollTop)
|| (top >= scrollTop + scrollHeight);
if (hidden) {
for (auto column = 0; column != kColumns; ++column) {
_sources[index]->clearHelper();
if (++index == _sources.size()) {
break;
}
}
} else {
index += kColumns;
}
if (index >= _sources.size()) {
break;
}
top += height + skips.height();
}
}, _inner->lifetime());
}
void ChooseSourceProcess::setupGeometryWithParent(
not_null<QWidget*> parent) {
const auto parentScreen = parent->screen();
const auto myScreen = _window->screen();
if (parentScreen && myScreen != parentScreen) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
_window->setScreen(parentScreen);
#else // Qt >= 6.0.0
_window->createWinId();
_window->windowHandle()->setScreen(parentScreen);
#endif // Qt < 6.0.0
}
_window->setFixedSize(_fixedSize);
_window->move(
parent->x() + (parent->width() - _window->width()) / 2,
parent->y() + (parent->height() - _window->height()) / 2);
}
void ChooseSourceProcess::destroy() {
auto &map = Map();
if (const auto i = map.find(_delegate); i != end(map)) {
if (i->second.get() == this) {
base::take(i->second);
}
}
}
} // namespace
void ChooseSource(not_null<ChooseSourceDelegate*> delegate) {
ChooseSourceProcess::Start(delegate);
}
} // namespace Calls::Group::Ui::DesktopCapture

View File

@@ -0,0 +1,34 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
} // namespace Calls::Group::Ui
namespace Calls::Group::Ui::DesktopCapture {
class ChooseSourceDelegate {
public:
virtual QWidget *chooseSourceParent() = 0;
virtual QString chooseSourceActiveDeviceId() = 0;
virtual bool chooseSourceActiveWithAudio() = 0;
virtual bool chooseSourceWithAudioSupported() = 0;
virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0;
virtual void chooseSourceAccepted(
const QString &deviceId,
bool withAudio) = 0;
virtual void chooseSourceStop() = 0;
};
void ChooseSource(not_null<ChooseSourceDelegate*> delegate);
} // namespace Calls::Group::Ui::DesktopCapture

View 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

View 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