Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486424af4f | ||
|
|
f3614d6402 | ||
|
|
71151f6bf6 | ||
|
|
2c0ef9c4e9 | ||
|
|
930e971881 | ||
|
|
a576025d4f | ||
|
|
bcd2560e8f | ||
|
|
aede42b0b6 | ||
|
|
56728a066e | ||
|
|
1951b7a8a1 | ||
|
|
0dc0f588c4 | ||
|
|
7d22c631ca | ||
|
|
cf5cc3646a | ||
|
|
375820d5cf | ||
|
|
3955543699 | ||
|
|
c03da00e37 | ||
|
|
edcd462fb9 | ||
|
|
e99558abeb | ||
|
|
feff514a07 | ||
|
|
b1b25b0df9 | ||
|
|
dfee8238c6 |
1
.github/workflows/snap.yml
vendored
1
.github/workflows/snap.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: First set up.
|
||||
|
||||
@@ -72,7 +72,6 @@ PRIVATE
|
||||
if (LINUX)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
desktop-app::external_xcb_screensaver
|
||||
desktop-app::external_xcb
|
||||
desktop-app::external_glib
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.5.0.0" />
|
||||
Version="2.5.2.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,5,0,0
|
||||
PRODUCTVERSION 2,5,0,0
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.5.0.0"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.5.0.0"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,5,0,0
|
||||
PRODUCTVERSION 2,5,0,0
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.5.0.0"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.5.0.0"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -928,6 +928,9 @@ void Updates::handleSendActionUpdate(
|
||||
const auto isSpeakingInCall = (action.type()
|
||||
== mtpc_speakingInGroupCallAction);
|
||||
if (isSpeakingInCall) {
|
||||
if (!peer->isChat() && !peer->isChannel()) {
|
||||
return;
|
||||
}
|
||||
const auto call = peer->groupCall();
|
||||
const auto now = crl::now();
|
||||
if (call) {
|
||||
@@ -943,8 +946,8 @@ void Updates::handleSendActionUpdate(
|
||||
: (channel->flags() & MTPDchannel::Flag::f_call_active);
|
||||
if (active) {
|
||||
_pendingSpeakingCallMembers.emplace(
|
||||
channel).first->second[userId] = now;
|
||||
session().api().requestFullPeer(channel);
|
||||
peer).first->second[userId] = now;
|
||||
session().api().requestFullPeer(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,37 +1229,56 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) {
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
|
||||
void PeerListContent::showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
showRowMenu(findRowIndex(row), QCursor::pos(), std::move(destroyed));
|
||||
}
|
||||
|
||||
bool PeerListContent::showRowMenu(
|
||||
RowIndex index,
|
||||
QPoint globalPos,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
if (_contextMenu) {
|
||||
_contextMenu->deleteLater();
|
||||
_contextMenu->setDestroyedCallback(nullptr);
|
||||
_contextMenu = nullptr;
|
||||
}
|
||||
setContexted(Selected());
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
|
||||
setContexted(_selected);
|
||||
if (_pressButton != Qt::LeftButton) {
|
||||
mousePressReleased(_pressButton);
|
||||
}
|
||||
|
||||
if (const auto row = getRow(_contexted.index)) {
|
||||
_contextMenu = _controller->rowContextMenu(this, row);
|
||||
if (_contextMenu) {
|
||||
_contextMenu->setDestroyedCallback(crl::guard(
|
||||
this,
|
||||
[this] {
|
||||
setContexted(Selected());
|
||||
handleMouseMove(QCursor::pos());
|
||||
}));
|
||||
_contextMenu->popup(e->globalPos());
|
||||
e->accept();
|
||||
} else {
|
||||
const auto row = getRow(index);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_contextMenu = _controller->rowContextMenu(this, row);
|
||||
const auto raw = _contextMenu.get();
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setContexted({ index, false });
|
||||
raw->setDestroyedCallback(crl::guard(
|
||||
this,
|
||||
[=] {
|
||||
setContexted(Selected());
|
||||
}
|
||||
} else {
|
||||
setContexted(Selected());
|
||||
handleMouseMove(QCursor::pos());
|
||||
if (destroyed) {
|
||||
destroyed(raw);
|
||||
}
|
||||
}));
|
||||
raw->popup(globalPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
if (showRowMenu(_selected.index, e->globalPos())) {
|
||||
e->accept();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +300,9 @@ public:
|
||||
peerListFinishSelectedRowsBunch();
|
||||
}
|
||||
|
||||
virtual void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) = 0;
|
||||
virtual int peerListSelectedRowsCount() = 0;
|
||||
virtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;
|
||||
virtual void peerListRestoreState(
|
||||
@@ -557,6 +560,10 @@ public:
|
||||
std::unique_ptr<PeerListState> saveState() const;
|
||||
void restoreState(std::unique_ptr<PeerListState> state);
|
||||
|
||||
void showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed);
|
||||
|
||||
auto scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
@@ -640,6 +647,11 @@ private:
|
||||
RowIndex findRowIndex(not_null<PeerListRow*> row, RowIndex hint = RowIndex());
|
||||
QRect getActiveActionRect(not_null<PeerListRow*> row, RowIndex index) const;
|
||||
|
||||
bool showRowMenu(
|
||||
RowIndex index,
|
||||
QPoint globalPos,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr);
|
||||
|
||||
crl::time paintRow(Painter &p, crl::time ms, RowIndex index);
|
||||
|
||||
void addRowEntry(not_null<PeerListRow*> row);
|
||||
@@ -823,6 +835,11 @@ public:
|
||||
std::unique_ptr<PeerListState> state) override {
|
||||
_content->restoreState(std::move(state));
|
||||
}
|
||||
void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) override {
|
||||
_content->showRowMenu(row, std::move(destroyed));
|
||||
}
|
||||
|
||||
protected:
|
||||
not_null<PeerListContent*> content() const {
|
||||
|
||||
@@ -9,6 +9,7 @@ using "ui/basic.style";
|
||||
|
||||
using "ui/widgets/widgets.style";
|
||||
using "ui/layers/layers.style";
|
||||
using "ui/chat/chat.style"; // GroupCallUserpics
|
||||
using "window/window.style";
|
||||
|
||||
CallSignalBars {
|
||||
@@ -605,9 +606,12 @@ groupCallButtonSkip: 43px;
|
||||
groupCallButtonBottomSkip: 145px;
|
||||
groupCallMuteBottomSkip: 160px;
|
||||
|
||||
groupCallTopBarUserpicSize: 28px;
|
||||
groupCallTopBarUserpicShift: 8px;
|
||||
groupCallTopBarUserpicStroke: 2px;
|
||||
groupCallTopBarUserpics: GroupCallUserpics {
|
||||
size: 28px;
|
||||
shift: 8px;
|
||||
stroke: 2px;
|
||||
align: align(left);
|
||||
}
|
||||
groupCallTopBarJoin: RoundButton(defaultActiveButton) {
|
||||
width: -26px;
|
||||
height: 26px;
|
||||
|
||||
@@ -368,10 +368,10 @@ void Call::setupOutgoingVideo() {
|
||||
_errors.fire({ ErrorType::NoCamera });
|
||||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (_state.current() != State::Established
|
||||
&& state != started
|
||||
&& !_videoCapture) {
|
||||
&& (state != Webrtc::VideoState::Inactive)
|
||||
&& (started == Webrtc::VideoState::Inactive)) {
|
||||
_errors.fire({ ErrorType::NotStartedCall });
|
||||
_videoOutgoing->setState(started);
|
||||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (state != Webrtc::VideoState::Inactive
|
||||
&& _instance
|
||||
&& !_instance->supportsVideo()) {
|
||||
|
||||
@@ -243,6 +243,9 @@ private:
|
||||
void prepareRows(not_null<Data::GroupCall*> real);
|
||||
//void repaintByTimer();
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row);
|
||||
void setupListChangeViewers(not_null<GroupCall*> call);
|
||||
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
||||
void updateRow(
|
||||
@@ -638,10 +641,7 @@ MembersController::MembersController(
|
||||
}
|
||||
|
||||
MembersController::~MembersController() {
|
||||
if (_menu) {
|
||||
_menu->setDestroyedCallback(nullptr);
|
||||
_menu = nullptr;
|
||||
}
|
||||
base::take(_menu);
|
||||
}
|
||||
|
||||
void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
|
||||
@@ -750,7 +750,21 @@ void MembersController::updateRow(
|
||||
if (row->speaking()) {
|
||||
delegate()->peerListPrependRow(std::move(row));
|
||||
} else {
|
||||
static constexpr auto kInvited = Row::State::Invited;
|
||||
const auto reorder = [&] {
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
const auto row = delegate()->peerListRowAt(count - 1).get();
|
||||
return (static_cast<Row*>(row)->state() == kInvited);
|
||||
}();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (reorder) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
return static_cast<const Row&>(row).state() != kInvited;
|
||||
});
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
@@ -1002,29 +1016,20 @@ auto MembersController::kickMemberRequests() const
|
||||
}
|
||||
|
||||
void MembersController::rowClicked(not_null<PeerListRow*> row) {
|
||||
if (_menu) {
|
||||
_menu->setDestroyedCallback(nullptr);
|
||||
_menu->deleteLater();
|
||||
_menu = nullptr;
|
||||
}
|
||||
_menu = rowContextMenu(_menuParent, row);
|
||||
if (const auto raw = _menu.get()) {
|
||||
raw->setDestroyedCallback([=] {
|
||||
if (_menu && _menu.get() != raw) {
|
||||
return;
|
||||
}
|
||||
auto saved = base::take(_menu);
|
||||
for (const auto peer : base::take(_menuCheckRowsAfterHidden)) {
|
||||
if (const auto row = findRow(peer->asUser())) {
|
||||
if (row->speaking()) {
|
||||
checkSpeakingRowPosition(row);
|
||||
}
|
||||
delegate()->peerListShowRowMenu(row, [=](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->asUser())) {
|
||||
if (row->speaking()) {
|
||||
checkSpeakingRowPosition(row);
|
||||
}
|
||||
}
|
||||
_menu = std::move(saved);
|
||||
});
|
||||
raw->popup(QCursor::pos());
|
||||
}
|
||||
}
|
||||
_menu = std::move(saved);
|
||||
});
|
||||
}
|
||||
|
||||
void MembersController::rowActionClicked(
|
||||
@@ -1035,6 +1040,23 @@ void MembersController::rowActionClicked(
|
||||
base::unique_qptr<Ui::PopupMenu> MembersController::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> MembersController::createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
Expects(row->peer()->isUser());
|
||||
|
||||
if (row->peer()->isSelf()) {
|
||||
@@ -1125,7 +1147,9 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
|
||||
_kickMemberRequests.fire_copy(user);
|
||||
});
|
||||
|
||||
if (_peer->canManageGroupCall() && (!admin || mute)) {
|
||||
if ((muteState != Row::State::Invited)
|
||||
&& _peer->canManageGroupCall()
|
||||
&& (!admin || mute)) {
|
||||
result->addAction(
|
||||
(mute
|
||||
? tr::lng_group_call_context_mute(tr::now)
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#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"
|
||||
@@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/timer.h"
|
||||
#include "app.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h" // style::GroupCallUserpics
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Calls {
|
||||
@@ -165,7 +167,7 @@ void DebugInfoBox::updateText() {
|
||||
} // namespace
|
||||
|
||||
struct TopBar::User {
|
||||
Ui::GroupCallBarContent::User data;
|
||||
Ui::GroupCallUser data;
|
||||
};
|
||||
|
||||
class Mute final : public Ui::IconButton {
|
||||
@@ -238,6 +240,12 @@ TopBar::TopBar(
|
||||
: RpWidget(parent)
|
||||
, _call(call)
|
||||
, _groupCall(groupCall)
|
||||
, _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))
|
||||
@@ -551,35 +559,31 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
|
||||
) | rpl::map([=](not_null<Data::GroupCall*> real) {
|
||||
return HistoryView::GroupCallTracker::ContentByCall(
|
||||
real,
|
||||
HistoryView::UserpicsInRowStyle{
|
||||
.size = st::groupCallTopBarUserpicSize,
|
||||
.shift = st::groupCallTopBarUserpicShift,
|
||||
.stroke = st::groupCallTopBarUserpicStroke,
|
||||
});
|
||||
st::groupCallTopBarUserpics.size);
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=](const Ui::GroupCallBarContent &content) {
|
||||
if (_users.size() != content.users.size()) {
|
||||
return true;
|
||||
}
|
||||
for (auto i = 0, count = int(_users.size()); i != count; ++i) {
|
||||
if (_users[i].data.userpicKey != content.users[i].userpicKey
|
||||
|| _users[i].data.id != content.users[i].id) {
|
||||
if (_users[i].userpicKey != content.users[i].userpicKey
|
||||
|| _users[i].id != content.users[i].id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) | rpl::start_with_next([=](const Ui::GroupCallBarContent &content) {
|
||||
const auto sizeChanged = (_users.size() != content.users.size());
|
||||
_users = ranges::view::all(
|
||||
content.users
|
||||
) | ranges::view::transform([](const auto &user) {
|
||||
return User{ user };
|
||||
}) | ranges::to_vector;
|
||||
generateUserpicsInRow();
|
||||
if (sizeChanged) {
|
||||
updateControlsGeometry();
|
||||
_users = content.users;
|
||||
for (auto &user : _users) {
|
||||
user.speaking = false;
|
||||
}
|
||||
update();
|
||||
_userpics->update(_users, !isHidden());
|
||||
}, lifetime());
|
||||
|
||||
_userpics->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
_userpicsWidth = width;
|
||||
updateControlsGeometry();
|
||||
}, lifetime());
|
||||
|
||||
call->peer()->session().changes().peerUpdates(
|
||||
@@ -591,41 +595,10 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
|
||||
}) | rpl::start_with_next([=] {
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
|
||||
}
|
||||
|
||||
void TopBar::generateUserpicsInRow() {
|
||||
const auto count = int(_users.size());
|
||||
if (!count) {
|
||||
_userpics = QImage();
|
||||
return;
|
||||
}
|
||||
const auto limit = std::min(count, kMaxUsersInBar);
|
||||
const auto single = st::groupCallTopBarUserpicSize;
|
||||
const auto shift = st::groupCallTopBarUserpicShift;
|
||||
const auto width = single + (limit - 1) * (single - shift);
|
||||
if (_userpics.width() != width * cIntRetinaFactor()) {
|
||||
_userpics = QImage(
|
||||
QSize(width, single) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
_userpics.fill(Qt::transparent);
|
||||
_userpics.setDevicePixelRatio(cRetinaFactor());
|
||||
|
||||
auto q = Painter(&_userpics);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::groupCallTopBarUserpicStroke);
|
||||
auto x = (count - 1) * (single - shift);
|
||||
for (auto i = count; i != 0;) {
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
q.drawImage(x, 0, _users[--i].data.userpic);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x, 0, single, single);
|
||||
x -= single - shift;
|
||||
}
|
||||
void TopBar::updateUserpics() {
|
||||
update(_mute->width(), 0, _userpics->maxWidth(), height());
|
||||
}
|
||||
|
||||
void TopBar::updateInfoLabels() {
|
||||
@@ -688,8 +661,13 @@ void TopBar::updateControlsGeometry() {
|
||||
_durationLabel->moveToLeft(left, st::callBarLabelTop);
|
||||
left += _durationLabel->width() + st::callBarSkip;
|
||||
}
|
||||
if (!_userpics.isNull()) {
|
||||
left += _userpics.width() / _userpics.devicePixelRatio();
|
||||
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);
|
||||
@@ -741,11 +719,10 @@ void TopBar::paintEvent(QPaintEvent *e) {
|
||||
: (_muted ? st::callBarBgMuted : st::callBarBg);
|
||||
p.fillRect(e->rect(), std::move(brush));
|
||||
|
||||
if (!_userpics.isNull()) {
|
||||
const auto imageSize = _userpics.size()
|
||||
/ _userpics.devicePixelRatio();
|
||||
const auto top = (height() - imageSize.height()) / 2;
|
||||
p.drawImage(_mute->width(), top, _userpics);
|
||||
if (_userpicsWidth) {
|
||||
const auto size = st::groupCallTopBarUserpics.size;
|
||||
const auto top = (height() - size) / 2;
|
||||
_userpics->paint(p, _mute->width(), top, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ class IconButton;
|
||||
class AbstractButton;
|
||||
class LabelSimple;
|
||||
class FlatLabel;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
@@ -66,14 +68,15 @@ private:
|
||||
void setMuted(bool mute);
|
||||
|
||||
void subscribeToMembersChanges(not_null<GroupCall*> call);
|
||||
void generateUserpicsInRow();
|
||||
void updateUserpics();
|
||||
|
||||
const base::weak_ptr<Call> _call;
|
||||
const base::weak_ptr<GroupCall> _groupCall;
|
||||
|
||||
bool _muted = false;
|
||||
std::vector<User> _users;
|
||||
QImage _userpics;
|
||||
std::vector<Ui::GroupCallUser> _users;
|
||||
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
|
||||
int _userpicsWidth = 0;
|
||||
object_ptr<Ui::LabelSimple> _durationLabel;
|
||||
object_ptr<SignalBars> _signalBars;
|
||||
object_ptr<Ui::FlatLabel> _fullInfoLabel;
|
||||
|
||||
@@ -75,6 +75,14 @@ std::map<int, const char*> BetaLogs() {
|
||||
|
||||
"- Fix freeze on secondary screen disconnect.\n"
|
||||
},
|
||||
{
|
||||
2005002,
|
||||
"- Fix possible crash in video calls.\n"
|
||||
|
||||
"- Fix possible crash in connecting to voice chats.\n"
|
||||
|
||||
"- Use different audio module code on Windows in calls.\n"
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 2005000;
|
||||
constexpr auto AppVersionStr = "2.5";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppVersion = 2005002;
|
||||
constexpr auto AppVersionStr = "2.5.2";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/painter.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
@@ -24,7 +25,7 @@ namespace HistoryView {
|
||||
void GenerateUserpicsInRow(
|
||||
QImage &result,
|
||||
const std::vector<UserpicInRow> &list,
|
||||
const UserpicsInRowStyle &st,
|
||||
const style::GroupCallUserpics &st,
|
||||
int maxElements) {
|
||||
const auto count = int(list.size());
|
||||
if (!count) {
|
||||
@@ -67,7 +68,7 @@ GroupCallTracker::GroupCallTracker(not_null<PeerData*> peer)
|
||||
|
||||
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
struct State {
|
||||
std::vector<UserpicInRow> userpics;
|
||||
Ui::GroupCallBarContent current;
|
||||
@@ -130,7 +131,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
static const auto RegenerateUserpics = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st,
|
||||
int userpicSize,
|
||||
bool force = false) {
|
||||
const auto result = FillMissingUserpics(state, call) || force;
|
||||
if (!result) {
|
||||
@@ -141,7 +142,9 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
state->someUserpicsNotLoaded = false;
|
||||
for (auto &userpic : state->userpics) {
|
||||
userpic.peer->loadUserpic();
|
||||
const auto pic = userpic.peer->genUserpic(userpic.view, st.size);
|
||||
const auto pic = userpic.peer->genUserpic(
|
||||
userpic.view,
|
||||
userpicSize);
|
||||
userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);
|
||||
state->current.users.push_back({
|
||||
.userpic = pic.toImage(),
|
||||
@@ -161,7 +164,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
const auto i = ranges::find(
|
||||
state->userpics,
|
||||
user,
|
||||
@@ -170,7 +173,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
return false;
|
||||
}
|
||||
state->userpics.erase(i);
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -178,7 +181,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
Expects(state->userpics.size() <= kLimit);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
@@ -237,7 +240,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
}
|
||||
Assert(state->userpics.size() <= kLimit);
|
||||
}
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -262,12 +265,12 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
) | rpl::start_with_next([=](const ParticipantUpdate &update) {
|
||||
const auto user = update.now ? update.now->user : update.was->user;
|
||||
if (!update.now) {
|
||||
if (RemoveUserpic(state, call, user, st)) {
|
||||
if (RemoveUserpic(state, call, user, userpicSize)) {
|
||||
pushNext();
|
||||
}
|
||||
} else if (update.now->speaking
|
||||
&& (!update.was || !update.was->speaking)) {
|
||||
if (CheckPushToFront(state, call, user, st)) {
|
||||
if (CheckPushToFront(state, call, user, userpicSize)) {
|
||||
pushNext();
|
||||
}
|
||||
} else {
|
||||
@@ -287,7 +290,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
updateSpeakingState = false;
|
||||
}
|
||||
}
|
||||
if (RegenerateUserpics(state, call, st)
|
||||
if (RegenerateUserpics(state, call, userpicSize)
|
||||
|| updateSpeakingState) {
|
||||
pushNext();
|
||||
}
|
||||
@@ -296,7 +299,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
|
||||
call->participantsSliceAdded(
|
||||
) | rpl::filter([=] {
|
||||
return RegenerateUserpics(state, call, st);
|
||||
return RegenerateUserpics(state, call, userpicSize);
|
||||
}) | rpl::start_with_next(pushNext, lifetime);
|
||||
|
||||
call->peer()->session().downloaderTaskFinished(
|
||||
@@ -306,14 +309,14 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
for (const auto &userpic : state->userpics) {
|
||||
if (userpic.peer->userpicUniqueKey(userpic.view)
|
||||
!= userpic.uniqueKey) {
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
pushNext();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
RegenerateUserpics(state, call, st);
|
||||
RegenerateUserpics(state, call, userpicSize);
|
||||
|
||||
call->fullCountValue(
|
||||
) | rpl::start_with_next([=](int count) {
|
||||
@@ -345,12 +348,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
|
||||
} else if (!call->fullCount() && !call->participantsLoaded()) {
|
||||
call->reload();
|
||||
}
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = st::historyGroupCallUserpicSize,
|
||||
.shift = st::historyGroupCallUserpicShift,
|
||||
.stroke = st::historyGroupCallUserpicStroke,
|
||||
};
|
||||
return ContentByCall(call, st);
|
||||
return ContentByCall(call, st::historyGroupCallUserpics.size);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ class GroupCall;
|
||||
class CloudImageView;
|
||||
} // namespace Data
|
||||
|
||||
namespace style {
|
||||
struct GroupCallUserpics;
|
||||
} // namespace style
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
struct UserpicInRow {
|
||||
@@ -27,16 +31,10 @@ struct UserpicInRow {
|
||||
mutable InMemoryKey uniqueKey;
|
||||
};
|
||||
|
||||
struct UserpicsInRowStyle {
|
||||
int size = 0;
|
||||
int shift = 0;
|
||||
int stroke = 0;
|
||||
};
|
||||
|
||||
void GenerateUserpicsInRow(
|
||||
QImage &result,
|
||||
const std::vector<UserpicInRow> &list,
|
||||
const UserpicsInRowStyle &st,
|
||||
const style::GroupCallUserpics &st,
|
||||
int maxElements = 0);
|
||||
|
||||
class GroupCallTracker final {
|
||||
@@ -48,7 +46,7 @@ public:
|
||||
|
||||
[[nodiscard]] static rpl::producer<Ui::GroupCallBarContent> ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st);
|
||||
int userpicSize);
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
@@ -795,8 +795,8 @@ void Message::paintCommentsButton(
|
||||
auto &list = _comments->userpics;
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto count = std::min(int(views->recentRepliers.size()), limit);
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto single = st::historyCommentsUserpics.size;
|
||||
const auto shift = st::historyCommentsUserpics.shift;
|
||||
const auto regenerate = [&] {
|
||||
if (list.size() != count) {
|
||||
return true;
|
||||
@@ -828,12 +828,11 @@ void Message::paintCommentsButton(
|
||||
while (list.size() > count) {
|
||||
list.pop_back();
|
||||
}
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = single,
|
||||
.shift = shift,
|
||||
.stroke = st::historyCommentsUserpicStroke,
|
||||
};
|
||||
GenerateUserpicsInRow(_comments->cachedUserpics, list, st, limit);
|
||||
GenerateUserpicsInRow(
|
||||
_comments->cachedUserpics,
|
||||
list,
|
||||
st::historyCommentsUserpics,
|
||||
limit);
|
||||
}
|
||||
p.drawImage(
|
||||
left,
|
||||
@@ -2135,8 +2134,8 @@ int Message::minWidthForMedia() const {
|
||||
const auto views = data()->Get<HistoryMessageViews>();
|
||||
if (data()->repliesAreComments() && !views->replies.text.isEmpty()) {
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto single = st::historyCommentsUserpics.size;
|
||||
const auto shift = st::historyCommentsUserpics.shift;
|
||||
const auto added = single
|
||||
+ (limit - 1) * (single - shift)
|
||||
+ st::historyCommentsSkipLeft
|
||||
|
||||
@@ -209,6 +209,10 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
48,
|
||||
};
|
||||
|
||||
static const auto dprSize = [](const QImage &image) {
|
||||
return image.size() / image.devicePixelRatio();
|
||||
};
|
||||
|
||||
for (const auto iconSize : iconSizes) {
|
||||
auto ¤tImageBack = TrayIconImageBack[iconSize];
|
||||
const auto desiredSize = QSize(iconSize, iconSize);
|
||||
@@ -221,11 +225,17 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
systemIcon = QIcon::fromTheme(iconName);
|
||||
}
|
||||
|
||||
if (systemIcon.actualSize(desiredSize) == desiredSize) {
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(desiredSize)
|
||||
.toImage();
|
||||
} else {
|
||||
// We can't use QIcon::actualSize here
|
||||
// since it works incorrectly with svg icon themes
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(desiredSize)
|
||||
.toImage();
|
||||
|
||||
const auto firstAttemptSize = dprSize(currentImageBack);
|
||||
|
||||
// if current icon theme is not a svg one, Qt can return
|
||||
// a pixmap that less in size even if there are a bigger one
|
||||
if (firstAttemptSize.width() < desiredSize.width()) {
|
||||
const auto availableSizes = systemIcon.availableSizes();
|
||||
|
||||
const auto biggestSize = ranges::max_element(
|
||||
@@ -233,18 +243,17 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
std::less<>(),
|
||||
&QSize::width);
|
||||
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(*biggestSize)
|
||||
.toImage();
|
||||
if ((*biggestSize).width() > firstAttemptSize.width()) {
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(*biggestSize)
|
||||
.toImage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentImageBack = Core::App().logo();
|
||||
}
|
||||
|
||||
const auto currentImageBackSize = currentImageBack.size()
|
||||
/ currentImageBack.devicePixelRatio();
|
||||
|
||||
if (currentImageBackSize != desiredSize) {
|
||||
if (dprSize(currentImageBack) != desiredSize) {
|
||||
currentImageBack = currentImageBack.scaled(
|
||||
desiredSize * currentImageBack.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
@@ -331,17 +340,33 @@ std::unique_ptr<QTemporaryFile> TrayIconFile(
|
||||
static const auto templateName = AppRuntimeDirectory()
|
||||
+ kTrayIconFilename.utf16();
|
||||
|
||||
static const auto dprSize = [](const QPixmap &pixmap) {
|
||||
return pixmap.size() / pixmap.devicePixelRatio();
|
||||
};
|
||||
|
||||
static const auto desiredSize = QSize(22, 22);
|
||||
|
||||
static const auto scalePixmap = [=](const QPixmap &pixmap) {
|
||||
if (dprSize(pixmap) != desiredSize) {
|
||||
return pixmap.scaled(
|
||||
desiredSize * pixmap.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
} else {
|
||||
return pixmap;
|
||||
}
|
||||
};
|
||||
|
||||
auto ret = std::make_unique<QTemporaryFile>(
|
||||
templateName,
|
||||
parent);
|
||||
|
||||
ret->open();
|
||||
|
||||
if (icon.actualSize(desiredSize) == desiredSize) {
|
||||
icon.pixmap(desiredSize).save(ret.get());
|
||||
} else {
|
||||
const auto firstAttempt = icon.pixmap(desiredSize);
|
||||
const auto firstAttemptSize = dprSize(firstAttempt);
|
||||
|
||||
if (firstAttemptSize.width() < desiredSize.width()) {
|
||||
const auto availableSizes = icon.availableSizes();
|
||||
|
||||
const auto biggestSize = ranges::max_element(
|
||||
@@ -349,14 +374,13 @@ std::unique_ptr<QTemporaryFile> TrayIconFile(
|
||||
std::less<>(),
|
||||
&QSize::width);
|
||||
|
||||
const auto iconPixmap = icon.pixmap(*biggestSize);
|
||||
|
||||
iconPixmap
|
||||
.scaled(
|
||||
desiredSize * iconPixmap.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation)
|
||||
.save(ret.get());
|
||||
if ((*biggestSize).width() > firstAttemptSize.width()) {
|
||||
scalePixmap(icon.pixmap(*biggestSize)).save(ret.get());
|
||||
} else {
|
||||
scalePixmap(firstAttempt).save(ret.get());
|
||||
}
|
||||
} else {
|
||||
scalePixmap(firstAttempt).save(ret.get());
|
||||
}
|
||||
|
||||
ret->close();
|
||||
|
||||
@@ -42,7 +42,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/screensaver.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ bool HasExtensionFrom(const QString &file, const QStringList &extensions) {
|
||||
bool ValidPhotoForAlbum(
|
||||
const PreparedFileInformation::Image &image,
|
||||
const QString &mime) {
|
||||
if (image.animated || Core::IsMimeSticker(mime)) {
|
||||
if (image.animated
|
||||
|| Core::IsMimeSticker(mime)
|
||||
|| (mime == u"application/pdf"_q)) {
|
||||
return false;
|
||||
}
|
||||
const auto width = image.data.width();
|
||||
|
||||
@@ -18,6 +18,13 @@ MessageBar {
|
||||
duration: int;
|
||||
}
|
||||
|
||||
GroupCallUserpics {
|
||||
size: pixels;
|
||||
shift: pixels;
|
||||
stroke: pixels;
|
||||
align: align;
|
||||
}
|
||||
|
||||
defaultMessageBar: MessageBar {
|
||||
title: semiboldTextStyle;
|
||||
titleFg: windowActiveTextFg;
|
||||
@@ -739,10 +746,13 @@ historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSel
|
||||
historyCommentsButtonHeight: 40px;
|
||||
historyCommentsSkipLeft: 9px;
|
||||
historyCommentsSkipText: 10px;
|
||||
historyCommentsUserpicSize: 25px;
|
||||
historyCommentsUserpicStroke: 2px;
|
||||
historyCommentsUserpicOverlap: 6px;
|
||||
historyCommentsSkipRight: 8px;
|
||||
historyCommentsUserpics: GroupCallUserpics {
|
||||
size: 25px;
|
||||
shift: 6px;
|
||||
stroke: 2px;
|
||||
align: align(left);
|
||||
}
|
||||
|
||||
boxAttachEmoji: IconButton(historyAttachEmoji) {
|
||||
width: 30px;
|
||||
@@ -798,9 +808,12 @@ historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLin
|
||||
|
||||
historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);
|
||||
|
||||
historyGroupCallUserpicSize: 32px;
|
||||
historyGroupCallUserpicShift: 12px;
|
||||
historyGroupCallUserpicStroke: 4px;
|
||||
historyGroupCallUserpics: GroupCallUserpics {
|
||||
size: 32px;
|
||||
shift: 12px;
|
||||
stroke: 4px;
|
||||
align: align(top);
|
||||
}
|
||||
historyGroupCallBlobMinRadius: 23px;
|
||||
historyGroupCallBlobMaxRadius: 25px;
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
|
||||
#include "ui/chat/message_bar.h"
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
|
||||
@@ -21,71 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDuration = 160;
|
||||
constexpr auto kMaxUserpics = 4;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
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 kSendRandomLevelInterval = crl::time(100);
|
||||
|
||||
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::historyGroupCallBlobMinRadius,
|
||||
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GroupCallBar::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
}
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
};
|
||||
|
||||
struct GroupCallBar::Userpic {
|
||||
User data;
|
||||
std::pair<uint64, uint64> cacheKey;
|
||||
crl::time speakingStarted = 0;
|
||||
QImage cache;
|
||||
Animations::Simple leftAnimation;
|
||||
Animations::Simple shownAnimation;
|
||||
std::unique_ptr<BlobsAnimation> blobsAnimation;
|
||||
int left = 0;
|
||||
bool positionInited = false;
|
||||
bool topMost = false;
|
||||
bool hiding = false;
|
||||
bool cacheMasked = false;
|
||||
};
|
||||
|
||||
GroupCallBar::GroupCallBar(
|
||||
not_null<QWidget*> parent,
|
||||
@@ -98,16 +31,13 @@ GroupCallBar::GroupCallBar(
|
||||
tr::lng_group_call_join(),
|
||||
st::groupCallTopBarJoin))
|
||||
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
|
||||
, _randomSpeakingTimer([=] { sendRandomLevels(); }) {
|
||||
, _userpics(std::make_unique<GroupCallUserpics>(
|
||||
st::historyGroupCallUserpics,
|
||||
std::move(hideBlobs),
|
||||
[=] { updateUserpics(); })) {
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
|
||||
const auto limit = kMaxUserpics;
|
||||
const auto single = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
// + 1 * single for the blobs.
|
||||
_maxUserpicsWidth = 2 * single + (limit - 1) * (single - shift);
|
||||
|
||||
_wrap.entity()->paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
|
||||
@@ -122,7 +52,7 @@ GroupCallBar::GroupCallBar(
|
||||
copy
|
||||
) | rpl::start_with_next([=](GroupCallBarContent &&content) {
|
||||
_content = content;
|
||||
updateUserpicsFromContent();
|
||||
_userpics->update(_content.users, !_wrap.isHidden());
|
||||
_inner->update();
|
||||
}, lifetime());
|
||||
|
||||
@@ -140,53 +70,10 @@ GroupCallBar::GroupCallBar(
|
||||
_wrap.toggle(false, anim::type::normal);
|
||||
}, lifetime());
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.cache = QImage();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsEnterDuration)) {
|
||||
_speakingAnimation.stop();
|
||||
}
|
||||
for (auto &userpic : _userpics) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.updateLevel(now - blobs->lastTime);
|
||||
blobs->lastTime = now;
|
||||
}
|
||||
}
|
||||
updateUserpics();
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
||||
std::move(hideBlobs)
|
||||
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
|
||||
const auto hide = animDisabled || deactivated;
|
||||
|
||||
if (!(hide && _speakingAnimationHideLastTime)) {
|
||||
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
for (auto &userpic : _userpics) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.setLevel(0.);
|
||||
}
|
||||
}
|
||||
if (!hide && !_speakingAnimation.animating()) {
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
}, lifetime());
|
||||
|
||||
setupInner();
|
||||
}
|
||||
|
||||
GroupCallBar::~GroupCallBar() {
|
||||
}
|
||||
GroupCallBar::~GroupCallBar() = default;
|
||||
|
||||
void GroupCallBar::setupInner() {
|
||||
_inner->resize(0, st::historyReplyHeight);
|
||||
@@ -252,142 +139,11 @@ void GroupCallBar::paint(Painter &p) {
|
||||
? tr::lng_group_call_members(tr::now, lt_count, _content.count)
|
||||
: tr::lng_group_call_no_members(tr::now)));
|
||||
|
||||
const auto size = st::historyGroupCallUserpics.size;
|
||||
// Skip shadow of the bar above.
|
||||
paintUserpics(p);
|
||||
}
|
||||
|
||||
void GroupCallBar::paintUserpics(Painter &p) {
|
||||
const auto top = (st::historyReplyHeight
|
||||
- st::lineWidth
|
||||
- st::historyGroupCallUserpicSize) / 2 + st::lineWidth;
|
||||
const auto middle = _inner->width() / 2;
|
||||
const auto size = st::historyGroupCallUserpicSize;
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
for (auto &userpic : ranges::view::reverse(_userpics)) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown == 0.) {
|
||||
continue;
|
||||
}
|
||||
validateUserpicCache(userpic);
|
||||
p.setOpacity(shown);
|
||||
const auto left = middle + userpic.leftAnimation.value(userpic.left);
|
||||
const auto blobs = userpic.blobsAnimation.get();
|
||||
const auto shownScale = 0.5 + shown / 2.;
|
||||
const auto scale = shownScale * (!blobs
|
||||
? 1.
|
||||
: (minScale
|
||||
+ (1. - minScale) * (_speakingAnimationHideLastTime
|
||||
? (1. - blobs->blobs.currentLevel())
|
||||
: blobs->blobs.currentLevel())));
|
||||
if (blobs) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto shift = QPointF(left + size / 2., top + size / 2.);
|
||||
p.translate(shift);
|
||||
blobs->blobs.paint(p, st::windowActiveTextFg);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (std::abs(scale - 1.) < 0.001) {
|
||||
const auto skip = ((kWideScale - 1) / 2) * size * factor;
|
||||
p.drawImage(
|
||||
QRect(left, top, size, size),
|
||||
userpic.cache,
|
||||
QRect(skip, skip, size * factor, size * factor));
|
||||
} else {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto target = QRect(
|
||||
left + (1 - kWideScale) / 2 * size,
|
||||
top + (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), userpic.cache);
|
||||
}
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
|
||||
const auto hidden = [](const Userpic &userpic) {
|
||||
return userpic.hiding && !userpic.shownAnimation.animating();
|
||||
};
|
||||
_userpics.erase(ranges::remove_if(_userpics, hidden), end(_userpics));
|
||||
}
|
||||
|
||||
bool GroupCallBar::needUserpicCacheRefresh(Userpic &userpic) {
|
||||
if (userpic.cache.isNull()) {
|
||||
return true;
|
||||
} else if (userpic.hiding) {
|
||||
return false;
|
||||
} else if (userpic.cacheKey != userpic.data.userpicKey) {
|
||||
return true;
|
||||
}
|
||||
const auto shouldBeMasked = !userpic.topMost;
|
||||
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
|
||||
return true;
|
||||
}
|
||||
return !userpic.leftAnimation.animating();
|
||||
}
|
||||
|
||||
void GroupCallBar::ensureBlobsAnimation(Userpic &userpic) {
|
||||
if (userpic.blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
Blobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
userpic.blobsAnimation->lastTime = crl::now();
|
||||
}
|
||||
|
||||
void GroupCallBar::sendRandomLevels() {
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
for (auto &userpic : _userpics) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
const auto value = 30 + (openssl::RandomValue<uint32>() % 70);
|
||||
userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::validateUserpicCache(Userpic &userpic) {
|
||||
if (!needUserpicCacheRefresh(userpic)) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
const auto full = QSize(size, size) * kWideScale * factor;
|
||||
if (userpic.cache.isNull()) {
|
||||
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.cache.setDevicePixelRatio(factor);
|
||||
}
|
||||
userpic.cacheKey = userpic.data.userpicKey;
|
||||
userpic.cacheMasked = !userpic.topMost;
|
||||
userpic.cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&userpic.cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
p.drawImage(skip, skip, userpic.data.userpic);
|
||||
|
||||
if (userpic.cacheMasked) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::historyGroupCallUserpicStroke);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(skip - size + shift, skip, size, size);
|
||||
}
|
||||
}
|
||||
const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2
|
||||
+ st::lineWidth;
|
||||
_userpics->paint(p, _inner->width() / 2, top, size);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
|
||||
@@ -413,127 +169,14 @@ void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
|
||||
: regular);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpicsFromContent() {
|
||||
const auto idFromUserpic = [](const Userpic &userpic) {
|
||||
return userpic.data.id;
|
||||
};
|
||||
|
||||
// Use "topMost" as "willBeHidden" flag.
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.topMost = true;
|
||||
}
|
||||
for (const auto &user : _content.users) {
|
||||
const auto i = ranges::find(_userpics, user.id, idFromUserpic);
|
||||
if (i == end(_userpics)) {
|
||||
_userpics.push_back(Userpic{ user });
|
||||
toggleUserpic(_userpics.back(), true);
|
||||
continue;
|
||||
}
|
||||
i->topMost = false;
|
||||
|
||||
if (i->hiding) {
|
||||
toggleUserpic(*i, true);
|
||||
}
|
||||
i->data = user;
|
||||
|
||||
// Put this one after the last we are not hiding.
|
||||
for (auto j = end(_userpics) - 1; j != i; --j) {
|
||||
if (!j->topMost) {
|
||||
ranges::rotate(i, i + 1, j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
|
||||
// Set correct real values of "topMost" flag.
|
||||
const auto userpicsBegin = begin(_userpics);
|
||||
const auto userpicsEnd = end(_userpics);
|
||||
auto markedTopMost = userpicsEnd;
|
||||
auto hasBlobs = false;
|
||||
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
|
||||
auto &userpic = *i;
|
||||
if (userpic.data.speaking) {
|
||||
ensureBlobsAnimation(userpic);
|
||||
hasBlobs = true;
|
||||
} else {
|
||||
userpic.blobsAnimation = nullptr;
|
||||
}
|
||||
if (userpic.topMost) {
|
||||
toggleUserpic(userpic, false);
|
||||
userpic.topMost = false;
|
||||
} else if (markedTopMost == userpicsEnd) {
|
||||
userpic.topMost = true;
|
||||
markedTopMost = i;
|
||||
}
|
||||
}
|
||||
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
|
||||
// Bring the topMost userpic to the very beginning, above all hiding.
|
||||
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
|
||||
}
|
||||
updateUserpicsPositions();
|
||||
|
||||
if (!hasBlobs) {
|
||||
_randomSpeakingTimer.cancel();
|
||||
_speakingAnimation.stop();
|
||||
} else if (!_randomSpeakingTimer.isActive()) {
|
||||
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
|
||||
if (_wrap.isHidden()) {
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.shownAnimation.stop();
|
||||
userpic.leftAnimation.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::toggleUserpic(Userpic &userpic, bool shown) {
|
||||
userpic.hiding = !shown;
|
||||
userpic.shownAnimation.start(
|
||||
[=] { updateUserpics(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kDuration);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpicsPositions() {
|
||||
const auto shownCount = ranges::count(_userpics, false, &Userpic::hiding);
|
||||
if (!shownCount) {
|
||||
return;
|
||||
}
|
||||
const auto single = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
// + 1 * single for the blobs.
|
||||
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
||||
auto left = (-fullWidth / 2);
|
||||
for (auto &userpic : _userpics) {
|
||||
if (userpic.hiding) {
|
||||
continue;
|
||||
}
|
||||
if (!userpic.positionInited) {
|
||||
userpic.positionInited = true;
|
||||
userpic.left = left;
|
||||
} else if (userpic.left != left) {
|
||||
userpic.leftAnimation.start(
|
||||
[=] { updateUserpics(); },
|
||||
userpic.left,
|
||||
left,
|
||||
kDuration);
|
||||
userpic.left = left;
|
||||
}
|
||||
left += (single - shift);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpics() {
|
||||
const auto widget = _wrap.entity();
|
||||
const auto middle = widget->width() / 2;
|
||||
_wrap.entity()->update(
|
||||
(middle - _maxUserpicsWidth / 2),
|
||||
const auto width = _userpics->maxWidth();
|
||||
widget->update(
|
||||
(middle - width / 2),
|
||||
0,
|
||||
_maxUserpicsWidth,
|
||||
width,
|
||||
widget->height());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
@@ -18,17 +17,13 @@ namespace Ui {
|
||||
|
||||
class PlainShadow;
|
||||
class RoundButton;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
|
||||
struct GroupCallBarContent {
|
||||
struct User {
|
||||
QImage userpic;
|
||||
std::pair<uint64, uint64> userpicKey = {};
|
||||
int32 id = 0;
|
||||
bool speaking = false;
|
||||
};
|
||||
int count = 0;
|
||||
bool shown = false;
|
||||
std::vector<User> users;
|
||||
std::vector<GroupCallUser> users;
|
||||
};
|
||||
|
||||
class GroupCallBar final {
|
||||
@@ -58,24 +53,13 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallBarContent::User;
|
||||
struct BlobsAnimation;
|
||||
struct Userpic;
|
||||
using User = GroupCallUser;
|
||||
|
||||
void updateShadowGeometry(QRect wrapGeometry);
|
||||
void updateControlsGeometry(QRect wrapGeometry);
|
||||
void updateUserpicsFromContent();
|
||||
void updateUserpics();
|
||||
void setupInner();
|
||||
void paint(Painter &p);
|
||||
void paintUserpics(Painter &p);
|
||||
|
||||
void toggleUserpic(Userpic &userpic, bool shown);
|
||||
void updateUserpics();
|
||||
void updateUserpicsPositions();
|
||||
void validateUserpicCache(Userpic &userpic);
|
||||
[[nodiscard]] bool needUserpicCacheRefresh(Userpic &userpic);
|
||||
void ensureBlobsAnimation(Userpic &userpic);
|
||||
void sendRandomLevels();
|
||||
|
||||
SlideWrap<> _wrap;
|
||||
not_null<RpWidget*> _inner;
|
||||
@@ -83,17 +67,11 @@ private:
|
||||
std::unique_ptr<PlainShadow> _shadow;
|
||||
rpl::event_stream<> _barClicks;
|
||||
Fn<QRect(QRect)> _shadowGeometryPostprocess;
|
||||
std::vector<Userpic> _userpics;
|
||||
base::Timer _randomSpeakingTimer;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
int _maxUserpicsWidth = 0;
|
||||
bool _shouldBeShown = false;
|
||||
bool _forceHidden = false;
|
||||
|
||||
bool _skipLevelUpdate = false;
|
||||
crl::time _speakingAnimationHideLastTime = 0;
|
||||
|
||||
GroupCallBarContent _content;
|
||||
std::unique_ptr<GroupCallUserpics> _userpics;
|
||||
|
||||
};
|
||||
|
||||
|
||||
416
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
416
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
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 "ui/chat/group_call_userpics.h"
|
||||
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDuration = 160;
|
||||
constexpr auto kMaxUserpics = 4;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
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 kSendRandomLevelInterval = crl::time(100);
|
||||
|
||||
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::historyGroupCallBlobMinRadius,
|
||||
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GroupCallUserpics::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
}
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
};
|
||||
|
||||
struct GroupCallUserpics::Userpic {
|
||||
User data;
|
||||
std::pair<uint64, uint64> cacheKey;
|
||||
crl::time speakingStarted = 0;
|
||||
QImage cache;
|
||||
Animations::Simple leftAnimation;
|
||||
Animations::Simple shownAnimation;
|
||||
std::unique_ptr<BlobsAnimation> blobsAnimation;
|
||||
int left = 0;
|
||||
bool positionInited = false;
|
||||
bool topMost = false;
|
||||
bool hiding = false;
|
||||
bool cacheMasked = false;
|
||||
};
|
||||
|
||||
GroupCallUserpics::GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint)
|
||||
: _st(st)
|
||||
, _randomSpeakingTimer([=] { sendRandomLevels(); })
|
||||
, _repaint(std::move(repaint)) {
|
||||
const auto limit = kMaxUserpics;
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
_maxWidth = 2 * single + (limit - 1) * (single - shift);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.cache = QImage();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsEnterDuration)) {
|
||||
_speakingAnimation.stop();
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.updateLevel(now - blobs->lastTime);
|
||||
blobs->lastTime = now;
|
||||
}
|
||||
}
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
||||
std::move(hideBlobs)
|
||||
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
|
||||
const auto hide = animDisabled || deactivated;
|
||||
|
||||
if (!(hide && _speakingAnimationHideLastTime)) {
|
||||
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.setLevel(0.);
|
||||
}
|
||||
}
|
||||
if (!hide && !_speakingAnimation.animating()) {
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
GroupCallUserpics::~GroupCallUserpics() = default;
|
||||
|
||||
void GroupCallUserpics::paint(Painter &p, int x, int y, int size) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
for (auto &userpic : ranges::view::reverse(_list)) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown == 0.) {
|
||||
continue;
|
||||
}
|
||||
validateCache(userpic);
|
||||
p.setOpacity(shown);
|
||||
const auto left = x + userpic.leftAnimation.value(userpic.left);
|
||||
const auto blobs = userpic.blobsAnimation.get();
|
||||
const auto shownScale = 0.5 + shown / 2.;
|
||||
const auto scale = shownScale * (!blobs
|
||||
? 1.
|
||||
: (minScale
|
||||
+ (1. - minScale) * (_speakingAnimationHideLastTime
|
||||
? (1. - blobs->blobs.currentLevel())
|
||||
: blobs->blobs.currentLevel())));
|
||||
if (blobs) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto shift = QPointF(left + size / 2., y + size / 2.);
|
||||
p.translate(shift);
|
||||
blobs->blobs.paint(p, st::windowActiveTextFg);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (std::abs(scale - 1.) < 0.001) {
|
||||
const auto skip = ((kWideScale - 1) / 2) * size * factor;
|
||||
p.drawImage(
|
||||
QRect(left, y, size, size),
|
||||
userpic.cache,
|
||||
QRect(skip, skip, size * factor, size * factor));
|
||||
} else {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto target = QRect(
|
||||
left + (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), userpic.cache);
|
||||
}
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
|
||||
const auto hidden = [](const Userpic &userpic) {
|
||||
return userpic.hiding && !userpic.shownAnimation.animating();
|
||||
};
|
||||
_list.erase(ranges::remove_if(_list, hidden), end(_list));
|
||||
}
|
||||
|
||||
int GroupCallUserpics::maxWidth() const {
|
||||
return _maxWidth;
|
||||
}
|
||||
|
||||
rpl::producer<int> GroupCallUserpics::widthValue() const {
|
||||
return _width.value();
|
||||
}
|
||||
|
||||
bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
|
||||
if (userpic.cache.isNull()) {
|
||||
return true;
|
||||
} else if (userpic.hiding) {
|
||||
return false;
|
||||
} else if (userpic.cacheKey != userpic.data.userpicKey) {
|
||||
return true;
|
||||
}
|
||||
const auto shouldBeMasked = !userpic.topMost;
|
||||
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
|
||||
return true;
|
||||
}
|
||||
return !userpic.leftAnimation.animating();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
|
||||
if (userpic.blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
Blobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
userpic.blobsAnimation->lastTime = crl::now();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::sendRandomLevels() {
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
const auto value = 30 + (openssl::RandomValue<uint32>() % 70);
|
||||
userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::validateCache(Userpic &userpic) {
|
||||
if (!needCacheRefresh(userpic)) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
const auto full = QSize(size, size) * kWideScale * factor;
|
||||
if (userpic.cache.isNull()) {
|
||||
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.cache.setDevicePixelRatio(factor);
|
||||
}
|
||||
userpic.cacheKey = userpic.data.userpicKey;
|
||||
userpic.cacheMasked = !userpic.topMost;
|
||||
userpic.cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&userpic.cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
p.drawImage(skip, skip, userpic.data.userpic);
|
||||
|
||||
if (userpic.cacheMasked) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(_st.stroke);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(skip - size + shift, skip, size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible) {
|
||||
const auto idFromUserpic = [](const Userpic &userpic) {
|
||||
return userpic.data.id;
|
||||
};
|
||||
|
||||
// Use "topMost" as "willBeHidden" flag.
|
||||
for (auto &userpic : _list) {
|
||||
userpic.topMost = true;
|
||||
}
|
||||
for (const auto &user : users) {
|
||||
const auto i = ranges::find(_list, user.id, idFromUserpic);
|
||||
if (i == end(_list)) {
|
||||
_list.push_back(Userpic{ user });
|
||||
toggle(_list.back(), true);
|
||||
continue;
|
||||
}
|
||||
i->topMost = false;
|
||||
|
||||
if (i->hiding) {
|
||||
toggle(*i, true);
|
||||
}
|
||||
i->data = user;
|
||||
|
||||
// Put this one after the last we are not hiding.
|
||||
for (auto j = end(_list) - 1; j != i; --j) {
|
||||
if (!j->topMost) {
|
||||
ranges::rotate(i, i + 1, j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
|
||||
// Set correct real values of "topMost" flag.
|
||||
const auto userpicsBegin = begin(_list);
|
||||
const auto userpicsEnd = end(_list);
|
||||
auto markedTopMost = userpicsEnd;
|
||||
auto hasBlobs = false;
|
||||
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
|
||||
auto &userpic = *i;
|
||||
if (userpic.data.speaking) {
|
||||
ensureBlobsAnimation(userpic);
|
||||
hasBlobs = true;
|
||||
} else {
|
||||
userpic.blobsAnimation = nullptr;
|
||||
}
|
||||
if (userpic.topMost) {
|
||||
toggle(userpic, false);
|
||||
userpic.topMost = false;
|
||||
} else if (markedTopMost == userpicsEnd) {
|
||||
userpic.topMost = true;
|
||||
markedTopMost = i;
|
||||
}
|
||||
}
|
||||
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
|
||||
// Bring the topMost userpic to the very beginning, above all hiding.
|
||||
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
|
||||
}
|
||||
updatePositions();
|
||||
|
||||
if (!hasBlobs) {
|
||||
_randomSpeakingTimer.cancel();
|
||||
_speakingAnimation.stop();
|
||||
} else if (!_randomSpeakingTimer.isActive()) {
|
||||
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.shownAnimation.stop();
|
||||
userpic.leftAnimation.stop();
|
||||
}
|
||||
}
|
||||
recountAndRepaint();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
|
||||
userpic.hiding = !shown;
|
||||
userpic.shownAnimation.start(
|
||||
[=] { recountAndRepaint(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kDuration);
|
||||
}
|
||||
|
||||
void GroupCallUserpics::updatePositions() {
|
||||
const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
|
||||
if (!shownCount) {
|
||||
return;
|
||||
}
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
||||
auto left = (_st.align & Qt::AlignLeft)
|
||||
? 0
|
||||
: (_st.align & Qt::AlignHCenter)
|
||||
? (-fullWidth / 2)
|
||||
: -fullWidth;
|
||||
for (auto &userpic : _list) {
|
||||
if (userpic.hiding) {
|
||||
continue;
|
||||
}
|
||||
if (!userpic.positionInited) {
|
||||
userpic.positionInited = true;
|
||||
userpic.left = left;
|
||||
} else if (userpic.left != left) {
|
||||
userpic.leftAnimation.start(
|
||||
_repaint,
|
||||
userpic.left,
|
||||
left,
|
||||
kDuration);
|
||||
userpic.left = left;
|
||||
}
|
||||
left += (single - shift);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::recountAndRepaint() {
|
||||
auto width = 0;
|
||||
auto maxShown = 0.;
|
||||
for (const auto &userpic : _list) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown > maxShown) {
|
||||
maxShown = shown;
|
||||
}
|
||||
width += anim::interpolate(0, _st.size - _st.shift, shown);
|
||||
}
|
||||
_width = width + anim::interpolate(0, _st.shift, maxShown);
|
||||
if (_repaint) {
|
||||
_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
73
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
73
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace style {
|
||||
struct GroupCallUserpics;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct GroupCallUser {
|
||||
QImage userpic;
|
||||
std::pair<uint64, uint64> userpicKey = {};
|
||||
int32 id = 0;
|
||||
bool speaking = false;
|
||||
};
|
||||
|
||||
class GroupCallUserpics final {
|
||||
public:
|
||||
GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint);
|
||||
~GroupCallUserpics();
|
||||
|
||||
void update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible);
|
||||
void paint(Painter &p, int x, int y, int size);
|
||||
|
||||
[[nodiscard]] int maxWidth() const;
|
||||
[[nodiscard]] rpl::producer<int> widthValue() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallUser;
|
||||
struct BlobsAnimation;
|
||||
struct Userpic;
|
||||
|
||||
void toggle(Userpic &userpic, bool shown);
|
||||
void updatePositions();
|
||||
void validateCache(Userpic &userpic);
|
||||
[[nodiscard]] bool needCacheRefresh(Userpic &userpic);
|
||||
void ensureBlobsAnimation(Userpic &userpic);
|
||||
void sendRandomLevels();
|
||||
void recountAndRepaint();
|
||||
|
||||
const style::GroupCallUserpics &_st;
|
||||
std::vector<Userpic> _list;
|
||||
base::Timer _randomSpeakingTimer;
|
||||
Fn<void()> _repaint;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
int _maxWidth = 0;
|
||||
bool _skipLevelUpdate = false;
|
||||
crl::time _speakingAnimationHideLastTime = 0;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -45,6 +45,11 @@ MessageBar::MessageBar(not_null<QWidget*> parent, const style::MessageBar &st)
|
||||
: _st(st)
|
||||
, _widget(parent) {
|
||||
setup();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_topBarGradient = _bottomBarGradient = QPixmap();
|
||||
}, _widget.lifetime());
|
||||
}
|
||||
|
||||
void MessageBar::setup() {
|
||||
|
||||
2
Telegram/ThirdParty/tgcalls
vendored
2
Telegram/ThirdParty/tgcalls
vendored
Submodule Telegram/ThirdParty/tgcalls updated: 178983f723...cba2d0daa7
@@ -1,7 +1,7 @@
|
||||
AppVersion 2005000
|
||||
AppVersion 2005002
|
||||
AppVersionStrMajor 2.5
|
||||
AppVersionStrSmall 2.5
|
||||
AppVersionStr 2.5.0
|
||||
BetaChannel 0
|
||||
AppVersionStrSmall 2.5.2
|
||||
AppVersionStr 2.5.2
|
||||
BetaChannel 1
|
||||
AlphaVersion 0
|
||||
AppVersionOriginal 2.5
|
||||
AppVersionOriginal 2.5.2.beta
|
||||
|
||||
@@ -80,6 +80,8 @@ PRIVATE
|
||||
ui/chat/attach/attach_single_media_preview.h
|
||||
ui/chat/group_call_bar.cpp
|
||||
ui/chat/group_call_bar.h
|
||||
ui/chat/group_call_userpics.cpp
|
||||
ui/chat/group_call_userpics.h
|
||||
ui/chat/message_bar.cpp
|
||||
ui/chat/message_bar.h
|
||||
ui/chat/pinned_bar.cpp
|
||||
|
||||
Submodule Telegram/lib_ui updated: 1e2799245c...17eb0f22b4
@@ -1,3 +1,13 @@
|
||||
2.5.2 beta (25.12.20)
|
||||
|
||||
- Fix possible crash in video calls.
|
||||
- Fix possible crash in connecting to voice chats.
|
||||
- Use different audio module code on Windows in calls.
|
||||
|
||||
2.5.1 (23.12.20)
|
||||
|
||||
- Fix crash in voice calls.
|
||||
|
||||
2.5 (23.12.20)
|
||||
|
||||
- Turn any of your group chats into a hop-on, hop-off conference call.
|
||||
|
||||
2
cmake
2
cmake
Submodule cmake updated: a81345a28d...2208358765
@@ -20,6 +20,8 @@ apps:
|
||||
environment:
|
||||
# Tell glib to use portals on file associations handling.
|
||||
GTK_USE_PORTAL: 1
|
||||
# Use sandboxed ibus api
|
||||
IBUS_USE_PORTAL: 1
|
||||
plugs:
|
||||
- alsa
|
||||
- audio-playback
|
||||
@@ -75,7 +77,6 @@ parts:
|
||||
- liblzma-dev
|
||||
- libopus-dev
|
||||
- libpulse-dev
|
||||
- libqt5svg5-dev
|
||||
- libqt5waylandclient5-dev
|
||||
- libssl-dev
|
||||
- libxcb1-dev
|
||||
@@ -87,11 +88,11 @@ parts:
|
||||
- qt5-image-formats-plugins
|
||||
- qtwayland5
|
||||
- libasound2
|
||||
- libglib2.0-0
|
||||
- libgtk-3-0
|
||||
- liblzma5
|
||||
- libopus0
|
||||
- libpulse0
|
||||
- libqt5svg5
|
||||
- libqt5waylandclient5
|
||||
- libssl1.1
|
||||
- libxcb1
|
||||
@@ -174,16 +175,6 @@ parts:
|
||||
after:
|
||||
- mozjpeg
|
||||
|
||||
# Qt checks that ibus-daemon binary is present, otherwise doesn't work
|
||||
ibus:
|
||||
plugin: nil
|
||||
stage-packages:
|
||||
- ibus
|
||||
stage:
|
||||
- -./usr/lib/$SNAPCRAFT_ARCH_TRIPLET/libjpeg.so.8.2.2
|
||||
after:
|
||||
- mozjpeg
|
||||
|
||||
ffmpeg:
|
||||
plugin: nil
|
||||
build-packages:
|
||||
@@ -223,18 +214,14 @@ parts:
|
||||
openal:
|
||||
source: https://github.com/kcat/openal-soft.git
|
||||
source-depth: 1
|
||||
source-tag: openal-soft-1.20.1
|
||||
source-tag: openal-soft-1.21.0
|
||||
plugin: cmake
|
||||
build-packages:
|
||||
- libasound2-dev
|
||||
- libpulse-dev
|
||||
- libsndio-dev
|
||||
- portaudio19-dev
|
||||
stage-packages:
|
||||
- libasound2
|
||||
- libpulse0
|
||||
- libportaudio2
|
||||
- libsndio7.0
|
||||
cmake-parameters:
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
|
||||
Reference in New Issue
Block a user