1994 lines
56 KiB
C++
1994 lines
56 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "calls/group/calls_group_members.h"
|
|
|
|
#include "calls/group/calls_cover_item.h"
|
|
#include "calls/group/calls_group_call.h"
|
|
#include "calls/group/calls_group_menu.h"
|
|
#include "calls/group/calls_volume_item.h"
|
|
#include "calls/group/calls_group_members_row.h"
|
|
#include "calls/group/calls_group_viewport.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_chat.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_peer.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_group_call.h"
|
|
#include "data/data_peer_values.h" // Data::CanWriteValue.
|
|
#include "data/data_session.h" // Data::Session::invitedToCallUsers.
|
|
#include "settings/settings_common.h" // Settings::CreateButton.
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/scroll_area.h"
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "ui/effects/ripple_animation.h"
|
|
#include "ui/effects/cross_line.h"
|
|
#include "core/application.h" // Core::App().domain, .activeWindow.
|
|
#include "main/main_domain.h" // Core::App().domain().activate.
|
|
#include "main/main_session.h"
|
|
#include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
|
|
#include "boxes/peers/edit_participants_box.h" // SubscribeToMigration.
|
|
#include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfo...
|
|
#include "window/window_controller.h" // Controller::sessionController.
|
|
#include "window/window_session_controller.h"
|
|
#include "webrtc/webrtc_video_track.h"
|
|
#include "styles/style_calls.h"
|
|
|
|
namespace Calls::Group {
|
|
namespace {
|
|
|
|
constexpr auto kKeepRaisedHandStatusDuration = 3 * crl::time(1000);
|
|
|
|
using Row = MembersRow;
|
|
|
|
} // namespace
|
|
|
|
class Members::Controller final
|
|
: public PeerListController
|
|
, public MembersRowDelegate
|
|
, public base::has_weak_ptr {
|
|
public:
|
|
Controller(
|
|
not_null<GroupCall*> call,
|
|
not_null<QWidget*> menuParent,
|
|
PanelMode mode);
|
|
~Controller();
|
|
|
|
using MuteRequest = Group::MuteRequest;
|
|
using VolumeRequest = Group::VolumeRequest;
|
|
|
|
Main::Session &session() const override;
|
|
void prepare() override;
|
|
void rowClicked(not_null<PeerListRow*> row) override;
|
|
void rowRightActionClicked(not_null<PeerListRow*> row) override;
|
|
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
|
QWidget *parent,
|
|
not_null<PeerListRow*> row) override;
|
|
void loadMoreRows() override;
|
|
|
|
[[nodiscard]] rpl::producer<int> fullCountValue() const {
|
|
return _fullCount.value();
|
|
}
|
|
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
|
|
[[nodiscard]] rpl::producer<VolumeRequest> changeVolumeRequests() const;
|
|
[[nodiscard]] auto kickParticipantRequests() const
|
|
-> rpl::producer<not_null<PeerData*>>;
|
|
|
|
Row *findRow(not_null<PeerData*> participantPeer) const;
|
|
void setMode(PanelMode mode);
|
|
|
|
bool rowIsMe(not_null<PeerData*> participantPeer) override;
|
|
bool rowCanMuteMembers() override;
|
|
void rowUpdateRow(not_null<Row*> row) override;
|
|
void rowScheduleRaisedHandStatusRemove(not_null<Row*> row) override;
|
|
void rowPaintIcon(
|
|
Painter &p,
|
|
QRect rect,
|
|
const IconState &state) override;
|
|
int rowPaintStatusIcon(
|
|
Painter &p,
|
|
int x,
|
|
int y,
|
|
int outerWidth,
|
|
not_null<MembersRow*> row,
|
|
const IconState &state) override;
|
|
bool rowIsNarrow() override;
|
|
void rowShowContextMenu(not_null<PeerListRow*> row) override;
|
|
|
|
private:
|
|
[[nodiscard]] std::unique_ptr<Row> createRowForMe();
|
|
[[nodiscard]] std::unique_ptr<Row> createRow(
|
|
const Data::GroupCallParticipant &participant);
|
|
[[nodiscard]] std::unique_ptr<Row> createInvitedRow(
|
|
not_null<PeerData*> participantPeer);
|
|
|
|
[[nodiscard]] bool isMe(not_null<PeerData*> participantPeer) const;
|
|
void prepareRows(not_null<Data::GroupCall*> real);
|
|
|
|
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
|
QWidget *parent,
|
|
not_null<PeerListRow*> row);
|
|
void addMuteActionsToContextMenu(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
not_null<PeerData*> participantPeer,
|
|
bool participantIsCallAdmin,
|
|
not_null<Row*> row);
|
|
void setupListChangeViewers();
|
|
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
|
void updateRow(
|
|
const std::optional<Data::GroupCallParticipant> &was,
|
|
const Data::GroupCallParticipant &now);
|
|
void updateRow(
|
|
not_null<Row*> row,
|
|
const std::optional<Data::GroupCallParticipant> &was,
|
|
const Data::GroupCallParticipant *participant);
|
|
void updateRowInSoundingMap(
|
|
not_null<Row*> row,
|
|
bool wasSounding,
|
|
uint32 wasSsrc,
|
|
uint32 wasAdditionalSsrc,
|
|
const Data::GroupCallParticipant *participant);
|
|
void updateRowInSoundingMap(
|
|
not_null<Row*> row,
|
|
bool wasSounding,
|
|
uint32 wasSsrc,
|
|
bool nowSounding,
|
|
uint32 nowSsrc);
|
|
void removeRow(not_null<Row*> row);
|
|
void removeRowFromSoundingMap(not_null<Row*> row);
|
|
void updateRowLevel(not_null<Row*> row, float level);
|
|
void checkRowPosition(not_null<Row*> row);
|
|
[[nodiscard]] bool needToReorder(not_null<Row*> row) const;
|
|
[[nodiscard]] bool allRowsAboveAreSpeaking(not_null<Row*> row) const;
|
|
[[nodiscard]] bool allRowsAboveMoreImportantThanHand(
|
|
not_null<Row*> row,
|
|
uint64 raiseHandRating) const;
|
|
[[nodiscard]] const Data::GroupCallParticipant *findParticipant(
|
|
const std::string &endpoint) const;
|
|
[[nodiscard]] const std::string &computeScreenEndpoint(
|
|
not_null<const Data::GroupCallParticipant*> participant) const;
|
|
[[nodiscard]] const std::string &computeCameraEndpoint(
|
|
not_null<const Data::GroupCallParticipant*> participant) const;
|
|
void showRowMenu(not_null<PeerListRow*> row, bool highlightRow);
|
|
|
|
void toggleVideoEndpointActive(
|
|
const VideoEndpoint &endpoint,
|
|
bool active);
|
|
|
|
void appendInvitedUsers();
|
|
void scheduleRaisedHandStatusRemove();
|
|
|
|
void hideRowsWithVideoExcept(const VideoEndpoint &large);
|
|
void showAllHiddenRows();
|
|
void hideRowWithVideo(const VideoEndpoint &endpoint);
|
|
void showRowWithVideo(const VideoEndpoint &endpoint);
|
|
|
|
const not_null<GroupCall*> _call;
|
|
not_null<PeerData*> _peer;
|
|
std::string _largeEndpoint;
|
|
bool _prepared = false;
|
|
|
|
rpl::event_stream<MuteRequest> _toggleMuteRequests;
|
|
rpl::event_stream<VolumeRequest> _changeVolumeRequests;
|
|
rpl::event_stream<not_null<PeerData*>> _kickParticipantRequests;
|
|
rpl::variable<int> _fullCount = 1;
|
|
|
|
not_null<QWidget*> _menuParent;
|
|
base::unique_qptr<Ui::PopupMenu> _menu;
|
|
base::flat_set<not_null<PeerData*>> _menuCheckRowsAfterHidden;
|
|
|
|
base::flat_map<PeerListRowId, crl::time> _raisedHandStatusRemoveAt;
|
|
base::Timer _raisedHandStatusRemoveTimer;
|
|
|
|
base::flat_map<uint32, not_null<Row*>> _soundingRowBySsrc;
|
|
base::flat_set<not_null<PeerData*>> _cameraActive;
|
|
base::flat_set<not_null<PeerData*>> _screenActive;
|
|
Ui::Animations::Basic _soundingAnimation;
|
|
|
|
crl::time _soundingAnimationHideLastTime = 0;
|
|
bool _skipRowLevelUpdate = false;
|
|
|
|
PanelMode _mode = PanelMode::Default;
|
|
Ui::CrossLineAnimation _inactiveCrossLine;
|
|
Ui::CrossLineAnimation _coloredCrossLine;
|
|
Ui::CrossLineAnimation _inactiveNarrowCrossLine;
|
|
Ui::CrossLineAnimation _coloredNarrowCrossLine;
|
|
Ui::CrossLineAnimation _videoCrossLine;
|
|
Ui::RoundRect _narrowRoundRectSelected;
|
|
Ui::RoundRect _narrowRoundRect;
|
|
QImage _narrowShadow;
|
|
|
|
rpl::lifetime _lifetime;
|
|
|
|
};
|
|
|
|
Members::Controller::Controller(
|
|
not_null<GroupCall*> call,
|
|
not_null<QWidget*> menuParent,
|
|
PanelMode mode)
|
|
: _call(call)
|
|
, _peer(call->peer())
|
|
, _menuParent(menuParent)
|
|
, _raisedHandStatusRemoveTimer([=] { scheduleRaisedHandStatusRemove(); })
|
|
, _mode(mode)
|
|
, _inactiveCrossLine(st::groupCallMemberInactiveCrossLine)
|
|
, _coloredCrossLine(st::groupCallMemberColoredCrossLine)
|
|
, _inactiveNarrowCrossLine(st::groupCallNarrowInactiveCrossLine)
|
|
, _coloredNarrowCrossLine(st::groupCallNarrowColoredCrossLine)
|
|
, _videoCrossLine(st::groupCallVideoCrossLine)
|
|
, _narrowRoundRectSelected(
|
|
ImageRoundRadius::Large,
|
|
st::groupCallMembersBgOver)
|
|
, _narrowRoundRect(ImageRoundRadius::Large, st::groupCallMembersBg) {
|
|
style::PaletteChanged(
|
|
) | rpl::start_with_next([=] {
|
|
_inactiveCrossLine.invalidate();
|
|
_coloredCrossLine.invalidate();
|
|
_inactiveNarrowCrossLine.invalidate();
|
|
_coloredNarrowCrossLine.invalidate();
|
|
}, _lifetime);
|
|
|
|
rpl::combine(
|
|
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
|
Core::App().appDeactivatedValue()
|
|
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
|
|
const auto hide = !(!animDisabled && !deactivated);
|
|
|
|
if (!(hide && _soundingAnimationHideLastTime)) {
|
|
_soundingAnimationHideLastTime = hide ? crl::now() : 0;
|
|
}
|
|
for (const auto &[_, row] : _soundingRowBySsrc) {
|
|
if (hide) {
|
|
updateRowLevel(row, 0.);
|
|
}
|
|
row->setSkipLevelUpdate(hide);
|
|
}
|
|
if (!hide && !_soundingAnimation.animating()) {
|
|
_soundingAnimation.start();
|
|
}
|
|
_skipRowLevelUpdate = hide;
|
|
}, _lifetime);
|
|
|
|
_soundingAnimation.init([=](crl::time now) {
|
|
if (const auto &last = _soundingAnimationHideLastTime; (last > 0)
|
|
&& (now - last >= kBlobsEnterDuration)) {
|
|
_soundingAnimation.stop();
|
|
return false;
|
|
}
|
|
for (const auto &[ssrc, row] : _soundingRowBySsrc) {
|
|
row->updateBlobAnimation(now);
|
|
delegate()->peerListUpdateRow(row);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
_peer->session().changes().peerUpdates(
|
|
Data::PeerUpdate::Flag::About
|
|
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
|
|
if (const auto row = findRow(update.peer)) {
|
|
row->setAbout(update.peer->about());
|
|
}
|
|
}, _lifetime);
|
|
}
|
|
|
|
Members::Controller::~Controller() {
|
|
base::take(_menu);
|
|
}
|
|
|
|
void Members::Controller::setupListChangeViewers() {
|
|
_call->real(
|
|
) | rpl::start_with_next([=](not_null<Data::GroupCall*> real) {
|
|
subscribeToChanges(real);
|
|
}, _lifetime);
|
|
|
|
_call->levelUpdates(
|
|
) | rpl::start_with_next([=](const LevelUpdate &update) {
|
|
const auto i = _soundingRowBySsrc.find(update.ssrc);
|
|
if (i != end(_soundingRowBySsrc)) {
|
|
updateRowLevel(i->second, update.value);
|
|
}
|
|
}, _lifetime);
|
|
|
|
_call->videoEndpointLargeValue(
|
|
) | rpl::start_with_next([=](const VideoEndpoint &large) {
|
|
if (large) {
|
|
hideRowsWithVideoExcept(large);
|
|
} else {
|
|
showAllHiddenRows();
|
|
}
|
|
}, _lifetime);
|
|
|
|
_call->videoStreamShownUpdates(
|
|
) | rpl::filter([=](const VideoStateToggle &update) {
|
|
const auto &large = _call->videoEndpointLarge();
|
|
return large && (update.endpoint != large);
|
|
}) | rpl::start_with_next([=](const VideoStateToggle &update) {
|
|
if (update.value) {
|
|
hideRowWithVideo(update.endpoint);
|
|
} else {
|
|
showRowWithVideo(update.endpoint);
|
|
}
|
|
}, _lifetime);
|
|
|
|
_call->rejoinEvents(
|
|
) | rpl::start_with_next([=](const Group::RejoinEvent &event) {
|
|
const auto guard = gsl::finally([&] {
|
|
delegate()->peerListRefreshRows();
|
|
});
|
|
if (const auto row = findRow(event.wasJoinAs)) {
|
|
removeRow(row);
|
|
}
|
|
if (findRow(event.nowJoinAs)) {
|
|
return;
|
|
} else if (auto row = createRowForMe()) {
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
}
|
|
}, _lifetime);
|
|
}
|
|
|
|
void Members::Controller::hideRowsWithVideoExcept(
|
|
const VideoEndpoint &large) {
|
|
auto changed = false;
|
|
auto showLargeRow = true;
|
|
for (const auto &endpoint : _call->shownVideoTracks()) {
|
|
if (endpoint != large) {
|
|
if (const auto row = findRow(endpoint.peer)) {
|
|
if (endpoint.peer == large.peer) {
|
|
showLargeRow = false;
|
|
}
|
|
delegate()->peerListSetRowHidden(row, true);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
if (const auto row = showLargeRow ? findRow(large.peer) : nullptr) {
|
|
delegate()->peerListSetRowHidden(row, false);
|
|
changed = true;
|
|
}
|
|
if (changed) {
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::showAllHiddenRows() {
|
|
auto shown = false;
|
|
for (const auto &endpoint : _call->shownVideoTracks()) {
|
|
if (const auto row = findRow(endpoint.peer)) {
|
|
delegate()->peerListSetRowHidden(row, false);
|
|
shown = true;
|
|
}
|
|
}
|
|
if (shown) {
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::hideRowWithVideo(const VideoEndpoint &endpoint) {
|
|
if (const auto row = findRow(endpoint.peer)) {
|
|
delegate()->peerListSetRowHidden(row, true);
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::showRowWithVideo(const VideoEndpoint &endpoint) {
|
|
const auto peer = endpoint.peer;
|
|
const auto &large = _call->videoEndpointLarge();
|
|
if (large) {
|
|
for (const auto &endpoint : _call->shownVideoTracks()) {
|
|
if (endpoint != large && endpoint.peer == peer) {
|
|
// Still hidden with another video.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (const auto row = findRow(endpoint.peer)) {
|
|
delegate()->peerListSetRowHidden(row, false);
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
|
_fullCount = real->fullCountValue();
|
|
|
|
real->participantsReloaded(
|
|
) | rpl::start_with_next([=] {
|
|
prepareRows(real);
|
|
}, _lifetime);
|
|
|
|
using Update = Data::GroupCall::ParticipantUpdate;
|
|
real->participantUpdated(
|
|
) | rpl::start_with_next([=](const Update &update) {
|
|
Expects(update.was.has_value() || update.now.has_value());
|
|
|
|
const auto participantPeer = update.was
|
|
? update.was->peer
|
|
: update.now->peer;
|
|
if (!update.now) {
|
|
if (const auto row = findRow(participantPeer)) {
|
|
if (isMe(participantPeer)) {
|
|
updateRow(row, update.was, nullptr);
|
|
} else {
|
|
removeRow(row);
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
} else {
|
|
updateRow(update.was, *update.now);
|
|
}
|
|
}, _lifetime);
|
|
|
|
for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
|
|
toggleVideoEndpointActive(endpoint, true);
|
|
}
|
|
_call->videoStreamActiveUpdates(
|
|
) | rpl::start_with_next([=](const VideoStateToggle &update) {
|
|
toggleVideoEndpointActive(update.endpoint, update.value);
|
|
}, _lifetime);
|
|
|
|
if (_prepared) {
|
|
appendInvitedUsers();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::toggleVideoEndpointActive(
|
|
const VideoEndpoint &endpoint,
|
|
bool active) {
|
|
const auto toggleOne = [=](
|
|
base::flat_set<not_null<PeerData*>> &set,
|
|
not_null<PeerData*> participantPeer,
|
|
bool active) {
|
|
if ((active && set.emplace(participantPeer).second)
|
|
|| (!active && set.remove(participantPeer))) {
|
|
if (_mode == PanelMode::Wide) {
|
|
if (const auto row = findRow(participantPeer)) {
|
|
delegate()->peerListUpdateRow(row);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const auto &id = endpoint.id;
|
|
const auto participantPeer = endpoint.peer;
|
|
const auto real = _call->lookupReal();
|
|
if (active) {
|
|
if (const auto participant = findParticipant(id)) {
|
|
if (computeCameraEndpoint(participant) == id) {
|
|
toggleOne(_cameraActive, participantPeer, true);
|
|
} else if (computeScreenEndpoint(participant) == id) {
|
|
toggleOne(_screenActive, participantPeer, true);
|
|
}
|
|
}
|
|
} else if (const auto participant = real->participantByPeer(
|
|
participantPeer)) {
|
|
const auto &camera = computeCameraEndpoint(participant);
|
|
const auto &screen = computeScreenEndpoint(participant);
|
|
if (camera == id || camera.empty()) {
|
|
toggleOne(_cameraActive, participantPeer, false);
|
|
}
|
|
if (screen == id || screen.empty()) {
|
|
toggleOne(_screenActive, participantPeer, false);
|
|
}
|
|
} else {
|
|
toggleOne(_cameraActive, participantPeer, false);
|
|
toggleOne(_screenActive, participantPeer, false);
|
|
}
|
|
|
|
}
|
|
|
|
void Members::Controller::appendInvitedUsers() {
|
|
if (const auto id = _call->id()) {
|
|
for (const auto &user : _peer->owner().invitedToCallUsers(id)) {
|
|
if (auto row = createInvitedRow(user)) {
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
}
|
|
}
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
|
|
using Invite = Data::Session::InviteToCall;
|
|
_peer->owner().invitesToCalls(
|
|
) | rpl::filter([=](const Invite &invite) {
|
|
return (invite.id == _call->id());
|
|
}) | rpl::start_with_next([=](const Invite &invite) {
|
|
if (auto row = createInvitedRow(invite.user)) {
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}, _lifetime);
|
|
}
|
|
|
|
void Members::Controller::updateRow(
|
|
const std::optional<Data::GroupCallParticipant> &was,
|
|
const Data::GroupCallParticipant &now) {
|
|
auto reorderIfInvitedBefore = 0;
|
|
auto checkPosition = (Row*)nullptr;
|
|
auto addedToBottom = (Row*)nullptr;
|
|
if (const auto row = findRow(now.peer)) {
|
|
if (row->state() == Row::State::Invited) {
|
|
reorderIfInvitedBefore = row->absoluteIndex();
|
|
}
|
|
updateRow(row, was, &now);
|
|
if ((now.speaking && (!was || !was->speaking))
|
|
|| (now.raisedHandRating != (was ? was->raisedHandRating : 0))
|
|
|| (!now.canSelfUnmute && was && was->canSelfUnmute)) {
|
|
checkPosition = row;
|
|
}
|
|
} else if (auto row = createRow(now)) {
|
|
if (row->speaking()) {
|
|
delegate()->peerListPrependRow(std::move(row));
|
|
} else {
|
|
reorderIfInvitedBefore = delegate()->peerListFullRowsCount();
|
|
if (now.raisedHandRating != 0) {
|
|
checkPosition = row.get();
|
|
} else {
|
|
addedToBottom = row.get();
|
|
}
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
}
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
static constexpr auto kInvited = Row::State::Invited;
|
|
const auto reorder = [&] {
|
|
const auto count = reorderIfInvitedBefore;
|
|
if (count <= 0) {
|
|
return false;
|
|
}
|
|
const auto row = delegate()->peerListRowAt(
|
|
reorderIfInvitedBefore - 1).get();
|
|
return (static_cast<Row*>(row)->state() == kInvited);
|
|
}();
|
|
if (reorder) {
|
|
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
|
return static_cast<const Row&>(row).state() != kInvited;
|
|
});
|
|
}
|
|
if (checkPosition) {
|
|
checkRowPosition(checkPosition);
|
|
} else if (addedToBottom) {
|
|
const auto real = _call->lookupReal();
|
|
if (real && real->joinedToTop()) {
|
|
const auto proj = [&](const PeerListRow &other) {
|
|
const auto &real = static_cast<const Row&>(other);
|
|
return real.speaking()
|
|
? 2
|
|
: (&real == addedToBottom)
|
|
? 1
|
|
: 0;
|
|
};
|
|
delegate()->peerListSortRows([&](
|
|
const PeerListRow &a,
|
|
const PeerListRow &b) {
|
|
return proj(a) > proj(b);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Members::Controller::allRowsAboveAreSpeaking(not_null<Row*> row) const {
|
|
const auto count = delegate()->peerListFullRowsCount();
|
|
for (auto i = 0; i != count; ++i) {
|
|
const auto above = delegate()->peerListRowAt(i);
|
|
if (above == row) {
|
|
// All rows above are speaking.
|
|
return true;
|
|
} else if (!static_cast<Row*>(above.get())->speaking()) {
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Members::Controller::allRowsAboveMoreImportantThanHand(
|
|
not_null<Row*> row,
|
|
uint64 raiseHandRating) const {
|
|
Expects(raiseHandRating > 0);
|
|
|
|
const auto count = delegate()->peerListFullRowsCount();
|
|
for (auto i = 0; i != count; ++i) {
|
|
const auto above = delegate()->peerListRowAt(i);
|
|
if (above == row) {
|
|
// All rows above are 'more important' than this raised hand.
|
|
return true;
|
|
}
|
|
const auto real = static_cast<Row*>(above.get());
|
|
const auto state = real->state();
|
|
if (state == Row::State::Muted
|
|
|| (state == Row::State::RaisedHand
|
|
&& real->raisedHandRating() < raiseHandRating)) {
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Members::Controller::needToReorder(not_null<Row*> row) const {
|
|
// All reorder cases:
|
|
// - bring speaking up
|
|
// - bring raised hand up
|
|
// - bring muted down
|
|
|
|
if (row->speaking()) {
|
|
return !allRowsAboveAreSpeaking(row);
|
|
} else if (!_peer->canManageGroupCall()) {
|
|
// Raising hands reorder participants only for voice chat admins.
|
|
return false;
|
|
}
|
|
|
|
const auto rating = row->raisedHandRating();
|
|
if (!rating && row->state() != Row::State::Muted) {
|
|
return false;
|
|
}
|
|
if (rating > 0 && !allRowsAboveMoreImportantThanHand(row, rating)) {
|
|
return true;
|
|
}
|
|
const auto index = row->absoluteIndex();
|
|
if (index + 1 == delegate()->peerListFullRowsCount()) {
|
|
// Last one, can't bring lower.
|
|
return false;
|
|
}
|
|
const auto next = delegate()->peerListRowAt(index + 1);
|
|
const auto state = static_cast<Row*>(next.get())->state();
|
|
if ((state != Row::State::Muted) && (state != Row::State::RaisedHand)) {
|
|
return true;
|
|
}
|
|
if (!rating && static_cast<Row*>(next.get())->raisedHandRating()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Members::Controller::checkRowPosition(not_null<Row*> row) {
|
|
if (_menu) {
|
|
// Don't reorder rows while we show the popup menu.
|
|
_menuCheckRowsAfterHidden.emplace(row->peer());
|
|
return;
|
|
} else if (!needToReorder(row)) {
|
|
return;
|
|
}
|
|
|
|
// Someone started speaking and has a non-speaking row above him.
|
|
// Or someone raised hand and has force muted above him.
|
|
// Or someone was forced muted and had can_unmute_self below him. Sort.
|
|
static constexpr auto kTop = std::numeric_limits<uint64>::max();
|
|
const auto projForAdmin = [&](const PeerListRow &other) {
|
|
const auto &real = static_cast<const Row&>(other);
|
|
return real.speaking()
|
|
// Speaking 'row' to the top, all other speaking below it.
|
|
? (&real == row.get() ? kTop : (kTop - 1))
|
|
: (real.raisedHandRating() > 0)
|
|
// Then all raised hands sorted by rating.
|
|
? real.raisedHandRating()
|
|
: (real.state() == Row::State::Muted)
|
|
// All force muted at the bottom, but 'row' still above others.
|
|
? (&real == row.get() ? 1ULL : 0ULL)
|
|
// All not force-muted lie between raised hands and speaking.
|
|
: (kTop - 2);
|
|
};
|
|
const auto projForOther = [&](const PeerListRow &other) {
|
|
const auto &real = static_cast<const Row&>(other);
|
|
return real.speaking()
|
|
// Speaking 'row' to the top, all other speaking below it.
|
|
? (&real == row.get() ? kTop : (kTop - 1))
|
|
: 0ULL;
|
|
};
|
|
|
|
using Comparator = Fn<bool(const PeerListRow&, const PeerListRow&)>;
|
|
const auto makeComparator = [&](const auto &proj) -> Comparator {
|
|
return [&](const PeerListRow &a, const PeerListRow &b) {
|
|
return proj(a) > proj(b);
|
|
};
|
|
};
|
|
delegate()->peerListSortRows(_peer->canManageGroupCall()
|
|
? makeComparator(projForAdmin)
|
|
: makeComparator(projForOther));
|
|
}
|
|
|
|
void Members::Controller::updateRow(
|
|
not_null<Row*> row,
|
|
const std::optional<Data::GroupCallParticipant> &was,
|
|
const Data::GroupCallParticipant *participant) {
|
|
const auto wasSounding = row->sounding();
|
|
const auto wasSsrc = was ? was->ssrc : 0;
|
|
const auto wasAdditionalSsrc = was
|
|
? GetAdditionalAudioSsrc(was->videoParams)
|
|
: 0;
|
|
row->setSkipLevelUpdate(_skipRowLevelUpdate);
|
|
row->updateState(participant);
|
|
|
|
const auto wasNoSounding = _soundingRowBySsrc.empty();
|
|
updateRowInSoundingMap(
|
|
row,
|
|
wasSounding,
|
|
wasSsrc,
|
|
wasAdditionalSsrc,
|
|
participant);
|
|
const auto nowNoSounding = _soundingRowBySsrc.empty();
|
|
if (wasNoSounding && !nowNoSounding) {
|
|
_soundingAnimation.start();
|
|
} else if (nowNoSounding && !wasNoSounding) {
|
|
_soundingAnimation.stop();
|
|
}
|
|
|
|
delegate()->peerListUpdateRow(row);
|
|
}
|
|
|
|
void Members::Controller::updateRowInSoundingMap(
|
|
not_null<Row*> row,
|
|
bool wasSounding,
|
|
uint32 wasSsrc,
|
|
uint32 wasAdditionalSsrc,
|
|
const Data::GroupCallParticipant *participant) {
|
|
const auto nowSounding = row->sounding();
|
|
const auto nowSsrc = participant ? participant->ssrc : 0;
|
|
const auto nowAdditionalSsrc = participant
|
|
? GetAdditionalAudioSsrc(participant->videoParams)
|
|
: 0;
|
|
updateRowInSoundingMap(row, wasSounding, wasSsrc, nowSounding, nowSsrc);
|
|
updateRowInSoundingMap(
|
|
row,
|
|
wasSounding,
|
|
wasAdditionalSsrc,
|
|
nowSounding,
|
|
nowAdditionalSsrc);
|
|
}
|
|
|
|
void Members::Controller::updateRowInSoundingMap(
|
|
not_null<Row*> row,
|
|
bool wasSounding,
|
|
uint32 wasSsrc,
|
|
bool nowSounding,
|
|
uint32 nowSsrc) {
|
|
if (wasSsrc == nowSsrc) {
|
|
if (nowSsrc && nowSounding != wasSounding) {
|
|
if (nowSounding) {
|
|
_soundingRowBySsrc.emplace(nowSsrc, row);
|
|
} else {
|
|
_soundingRowBySsrc.remove(nowSsrc);
|
|
}
|
|
}
|
|
} else {
|
|
_soundingRowBySsrc.remove(wasSsrc);
|
|
if (nowSounding && nowSsrc) {
|
|
_soundingRowBySsrc.emplace(nowSsrc, row);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Members::Controller::removeRow(not_null<Row*> row) {
|
|
removeRowFromSoundingMap(row);
|
|
delegate()->peerListRemoveRow(row);
|
|
}
|
|
|
|
void Members::Controller::removeRowFromSoundingMap(not_null<Row*> row) {
|
|
// There may be 0, 1 or 2 entries for a row.
|
|
for (auto i = begin(_soundingRowBySsrc); i != end(_soundingRowBySsrc);) {
|
|
if (i->second == row) {
|
|
i = _soundingRowBySsrc.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Members::Controller::updateRowLevel(
|
|
not_null<Row*> row,
|
|
float level) {
|
|
if (_skipRowLevelUpdate) {
|
|
return;
|
|
}
|
|
row->updateLevel(level);
|
|
}
|
|
|
|
Row *Members::Controller::findRow(
|
|
not_null<PeerData*> participantPeer) const {
|
|
return static_cast<Row*>(
|
|
delegate()->peerListFindRow(participantPeer->id.value));
|
|
}
|
|
|
|
void Members::Controller::setMode(PanelMode mode) {
|
|
_mode = mode;
|
|
}
|
|
|
|
const Data::GroupCallParticipant *Members::Controller::findParticipant(
|
|
const std::string &endpoint) const {
|
|
if (endpoint.empty()) {
|
|
return nullptr;
|
|
}
|
|
const auto real = _call->lookupReal();
|
|
if (!real) {
|
|
return nullptr;
|
|
} else if (endpoint == _call->screenSharingEndpoint()
|
|
|| endpoint == _call->cameraSharingEndpoint()) {
|
|
return real->participantByPeer(_call->joinAs());
|
|
} else {
|
|
return real->participantByEndpoint(endpoint);
|
|
}
|
|
}
|
|
|
|
const std::string &Members::Controller::computeScreenEndpoint(
|
|
not_null<const Data::GroupCallParticipant*> participant) const {
|
|
return (participant->peer == _call->joinAs())
|
|
? _call->screenSharingEndpoint()
|
|
: participant->screenEndpoint();
|
|
}
|
|
|
|
const std::string &Members::Controller::computeCameraEndpoint(
|
|
not_null<const Data::GroupCallParticipant*> participant) const {
|
|
return (participant->peer == _call->joinAs())
|
|
? _call->cameraSharingEndpoint()
|
|
: participant->cameraEndpoint();
|
|
}
|
|
|
|
Main::Session &Members::Controller::session() const {
|
|
return _call->peer()->session();
|
|
}
|
|
|
|
void Members::Controller::prepare() {
|
|
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
|
|
setDescription(nullptr);
|
|
setSearchNoResults(nullptr);
|
|
|
|
if (const auto real = _call->lookupReal()) {
|
|
prepareRows(real);
|
|
} else if (auto row = createRowForMe()) {
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
|
|
loadMoreRows();
|
|
appendInvitedUsers();
|
|
_prepared = true;
|
|
|
|
setupListChangeViewers();
|
|
}
|
|
|
|
bool Members::Controller::isMe(not_null<PeerData*> participantPeer) const {
|
|
return (_call->joinAs() == participantPeer);
|
|
}
|
|
|
|
void Members::Controller::prepareRows(not_null<Data::GroupCall*> real) {
|
|
auto foundMe = false;
|
|
auto changed = false;
|
|
auto count = delegate()->peerListFullRowsCount();
|
|
for (auto i = 0; i != count;) {
|
|
const auto row = static_cast<Row*>(
|
|
delegate()->peerListRowAt(i).get());
|
|
removeRowFromSoundingMap(row);
|
|
const auto participantPeer = row->peer();
|
|
const auto me = isMe(participantPeer);
|
|
if (me) {
|
|
foundMe = true;
|
|
}
|
|
if (const auto found = real->participantByPeer(participantPeer)) {
|
|
updateRowInSoundingMap(row, false, 0, 0, found);
|
|
++i;
|
|
} else if (me) {
|
|
++i;
|
|
} else {
|
|
changed = true;
|
|
removeRow(row);
|
|
--count;
|
|
}
|
|
}
|
|
if (!foundMe) {
|
|
const auto me = _call->joinAs();
|
|
const auto participant = real->participantByPeer(me);
|
|
auto row = participant
|
|
? createRow(*participant)
|
|
: createRowForMe();
|
|
if (row) {
|
|
changed = true;
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
}
|
|
}
|
|
for (const auto &participant : real->participants()) {
|
|
if (auto row = createRow(participant)) {
|
|
changed = true;
|
|
delegate()->peerListAppendRow(std::move(row));
|
|
}
|
|
}
|
|
if (changed) {
|
|
delegate()->peerListRefreshRows();
|
|
}
|
|
}
|
|
|
|
void Members::Controller::loadMoreRows() {
|
|
if (const auto real = _call->lookupReal()) {
|
|
real->requestParticipants();
|
|
}
|
|
}
|
|
|
|
auto Members::Controller::toggleMuteRequests() const
|
|
-> rpl::producer<MuteRequest> {
|
|
return _toggleMuteRequests.events();
|
|
}
|
|
|
|
auto Members::Controller::changeVolumeRequests() const
|
|
-> rpl::producer<VolumeRequest> {
|
|
return _changeVolumeRequests.events();
|
|
}
|
|
|
|
bool Members::Controller::rowIsMe(not_null<PeerData*> participantPeer) {
|
|
return isMe(participantPeer);
|
|
}
|
|
|
|
bool Members::Controller::rowCanMuteMembers() {
|
|
return _peer->canManageGroupCall();
|
|
}
|
|
|
|
void Members::Controller::rowUpdateRow(not_null<Row*> row) {
|
|
delegate()->peerListUpdateRow(row);
|
|
}
|
|
|
|
void Members::Controller::rowScheduleRaisedHandStatusRemove(
|
|
not_null<Row*> row) {
|
|
const auto id = row->id();
|
|
const auto when = crl::now() + kKeepRaisedHandStatusDuration;
|
|
const auto i = _raisedHandStatusRemoveAt.find(id);
|
|
if (i != _raisedHandStatusRemoveAt.end()) {
|
|
i->second = when;
|
|
} else {
|
|
_raisedHandStatusRemoveAt.emplace(id, when);
|
|
}
|
|
scheduleRaisedHandStatusRemove();
|
|
}
|
|
|
|
void Members::Controller::scheduleRaisedHandStatusRemove() {
|
|
auto waiting = crl::time(0);
|
|
const auto now = crl::now();
|
|
for (auto i = begin(_raisedHandStatusRemoveAt)
|
|
; i != end(_raisedHandStatusRemoveAt);) {
|
|
if (i->second <= now) {
|
|
if (const auto row = delegate()->peerListFindRow(i->first)) {
|
|
static_cast<Row*>(row)->clearRaisedHandStatus();
|
|
}
|
|
i = _raisedHandStatusRemoveAt.erase(i);
|
|
} else {
|
|
if (!waiting || waiting > (i->second - now)) {
|
|
waiting = i->second - now;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
if (waiting > 0) {
|
|
if (!_raisedHandStatusRemoveTimer.isActive()
|
|
|| _raisedHandStatusRemoveTimer.remainingTime() > waiting) {
|
|
_raisedHandStatusRemoveTimer.callOnce(waiting);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Members::Controller::rowPaintIcon(
|
|
Painter &p,
|
|
QRect rect,
|
|
const IconState &state) {
|
|
if (_mode == PanelMode::Wide
|
|
&& state.style == MembersRowStyle::Default) {
|
|
return;
|
|
}
|
|
const auto narrow = (state.style == MembersRowStyle::Narrow);
|
|
if (state.invited) {
|
|
if (narrow) {
|
|
st::groupCallNarrowInvitedIcon.paintInCenter(p, rect);
|
|
} else {
|
|
st::groupCallMemberInvited.paintInCenter(
|
|
p,
|
|
QRect(
|
|
rect.topLeft() + st::groupCallMemberInvitedPosition,
|
|
st::groupCallMemberInvited.size()));
|
|
}
|
|
return;
|
|
}
|
|
const auto video = (state.style == MembersRowStyle::Video);
|
|
const auto &greenIcon = video
|
|
? st::groupCallVideoCrossLine.icon
|
|
: narrow
|
|
? st::groupCallNarrowColoredCrossLine.icon
|
|
: st::groupCallMemberColoredCrossLine.icon;
|
|
const auto left = rect.x() + (rect.width() - greenIcon.width()) / 2;
|
|
const auto top = rect.y() + (rect.height() - greenIcon.height()) / 2;
|
|
if (state.speaking == 1. && !state.mutedByMe) {
|
|
// Just green icon, no cross, no coloring.
|
|
greenIcon.paintInCenter(p, rect);
|
|
return;
|
|
} else if (state.speaking == 0. && (!narrow || !state.mutedByMe)) {
|
|
if (state.active == 1.) {
|
|
// Just gray icon, no cross, no coloring.
|
|
const auto &grayIcon = video
|
|
? st::groupCallVideoCrossLine.icon
|
|
: narrow
|
|
? st::groupCallNarrowInactiveCrossLine.icon
|
|
: st::groupCallMemberInactiveCrossLine.icon;
|
|
grayIcon.paintInCenter(p, rect);
|
|
return;
|
|
} else if (state.active == 0.) {
|
|
if (state.muted == 1.) {
|
|
if (state.raisedHand) {
|
|
(narrow
|
|
? st::groupCallNarrowRaisedHand
|
|
: st::groupCallMemberRaisedHand).paintInCenter(p, rect);
|
|
return;
|
|
}
|
|
// Red crossed icon, colorized once, cached as last frame.
|
|
auto &line = video
|
|
? _videoCrossLine
|
|
: narrow
|
|
? _coloredNarrowCrossLine
|
|
: _coloredCrossLine;
|
|
const auto color = video
|
|
? std::nullopt
|
|
: std::make_optional(st::groupCallMemberMutedIcon->c);
|
|
line.paint(
|
|
p,
|
|
left,
|
|
top,
|
|
1.,
|
|
color);
|
|
return;
|
|
} else if (state.muted == 0.) {
|
|
// Gray crossed icon, no coloring, cached as last frame.
|
|
auto &line = video
|
|
? _videoCrossLine
|
|
: narrow
|
|
? _inactiveNarrowCrossLine
|
|
: _inactiveCrossLine;
|
|
line.paint(p, left, top, 1.);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
const auto activeInactiveColor = anim::color(
|
|
(narrow
|
|
? st::groupCallMemberNotJoinedStatus
|
|
: st::groupCallMemberInactiveIcon),
|
|
(narrow
|
|
? st::groupCallMemberActiveStatus
|
|
: state.mutedByMe
|
|
? st::groupCallMemberMutedIcon
|
|
: st::groupCallMemberActiveIcon),
|
|
state.speaking);
|
|
const auto iconColor = anim::color(
|
|
activeInactiveColor,
|
|
st::groupCallMemberMutedIcon,
|
|
state.muted);
|
|
const auto color = video
|
|
? std::nullopt
|
|
: std::make_optional((narrow && state.mutedByMe)
|
|
? st::groupCallMemberMutedIcon->c
|
|
: (narrow && state.raisedHand)
|
|
? st::groupCallMemberInactiveStatus->c
|
|
: iconColor);
|
|
|
|
// Don't use caching of the last frame,
|
|
// because 'muted' may animate color.
|
|
const auto crossProgress = std::min(1. - state.active, 0.9999);
|
|
auto &line = video
|
|
? _videoCrossLine
|
|
: narrow
|
|
? _inactiveNarrowCrossLine
|
|
: _inactiveCrossLine;
|
|
line.paint(p, left, top, crossProgress, color);
|
|
}
|
|
|
|
int Members::Controller::rowPaintStatusIcon(
|
|
Painter &p,
|
|
int x,
|
|
int y,
|
|
int outerWidth,
|
|
not_null<MembersRow*> row,
|
|
const IconState &state) {
|
|
Expects(state.style == MembersRowStyle::Narrow);
|
|
|
|
if (_mode != PanelMode::Wide) {
|
|
return 0;
|
|
}
|
|
const auto &icon = st::groupCallNarrowColoredCrossLine.icon;
|
|
x += st::groupCallNarrowIconPosition.x();
|
|
y += st::groupCallNarrowIconPosition.y();
|
|
const auto rect = QRect(x, y, icon.width(), icon.height());
|
|
rowPaintIcon(p, rect, state);
|
|
x += icon.width();
|
|
auto result = st::groupCallNarrowIconSkip;
|
|
const auto participantPeer = row->peer();
|
|
const auto camera = _cameraActive.contains(participantPeer);
|
|
const auto screen = _screenActive.contains(participantPeer);
|
|
if (camera || screen) {
|
|
const auto activeInactiveColor = anim::color(
|
|
st::groupCallMemberNotJoinedStatus,
|
|
st::groupCallMemberActiveStatus,
|
|
state.speaking);
|
|
const auto iconColor = anim::color(
|
|
activeInactiveColor,
|
|
st::groupCallMemberNotJoinedStatus,
|
|
state.muted);
|
|
const auto other = state.mutedByMe
|
|
? st::groupCallMemberMutedIcon->c
|
|
: state.raisedHand
|
|
? st::groupCallMemberInactiveStatus->c
|
|
: iconColor;
|
|
if (camera) {
|
|
st::groupCallNarrowCameraIcon.paint(p, x, y, outerWidth, other);
|
|
x += st::groupCallNarrowCameraIcon.width();
|
|
result += st::groupCallNarrowCameraIcon.width();
|
|
}
|
|
if (screen) {
|
|
st::groupCallNarrowScreenIcon.paint(p, x, y, outerWidth, other);
|
|
x += st::groupCallNarrowScreenIcon.width();
|
|
result += st::groupCallNarrowScreenIcon.width();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Members::Controller::rowIsNarrow() {
|
|
return (_mode == PanelMode::Wide);
|
|
}
|
|
|
|
void Members::Controller::rowShowContextMenu(not_null<PeerListRow*> row) {
|
|
showRowMenu(row, false);
|
|
}
|
|
|
|
auto Members::Controller::kickParticipantRequests() const
|
|
-> rpl::producer<not_null<PeerData*>>{
|
|
return _kickParticipantRequests.events();
|
|
}
|
|
|
|
void Members::Controller::rowClicked(not_null<PeerListRow*> row) {
|
|
showRowMenu(row, true);
|
|
}
|
|
|
|
void Members::Controller::showRowMenu(
|
|
not_null<PeerListRow*> row,
|
|
bool highlightRow) {
|
|
const auto cleanup = [=](not_null<Ui::PopupMenu*> menu) {
|
|
if (!_menu || _menu.get() != menu) {
|
|
return;
|
|
}
|
|
auto saved = base::take(_menu);
|
|
for (const auto &peer : base::take(_menuCheckRowsAfterHidden)) {
|
|
if (const auto row = findRow(peer)) {
|
|
checkRowPosition(row);
|
|
}
|
|
}
|
|
_menu = std::move(saved);
|
|
};
|
|
delegate()->peerListShowRowMenu(row, highlightRow, cleanup);
|
|
}
|
|
|
|
void Members::Controller::rowRightActionClicked(
|
|
not_null<PeerListRow*> row) {
|
|
showRowMenu(row, true);
|
|
}
|
|
|
|
base::unique_qptr<Ui::PopupMenu> Members::Controller::rowContextMenu(
|
|
QWidget *parent,
|
|
not_null<PeerListRow*> row) {
|
|
auto result = createRowContextMenu(parent, row);
|
|
|
|
if (result) {
|
|
// First clear _menu value, so that we don't check row positions yet.
|
|
base::take(_menu);
|
|
|
|
// Here unique_qptr is used like a shared pointer, where
|
|
// not the last destroyed pointer destroys the object, but the first.
|
|
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|
QWidget *parent,
|
|
not_null<PeerListRow*> row) {
|
|
const auto participantPeer = row->peer();
|
|
const auto real = static_cast<Row*>(row.get());
|
|
const auto muteState = real->state();
|
|
const auto muted = (muteState == Row::State::Muted)
|
|
|| (muteState == Row::State::RaisedHand);
|
|
const auto addCover = !_call->rtmp();
|
|
const auto addVolumeItem = (!muted || isMe(participantPeer));
|
|
const auto admin = IsGroupCallAdmin(_peer, participantPeer);
|
|
const auto session = &_peer->session();
|
|
const auto getCurrentWindow = [=]() -> Window::SessionController* {
|
|
if (const auto window = Core::App().separateWindowForPeer(
|
|
participantPeer)) {
|
|
return window->sessionController();
|
|
} else if (const auto window = Core::App().primaryWindow()) {
|
|
if (const auto controller = window->sessionController()) {
|
|
if (&controller->session() == session) {
|
|
return controller;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
};
|
|
const auto getWindow = [=] {
|
|
if (const auto current = getCurrentWindow()) {
|
|
return current;
|
|
} else if (&Core::App().domain().active() != &session->account()) {
|
|
Core::App().domain().activate(&session->account());
|
|
}
|
|
return getCurrentWindow();
|
|
};
|
|
|
|
auto result = base::make_unique_q<Ui::PopupMenu>(
|
|
parent,
|
|
(addCover
|
|
? st::groupCallPopupMenuWithCover
|
|
: addVolumeItem
|
|
? st::groupCallPopupMenuWithVolume
|
|
: st::groupCallPopupMenu));
|
|
const auto weakMenu = Ui::MakeWeak(result.get());
|
|
const auto withActiveWindow = [=](auto callback) {
|
|
if (const auto window = getWindow()) {
|
|
if (const auto menu = weakMenu.data()) {
|
|
menu->discardParentReActivate();
|
|
|
|
// We must hide PopupMenu before we activate the MainWindow,
|
|
// otherwise we set focus in field inside MainWindow and then
|
|
// PopupMenu::hide activates back the group call panel :(
|
|
delete weakMenu;
|
|
}
|
|
callback(window);
|
|
window->widget()->activate();
|
|
}
|
|
};
|
|
const auto showProfile = [=] {
|
|
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
|
window->showPeerInfo(participantPeer);
|
|
});
|
|
};
|
|
const auto showHistory = [=] {
|
|
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
|
window->showPeerHistory(
|
|
participantPeer,
|
|
Window::SectionShow::Way::Forward);
|
|
});
|
|
};
|
|
const auto removeFromVoiceChat = crl::guard(this, [=] {
|
|
_kickParticipantRequests.fire_copy(participantPeer);
|
|
});
|
|
|
|
if (addCover) {
|
|
result->addAction(base::make_unique_q<CoverItem>(
|
|
result->menu(),
|
|
st::groupCallPopupCoverMenu,
|
|
st::groupCallMenuCover,
|
|
Info::Profile::NameValue(
|
|
participantPeer
|
|
) | rpl::map([](const auto &text) { return text.text; }),
|
|
PrepareShortInfoStatus(participantPeer),
|
|
PrepareShortInfoUserpic(
|
|
participantPeer,
|
|
st::groupCallMenuCover)));
|
|
|
|
if (const auto about = participantPeer->about(); !about.isEmpty()) {
|
|
result->addAction(base::make_unique_q<AboutItem>(
|
|
result->menu(),
|
|
st::groupCallPopupCoverMenu,
|
|
Info::Profile::AboutWithEntities(participantPeer, about)));
|
|
}
|
|
}
|
|
|
|
if (const auto real = _call->lookupReal()) {
|
|
auto oneFound = false;
|
|
auto hasTwoOrMore = false;
|
|
const auto &shown = _call->shownVideoTracks();
|
|
for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
|
|
if (shown.contains(endpoint)) {
|
|
if (oneFound) {
|
|
hasTwoOrMore = true;
|
|
break;
|
|
}
|
|
oneFound = true;
|
|
}
|
|
}
|
|
const auto participant = real->participantByPeer(participantPeer);
|
|
if (participant && hasTwoOrMore) {
|
|
const auto &large = _call->videoEndpointLarge();
|
|
const auto pinned = _call->videoEndpointPinned();
|
|
const auto camera = VideoEndpoint{
|
|
VideoEndpointType::Camera,
|
|
participantPeer,
|
|
computeCameraEndpoint(participant),
|
|
};
|
|
const auto screen = VideoEndpoint{
|
|
VideoEndpointType::Screen,
|
|
participantPeer,
|
|
computeScreenEndpoint(participant),
|
|
};
|
|
if (shown.contains(camera)) {
|
|
if (pinned && large == camera) {
|
|
result->addAction(
|
|
tr::lng_group_call_context_unpin_camera(tr::now),
|
|
[=] { _call->pinVideoEndpoint({}); });
|
|
} else {
|
|
result->addAction(
|
|
tr::lng_group_call_context_pin_camera(tr::now),
|
|
[=] { _call->pinVideoEndpoint(camera); });
|
|
}
|
|
}
|
|
if (shown.contains(screen)) {
|
|
if (pinned && large == screen) {
|
|
result->addAction(
|
|
tr::lng_group_call_context_unpin_screen(tr::now),
|
|
[=] { _call->pinVideoEndpoint({}); });
|
|
} else {
|
|
result->addAction(
|
|
tr::lng_group_call_context_pin_screen(tr::now),
|
|
[=] { _call->pinVideoEndpoint(screen); });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_call->rtmp()) {
|
|
addMuteActionsToContextMenu(
|
|
result,
|
|
row->peer(),
|
|
false,
|
|
static_cast<Row*>(row.get()));
|
|
} else if (participant
|
|
&& (!isMe(participantPeer) || _peer->canManageGroupCall())
|
|
&& (participant->ssrc != 0
|
|
|| GetAdditionalAudioSsrc(participant->videoParams) != 0)) {
|
|
addMuteActionsToContextMenu(
|
|
result,
|
|
participantPeer,
|
|
admin,
|
|
static_cast<Row*>(row.get()));
|
|
}
|
|
}
|
|
|
|
if (isMe(participantPeer)) {
|
|
if (_call->muted() == MuteState::RaisedHand) {
|
|
const auto removeHand = [=] {
|
|
if (_call->muted() == MuteState::RaisedHand) {
|
|
_call->setMutedAndUpdate(MuteState::ForceMuted);
|
|
}
|
|
};
|
|
result->addAction(
|
|
tr::lng_group_call_context_remove_hand(tr::now),
|
|
removeHand);
|
|
}
|
|
} else {
|
|
result->addAction(
|
|
(participantPeer->isUser()
|
|
? tr::lng_context_view_profile(tr::now)
|
|
: participantPeer->isBroadcast()
|
|
? tr::lng_context_view_channel(tr::now)
|
|
: tr::lng_context_view_group(tr::now)),
|
|
showProfile);
|
|
if (participantPeer->isUser()) {
|
|
result->addAction(
|
|
tr::lng_context_send_message(tr::now),
|
|
showHistory);
|
|
}
|
|
const auto canKick = [&] {
|
|
const auto user = participantPeer->asUser();
|
|
if (static_cast<Row*>(row.get())->state()
|
|
== Row::State::Invited) {
|
|
return false;
|
|
} else if (const auto chat = _peer->asChat()) {
|
|
return chat->amCreator()
|
|
|| (user
|
|
&& chat->canBanMembers()
|
|
&& !chat->admins.contains(user));
|
|
} else if (const auto channel = _peer->asChannel()) {
|
|
return !participantPeer->isMegagroup() // That's the creator.
|
|
&& channel->canRestrictParticipant(participantPeer);
|
|
}
|
|
return false;
|
|
}();
|
|
if (canKick) {
|
|
result->addAction(MakeAttentionAction(
|
|
result->menu(),
|
|
tr::lng_group_call_context_remove(tr::now),
|
|
removeFromVoiceChat));
|
|
}
|
|
}
|
|
if (result->actions().size() < (addCover ? 2 : 1)) {
|
|
return nullptr;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Members::Controller::addMuteActionsToContextMenu(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
not_null<PeerData*> participantPeer,
|
|
bool participantIsCallAdmin,
|
|
not_null<Row*> row) {
|
|
const auto muteUnmuteString = [=](bool muted, bool mutedByMe) {
|
|
return (muted && _peer->canManageGroupCall())
|
|
? tr::lng_group_call_context_unmute(tr::now)
|
|
: mutedByMe
|
|
? tr::lng_group_call_context_unmute_for_me(tr::now)
|
|
: _peer->canManageGroupCall()
|
|
? tr::lng_group_call_context_mute(tr::now)
|
|
: tr::lng_group_call_context_mute_for_me(tr::now);
|
|
};
|
|
|
|
const auto toggleMute = crl::guard(this, [=](bool mute, bool local) {
|
|
_toggleMuteRequests.fire(Group::MuteRequest{
|
|
.peer = participantPeer,
|
|
.mute = mute,
|
|
.locallyOnly = local,
|
|
});
|
|
});
|
|
const auto changeVolume = crl::guard(this, [=](
|
|
int volume,
|
|
bool local) {
|
|
_changeVolumeRequests.fire(Group::VolumeRequest{
|
|
.peer = participantPeer,
|
|
.volume = std::clamp(volume, 1, Group::kMaxVolume),
|
|
.locallyOnly = local,
|
|
});
|
|
});
|
|
|
|
const auto muteState = row->state();
|
|
const auto muted = (muteState == Row::State::Muted)
|
|
|| (muteState == Row::State::RaisedHand);
|
|
const auto mutedByMe = row->mutedByMe();
|
|
|
|
auto mutesFromVolume = rpl::never<bool>() | rpl::type_erased();
|
|
|
|
const auto addVolumeItem = (!muted || isMe(participantPeer));
|
|
if (addVolumeItem) {
|
|
auto otherParticipantStateValue
|
|
= _call->otherParticipantStateValue(
|
|
) | rpl::filter([=](const Group::ParticipantState &data) {
|
|
return data.peer == participantPeer;
|
|
});
|
|
|
|
auto volumeItem = base::make_unique_q<MenuVolumeItem>(
|
|
menu->menu(),
|
|
st::groupCallPopupVolumeMenu,
|
|
otherParticipantStateValue,
|
|
_call->rtmp() ? _call->rtmpVolume() : row->volume(),
|
|
Group::kMaxVolume,
|
|
muted);
|
|
|
|
mutesFromVolume = volumeItem->toggleMuteRequests();
|
|
|
|
volumeItem->toggleMuteRequests(
|
|
) | rpl::start_with_next([=](bool muted) {
|
|
if (muted) {
|
|
// Slider value is changed after the callback is called.
|
|
// To capture good state inside the slider frame we postpone.
|
|
crl::on_main(menu, [=] {
|
|
menu->hideMenu();
|
|
});
|
|
}
|
|
toggleMute(muted, false);
|
|
}, volumeItem->lifetime());
|
|
|
|
volumeItem->toggleMuteLocallyRequests(
|
|
) | rpl::start_with_next([=](bool muted) {
|
|
if (!isMe(participantPeer)) {
|
|
toggleMute(muted, true);
|
|
}
|
|
}, volumeItem->lifetime());
|
|
|
|
volumeItem->changeVolumeRequests(
|
|
) | rpl::start_with_next([=](int volume) {
|
|
changeVolume(volume, false);
|
|
}, volumeItem->lifetime());
|
|
|
|
volumeItem->changeVolumeLocallyRequests(
|
|
) | rpl::start_with_next([=](int volume) {
|
|
if (!isMe(participantPeer)) {
|
|
changeVolume(volume, true);
|
|
}
|
|
}, volumeItem->lifetime());
|
|
|
|
if (menu->actions().size() > 1) { // First - cover.
|
|
menu->addSeparator();
|
|
}
|
|
|
|
menu->addAction(std::move(volumeItem));
|
|
|
|
if (!_call->rtmp() && !isMe(participantPeer)) {
|
|
menu->addSeparator();
|
|
}
|
|
};
|
|
|
|
const auto muteAction = [&]() -> QAction* {
|
|
if (muteState == Row::State::Invited
|
|
|| _call->rtmp()
|
|
|| isMe(participantPeer)
|
|
|| (muteState == Row::State::Inactive
|
|
&& participantIsCallAdmin
|
|
&& _peer->canManageGroupCall())) {
|
|
return nullptr;
|
|
}
|
|
auto callback = [=] {
|
|
const auto state = row->state();
|
|
const auto muted = (state == Row::State::Muted)
|
|
|| (state == Row::State::RaisedHand);
|
|
const auto mutedByMe = row->mutedByMe();
|
|
toggleMute(!mutedByMe && (!_call->canManage() || !muted), false);
|
|
};
|
|
return menu->addAction(
|
|
muteUnmuteString(muted, mutedByMe),
|
|
std::move(callback));
|
|
}();
|
|
|
|
if (muteAction) {
|
|
std::move(
|
|
mutesFromVolume
|
|
) | rpl::start_with_next([=](bool mutedFromVolume) {
|
|
const auto state = _call->canManage()
|
|
? (mutedFromVolume
|
|
? (row->raisedHandRating()
|
|
? Row::State::RaisedHand
|
|
: Row::State::Muted)
|
|
: Row::State::Inactive)
|
|
: row->state();
|
|
const auto muted = (state == Row::State::Muted)
|
|
|| (state == Row::State::RaisedHand);
|
|
const auto mutedByMe = _call->canManage()
|
|
? false
|
|
: mutedFromVolume;
|
|
muteAction->setText(muteUnmuteString(muted, mutedByMe));
|
|
}, menu->lifetime());
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Row> Members::Controller::createRowForMe() {
|
|
auto result = std::make_unique<Row>(this, _call->joinAs());
|
|
updateRow(result.get(), std::nullopt, nullptr);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<Row> Members::Controller::createRow(
|
|
const Data::GroupCallParticipant &participant) {
|
|
auto result = std::make_unique<Row>(this, participant.peer);
|
|
updateRow(result.get(), std::nullopt, &participant);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<Row> Members::Controller::createInvitedRow(
|
|
not_null<PeerData*> participantPeer) {
|
|
if (findRow(participantPeer)) {
|
|
return nullptr;
|
|
}
|
|
auto result = std::make_unique<Row>(this, participantPeer);
|
|
updateRow(result.get(), std::nullopt, nullptr);
|
|
return result;
|
|
}
|
|
|
|
Members::Members(
|
|
not_null<QWidget*> parent,
|
|
not_null<GroupCall*> call,
|
|
PanelMode mode,
|
|
Ui::GL::Backend backend)
|
|
: RpWidget(parent)
|
|
, _call(call)
|
|
, _mode(mode)
|
|
, _scroll(this)
|
|
, _listController(std::make_unique<Controller>(call, parent, mode))
|
|
, _layout(_scroll->setOwnedWidget(
|
|
object_ptr<Ui::VerticalLayout>(_scroll.data())))
|
|
, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))
|
|
, _viewport(
|
|
std::make_unique<Viewport>(
|
|
_videoWrap.get(),
|
|
PanelMode::Default,
|
|
backend)) {
|
|
setupList();
|
|
setupAddMember(call);
|
|
setContent(_list);
|
|
setupFakeRoundCorners();
|
|
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
|
|
trackViewportGeometry();
|
|
}
|
|
|
|
Members::~Members() {
|
|
_viewport = nullptr;
|
|
}
|
|
|
|
auto Members::toggleMuteRequests() const
|
|
-> rpl::producer<Group::MuteRequest> {
|
|
return _listController->toggleMuteRequests();
|
|
}
|
|
|
|
auto Members::changeVolumeRequests() const
|
|
-> rpl::producer<Group::VolumeRequest> {
|
|
return _listController->changeVolumeRequests();
|
|
}
|
|
|
|
auto Members::kickParticipantRequests() const
|
|
-> rpl::producer<not_null<PeerData*>> {
|
|
return _listController->kickParticipantRequests();
|
|
}
|
|
|
|
not_null<Viewport*> Members::viewport() const {
|
|
return _viewport.get();
|
|
}
|
|
|
|
int Members::desiredHeight() const {
|
|
const auto count = [&] {
|
|
if (const auto real = _call->lookupReal()) {
|
|
return real->fullCount();
|
|
}
|
|
return 0;
|
|
}();
|
|
const auto use = std::max(count, _list->fullRowsCount());
|
|
const auto single = st::groupCallMembersList.item.height;
|
|
const auto desired = (_layout->height() - _list->height())
|
|
+ (use * single)
|
|
+ (use ? st::lineWidth : 0);
|
|
return std::max(height(), desired);
|
|
}
|
|
|
|
rpl::producer<int> Members::desiredHeightValue() const {
|
|
return rpl::combine(
|
|
heightValue(),
|
|
_addMemberButton.value(),
|
|
_listController->fullCountValue(),
|
|
_mode.value()
|
|
) | rpl::map([=] {
|
|
return desiredHeight();
|
|
});
|
|
}
|
|
|
|
void Members::setupAddMember(not_null<GroupCall*> call) {
|
|
using namespace rpl::mappers;
|
|
|
|
const auto peer = call->peer();
|
|
const auto canAddByPeer = [=](not_null<PeerData*> peer) {
|
|
if (peer->isBroadcast()) {
|
|
return rpl::single(false) | rpl::type_erased();
|
|
}
|
|
return rpl::combine(
|
|
Data::CanWriteValue(peer.get()),
|
|
_call->joinAsValue()
|
|
) | rpl::map([=](bool can, not_null<PeerData*> joinAs) {
|
|
return can && joinAs->isSelf();
|
|
}) | rpl::type_erased();
|
|
};
|
|
const auto canInviteByLinkByPeer = [=](not_null<PeerData*> peer) {
|
|
const auto channel = peer->asChannel();
|
|
if (!channel) {
|
|
return rpl::single(false) | rpl::type_erased();
|
|
}
|
|
return rpl::single(
|
|
false
|
|
) | rpl::then(_call->real(
|
|
) | rpl::map([=] {
|
|
return Data::PeerFlagValue(
|
|
channel,
|
|
ChannelDataFlag::Username);
|
|
}) | rpl::flatten_latest()) | rpl::type_erased();
|
|
};
|
|
_canAddMembers = canAddByPeer(peer);
|
|
_canInviteByLink = canInviteByLinkByPeer(peer);
|
|
SubscribeToMigration(
|
|
peer,
|
|
lifetime(),
|
|
[=](not_null<ChannelData*> channel) {
|
|
_canAddMembers = canAddByPeer(channel);
|
|
_canInviteByLink = canInviteByLinkByPeer(channel);
|
|
});
|
|
|
|
rpl::combine(
|
|
_canAddMembers.value(),
|
|
_canInviteByLink.value(),
|
|
_mode.value()
|
|
) | rpl::start_with_next([=](bool add, bool invite, PanelMode mode) {
|
|
if (!add && !invite) {
|
|
if (const auto old = _addMemberButton.current()) {
|
|
delete old;
|
|
_addMemberButton = nullptr;
|
|
updateControlsGeometry();
|
|
}
|
|
return;
|
|
}
|
|
auto addMember = Settings::CreateButton(
|
|
_layout.get(),
|
|
tr::lng_group_call_invite(),
|
|
st::groupCallAddMember,
|
|
{ .icon = &st::groupCallAddMemberIcon });
|
|
addMember->clicks(
|
|
) | rpl::to_empty | rpl::start_to_stream(
|
|
_addMemberRequests,
|
|
addMember->lifetime());
|
|
addMember->show();
|
|
addMember->resizeToWidth(_layout->width());
|
|
delete _addMemberButton.current();
|
|
_addMemberButton = addMember.data();
|
|
_layout->insert(3, std::move(addMember));
|
|
}, lifetime());
|
|
|
|
updateControlsGeometry();
|
|
}
|
|
|
|
Row *Members::lookupRow(not_null<PeerData*> peer) const {
|
|
return _listController->findRow(peer);
|
|
}
|
|
|
|
not_null<MembersRow*> Members::rtmpFakeRow(not_null<PeerData*> peer) const {
|
|
if (!_rtmpFakeRow) {
|
|
_rtmpFakeRow = std::make_unique<Row>(_listController.get(), peer);
|
|
}
|
|
return _rtmpFakeRow.get();
|
|
}
|
|
|
|
void Members::setMode(PanelMode mode) {
|
|
if (_mode.current() == mode) {
|
|
return;
|
|
}
|
|
_mode = mode;
|
|
_listController->setMode(mode);
|
|
}
|
|
|
|
QRect Members::getInnerGeometry() const {
|
|
const auto addMembers = _addMemberButton.current();
|
|
const auto add = addMembers ? addMembers->height() : 0;
|
|
return QRect(
|
|
0,
|
|
-_scroll->scrollTop(),
|
|
width(),
|
|
_list->y() + _list->height() + _bottomSkip->height() + add);
|
|
}
|
|
|
|
rpl::producer<int> Members::fullCountValue() const {
|
|
return _listController->fullCountValue();
|
|
}
|
|
|
|
void Members::setupList() {
|
|
_listController->setStyleOverrides(&st::groupCallMembersList);
|
|
const auto addSkip = [&] {
|
|
const auto result = _layout->add(
|
|
object_ptr<Ui::FixedHeightWidget>(
|
|
_layout.get(),
|
|
st::groupCallMembersTopSkip));
|
|
result->paintRequest(
|
|
) | rpl::start_with_next([=](QRect clip) {
|
|
QPainter(result).fillRect(clip, st::groupCallMembersBg);
|
|
}, result->lifetime());
|
|
return result;
|
|
};
|
|
_topSkip = addSkip();
|
|
_list = _layout->add(
|
|
object_ptr<ListWidget>(
|
|
_layout.get(),
|
|
_listController.get()));
|
|
_bottomSkip = addSkip();
|
|
|
|
using namespace rpl::mappers;
|
|
rpl::combine(
|
|
_list->heightValue() | rpl::map(_1 > 0),
|
|
_addMemberButton.value() | rpl::map(_1 != nullptr)
|
|
) | rpl::distinct_until_changed(
|
|
) | rpl::start_with_next([=](bool hasList, bool hasAddMembers) {
|
|
_topSkip->resize(
|
|
_topSkip->width(),
|
|
hasList ? st::groupCallMembersTopSkip : 0);
|
|
_bottomSkip->resize(
|
|
_bottomSkip->width(),
|
|
(hasList && !hasAddMembers) ? st::groupCallMembersTopSkip : 0);
|
|
}, _list->lifetime());
|
|
|
|
const auto skip = _layout->add(object_ptr<Ui::RpWidget>(_layout.get()));
|
|
_mode.value(
|
|
) | rpl::start_with_next([=](PanelMode mode) {
|
|
skip->resize(skip->width(), (mode == PanelMode::Default)
|
|
? st::groupCallMembersBottomSkip
|
|
: 0);
|
|
}, skip->lifetime());
|
|
|
|
rpl::combine(
|
|
_mode.value(),
|
|
_layout->heightValue()
|
|
) | rpl::start_with_next([=] {
|
|
resizeToList();
|
|
}, _layout->lifetime());
|
|
|
|
rpl::combine(
|
|
_scroll->scrollTopValue(),
|
|
_scroll->heightValue()
|
|
) | rpl::start_with_next([=](int scrollTop, int scrollHeight) {
|
|
_layout->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight);
|
|
}, _scroll->lifetime());
|
|
}
|
|
|
|
void Members::trackViewportGeometry() {
|
|
_call->videoEndpointLargeValue(
|
|
) | rpl::start_with_next([=](const VideoEndpoint &large) {
|
|
_viewport->showLarge(large);
|
|
}, _viewport->lifetime());
|
|
|
|
const auto move = [=] {
|
|
const auto maxTop = _viewport->fullHeight()
|
|
- _viewport->widget()->height();
|
|
if (maxTop < 0) {
|
|
return;
|
|
}
|
|
const auto scrollTop = _scroll->scrollTop();
|
|
const auto shift = std::min(scrollTop, maxTop);
|
|
_viewport->setScrollTop(shift);
|
|
if (_viewport->widget()->y() != shift) {
|
|
_viewport->widget()->move(0, shift);
|
|
}
|
|
};
|
|
const auto resize = [=] {
|
|
_viewport->widget()->resize(
|
|
_layout->width(),
|
|
std::min(_scroll->height(), _viewport->fullHeight()));
|
|
};
|
|
_layout->widthValue(
|
|
) | rpl::start_with_next([=](int width) {
|
|
_viewport->resizeToWidth(width);
|
|
resize();
|
|
}, _viewport->lifetime());
|
|
|
|
_scroll->heightValue(
|
|
) | rpl::skip(1) | rpl::start_with_next(resize, _viewport->lifetime());
|
|
|
|
_scroll->scrollTopValue(
|
|
) | rpl::skip(1) | rpl::start_with_next(move, _viewport->lifetime());
|
|
|
|
_viewport->fullHeightValue(
|
|
) | rpl::start_with_next([=](int viewport) {
|
|
_videoWrap->resize(_videoWrap->width(), viewport);
|
|
if (viewport > 0) {
|
|
move();
|
|
resize();
|
|
}
|
|
}, _viewport->lifetime());
|
|
}
|
|
|
|
void Members::resizeEvent(QResizeEvent *e) {
|
|
updateControlsGeometry();
|
|
}
|
|
|
|
void Members::resizeToList() {
|
|
if (!_list) {
|
|
return;
|
|
}
|
|
const auto newHeight = (_list->height() > 0)
|
|
? (_layout->height() + st::lineWidth)
|
|
: 0;
|
|
if (height() == newHeight) {
|
|
updateControlsGeometry();
|
|
} else {
|
|
resize(width(), newHeight);
|
|
}
|
|
}
|
|
|
|
void Members::updateControlsGeometry() {
|
|
_scroll->setGeometry(rect());
|
|
_layout->resizeToWidth(width());
|
|
}
|
|
|
|
void Members::setupFakeRoundCorners() {
|
|
const auto size = st::roundRadiusLarge;
|
|
const auto full = 3 * size;
|
|
const auto imagePartSize = size * cIntRetinaFactor();
|
|
const auto imageSize = full * cIntRetinaFactor();
|
|
const auto image = std::make_shared<QImage>(
|
|
QImage(imageSize, imageSize, QImage::Format_ARGB32_Premultiplied));
|
|
image->setDevicePixelRatio(cRetinaFactor());
|
|
|
|
const auto refreshImage = [=] {
|
|
image->fill(st::groupCallBg->c);
|
|
{
|
|
QPainter p(image.get());
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(Qt::transparent);
|
|
p.drawRoundedRect(0, 0, full, full, size, size);
|
|
}
|
|
};
|
|
|
|
const auto create = [&](QPoint imagePartOrigin) {
|
|
const auto result = Ui::CreateChild<Ui::RpWidget>(_layout.get());
|
|
result->show();
|
|
result->resize(size, size);
|
|
result->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
result->paintRequest(
|
|
) | rpl::start_with_next([=] {
|
|
QPainter(result).drawImage(
|
|
result->rect(),
|
|
*image,
|
|
QRect(imagePartOrigin, QSize(imagePartSize, imagePartSize)));
|
|
}, result->lifetime());
|
|
result->raise();
|
|
return result;
|
|
};
|
|
const auto shift = imageSize - imagePartSize;
|
|
const auto topleft = create({ 0, 0 });
|
|
const auto topright = create({ shift, 0 });
|
|
const auto bottomleft = create({ 0, shift });
|
|
const auto bottomright = create({ shift, shift });
|
|
|
|
rpl::combine(
|
|
_list->geometryValue(),
|
|
_addMemberButton.value() | rpl::map([=](Ui::RpWidget *widget) {
|
|
topleft->raise();
|
|
topright->raise();
|
|
bottomleft->raise();
|
|
bottomright->raise();
|
|
return widget ? widget->heightValue() : rpl::single(0);
|
|
}) | rpl::flatten_latest()
|
|
) | rpl::start_with_next([=](QRect list, int addMembers) {
|
|
const auto left = list.x();
|
|
const auto top = list.y() - _topSkip->height();
|
|
const auto right = left + list.width() - topright->width();
|
|
const auto bottom = top
|
|
+ _topSkip->height()
|
|
+ list.height()
|
|
+ _bottomSkip->height()
|
|
+ addMembers
|
|
- bottomleft->height();
|
|
topleft->move(left, top);
|
|
topright->move(right, top);
|
|
bottomleft->move(left, bottom);
|
|
bottomright->move(right, bottom);
|
|
}, lifetime());
|
|
|
|
refreshImage();
|
|
style::PaletteChanged(
|
|
) | rpl::start_with_next([=] {
|
|
refreshImage();
|
|
topleft->update();
|
|
topright->update();
|
|
bottomleft->update();
|
|
bottomright->update();
|
|
}, lifetime());
|
|
}
|
|
|
|
void Members::peerListSetTitle(rpl::producer<QString> title) {
|
|
}
|
|
|
|
void Members::peerListSetAdditionalTitle(rpl::producer<QString> title) {
|
|
}
|
|
|
|
void Members::peerListSetHideEmpty(bool hide) {
|
|
}
|
|
|
|
bool Members::peerListIsRowChecked(not_null<PeerListRow*> row) {
|
|
return false;
|
|
}
|
|
|
|
void Members::peerListScrollToTop() {
|
|
}
|
|
|
|
int Members::peerListSelectedRowsCount() {
|
|
return 0;
|
|
}
|
|
|
|
void Members::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
|
|
Unexpected("Item selection in Calls::Members.");
|
|
}
|
|
|
|
void Members::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
|
|
Unexpected("Item selection in Calls::Members.");
|
|
}
|
|
|
|
void Members::peerListFinishSelectedRowsBunch() {
|
|
}
|
|
|
|
void Members::peerListSetDescription(
|
|
object_ptr<Ui::FlatLabel> description) {
|
|
description.destroy();
|
|
}
|
|
|
|
void Members::peerListShowBox(
|
|
object_ptr<Ui::BoxContent> content,
|
|
Ui::LayerOptions options) {
|
|
}
|
|
|
|
void Members::peerListHideLayer() {
|
|
}
|
|
|
|
not_null<QWidget*> Members::peerListToastParent() {
|
|
Unexpected("...Members::peerListToastParent");
|
|
}
|
|
|
|
} // namespace Calls::Group
|