mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 22:27:20 +02:00
Add recent members userpics to group call bar.
This commit is contained in:
parent
058199aa0d
commit
e3a73378e7
10 changed files with 348 additions and 43 deletions
|
@ -1840,6 +1840,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_group_call_invite_done_user" = "You invited {user} to the voice chat.";
|
||||
"lng_group_call_invite_done_many#one" = "You invited **{count} member** to the voice chat.";
|
||||
"lng_group_call_invite_done_many#other" = "You invited **{count} members** to the voice chat.";
|
||||
"lng_group_call_no_members" = "No members";
|
||||
"lng_group_call_members#one" = "{count} member";
|
||||
"lng_group_call_members#other" = "{count} members";
|
||||
|
||||
"lng_no_mic_permission" = "Telegram needs access to your microphone so that you can make calls and record voice messages.";
|
||||
|
||||
|
|
|
@ -496,3 +496,7 @@ groupCallSettings: CallButton(callMicrophoneMute) {
|
|||
groupCallButtonSkip: 43px;
|
||||
groupCallButtonBottomSkip: 134px;
|
||||
groupCallMuteBottomSkip: 149px;
|
||||
|
||||
groupCallTopBarUserpicSize: 28px;
|
||||
groupCallTopBarUserpicShift: 8px;
|
||||
groupCallTopBarUserpicStroke: 2px;
|
||||
|
|
|
@ -8,20 +8,275 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/view/history_view_group_call_tracker.h"
|
||||
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
#include "ui/painter.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
void GenerateUserpicsInRow(
|
||||
QImage &result,
|
||||
const std::vector<UserpicInRow> &list,
|
||||
const UserpicsInRowStyle &st,
|
||||
int maxElements) {
|
||||
const auto count = int(list.size());
|
||||
if (!count) {
|
||||
result = QImage();
|
||||
return;
|
||||
}
|
||||
const auto limit = std::max(count, maxElements);
|
||||
const auto single = st.size;
|
||||
const auto shift = st.shift;
|
||||
const auto width = single + (limit - 1) * (single - shift);
|
||||
if (result.width() != width * cIntRetinaFactor()) {
|
||||
result = QImage(
|
||||
QSize(width, single) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
result.fill(Qt::transparent);
|
||||
result.setDevicePixelRatio(cRetinaFactor());
|
||||
|
||||
auto q = Painter(&result);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st.stroke);
|
||||
auto x = (count - 1) * (single - shift);
|
||||
for (auto i = count; i != 0;) {
|
||||
auto &entry = list[--i];
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
entry.peer->paintUserpic(q, entry.view, x, 0, single);
|
||||
entry.uniqueKey = entry.peer->userpicUniqueKey(entry.view);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x, 0, single, single);
|
||||
x -= single - shift;
|
||||
}
|
||||
}
|
||||
|
||||
GroupCallTracker::GroupCallTracker(not_null<ChannelData*> channel)
|
||||
: _channel(channel) {
|
||||
}
|
||||
|
||||
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st) {
|
||||
struct State {
|
||||
std::vector<UserpicInRow> userpics;
|
||||
Ui::GroupCallBarContent current;
|
||||
base::has_weak_ptr guard;
|
||||
bool someUserpicsNotLoaded = false;
|
||||
bool scheduled = false;
|
||||
};
|
||||
|
||||
// speaking DESC, std::max(date, lastActive) DESC
|
||||
static const auto SortKey = [](const Data::GroupCall::Participant &p) {
|
||||
const auto result = (p.speaking ? uint64(0x100000000ULL) : uint64(0))
|
||||
| uint64(std::max(p.lastActive, p.date));
|
||||
return (~uint64(0)) - result; // sorting with less(), so invert.
|
||||
};
|
||||
|
||||
constexpr auto kLimit = 3;
|
||||
static const auto FillMissingUserpics = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call) {
|
||||
const auto already = int(state->userpics.size());
|
||||
const auto &participants = call->participants();
|
||||
if (already >= kLimit || participants.size() <= already) {
|
||||
return false;
|
||||
}
|
||||
std::array<const Data::GroupCall::Participant*, kLimit> adding{
|
||||
{ nullptr }
|
||||
};
|
||||
for (const auto &participant : call->participants()) {
|
||||
const auto alreadyInList = ranges::contains(
|
||||
state->userpics,
|
||||
participant.user,
|
||||
&UserpicInRow::peer);
|
||||
if (alreadyInList) {
|
||||
continue;
|
||||
}
|
||||
for (auto i = 0; i != kLimit; ++i) {
|
||||
if (!adding[i]) {
|
||||
adding[i] = &participant;
|
||||
break;
|
||||
} else if (SortKey(participant) < SortKey(*adding[i])) {
|
||||
for (auto j = kLimit - 1; j != i; --j) {
|
||||
adding[j] = adding[j - 1];
|
||||
}
|
||||
adding[i] = &participant;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto i = 0; i != kLimit - already; ++i) {
|
||||
if (adding[i]) {
|
||||
state->userpics.push_back(UserpicInRow{ adding[i]->user });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
static const auto RegenerateUserpics = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st,
|
||||
bool force = false) {
|
||||
const auto result = FillMissingUserpics(state, call) || force;
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
GenerateUserpicsInRow(
|
||||
state->current.userpics,
|
||||
state->userpics,
|
||||
st);
|
||||
state->someUserpicsNotLoaded = false;
|
||||
for (const auto &userpic : state->userpics) {
|
||||
if (userpic.peer->hasUserpic()
|
||||
&& userpic.peer->useEmptyUserpic(userpic.view)) {
|
||||
state->someUserpicsNotLoaded = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
static const auto RemoveUserpic = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
const auto i = ranges::find(
|
||||
state->userpics,
|
||||
user,
|
||||
&UserpicInRow::peer);
|
||||
if (i == state->userpics.end()) {
|
||||
return false;
|
||||
}
|
||||
state->userpics.erase(i);
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
static const auto CheckPushToFront = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
Expects(state->userpics.size() <= kLimit);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
auto i = state->userpics.begin();
|
||||
|
||||
// Find where to put a new speaking userpic.
|
||||
for (; i != state->userpics.end(); ++i) {
|
||||
if (i->peer == user) {
|
||||
return false;
|
||||
}
|
||||
const auto j = ranges::find(
|
||||
participants,
|
||||
not_null{ static_cast<UserData*>(i->peer.get()) },
|
||||
&Data::GroupCall::Participant::user);
|
||||
if (j == end(participants) || !j->speaking) {
|
||||
// Found a non-speaking one, put the new speaking one here.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i - state->userpics.begin() >= kLimit) {
|
||||
// Full kLimit of speaking userpics already.
|
||||
return false;
|
||||
}
|
||||
const auto added = state->userpics.insert(i, UserpicInRow{ user });
|
||||
if (state->userpics.size() > kLimit) {
|
||||
// Find last non-speaking userpic to remove. It must be there.
|
||||
for (auto i = state->userpics.end() - 1; i != added; --i) {
|
||||
const auto j = ranges::find(
|
||||
participants,
|
||||
not_null{ static_cast<UserData*>(i->peer.get()) },
|
||||
&Data::GroupCall::Participant::user);
|
||||
if (j == end(participants) || !j->speaking) {
|
||||
// Found a non-speaking one, remove.
|
||||
state->userpics.erase(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert(state->userpics.size() <= kLimit);
|
||||
}
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
auto state = lifetime.make_state<State>();
|
||||
state->current.shown = true;
|
||||
|
||||
const auto pushNext = [=] {
|
||||
if (state->scheduled) {
|
||||
return;
|
||||
}
|
||||
state->scheduled = true;
|
||||
crl::on_main(&state->guard, [=] {
|
||||
state->scheduled = false;
|
||||
consumer.put_next_copy(state->current);
|
||||
});
|
||||
};
|
||||
|
||||
using ParticipantUpdate = Data::GroupCall::ParticipantUpdate;
|
||||
call->participantUpdated(
|
||||
) | 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)) {
|
||||
pushNext();
|
||||
}
|
||||
} else if (update.now->speaking
|
||||
&& (!update.was || !update.was->speaking)) {
|
||||
if (CheckPushToFront(state, call, user, st)) {
|
||||
pushNext();
|
||||
}
|
||||
} else if (RegenerateUserpics(state, call, st)) {
|
||||
pushNext();
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
call->participantsSliceAdded(
|
||||
) | rpl::filter([=] {
|
||||
return RegenerateUserpics(state, call, st);
|
||||
}) | rpl::start_with_next(pushNext, lifetime);
|
||||
|
||||
call->channel()->session().downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
return state->someUserpicsNotLoaded;
|
||||
}) | rpl::start_with_next([=] {
|
||||
for (const auto &userpic : state->userpics) {
|
||||
if (userpic.peer->userpicUniqueKey(userpic.view)
|
||||
!= userpic.uniqueKey) {
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
pushNext();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
RegenerateUserpics(state, call, st);
|
||||
|
||||
call->fullCountValue(
|
||||
) | rpl::start_with_next([=](int count) {
|
||||
state->current.count = count;
|
||||
consumer.put_next_copy(state->current);
|
||||
}, lifetime);
|
||||
|
||||
return lifetime;
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
|
||||
const auto channel = _channel;
|
||||
return rpl::combine(
|
||||
|
@ -29,16 +284,26 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
|
|||
channel,
|
||||
Data::PeerUpdate::Flag::GroupCall),
|
||||
Core::App().calls().currentGroupCallValue()
|
||||
) | rpl::map([=](const auto&, Calls::GroupCall *current)
|
||||
-> Ui::GroupCallBarContent {
|
||||
) | rpl::map([=](const auto&, Calls::GroupCall *current) {
|
||||
const auto call = channel->call();
|
||||
if (!call || (current && current->channel() == channel)) {
|
||||
return { .shown = false };
|
||||
return (call && (!current || current->channel() != channel))
|
||||
? call
|
||||
: nullptr;
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::map([](Data::GroupCall *call)
|
||||
-> rpl::producer<Ui::GroupCallBarContent> {
|
||||
if (!call) {
|
||||
return rpl::single(Ui::GroupCallBarContent{ .shown = false });
|
||||
} else if (!call->fullCount() && !call->participantsLoaded()) {
|
||||
call->reload();
|
||||
}
|
||||
return { .count = call->fullCount(), .shown = true };
|
||||
});
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = st::historyGroupCallUserpicSize,
|
||||
.shift = st::historyGroupCallUserpicShift,
|
||||
.stroke = st::historyGroupCallUserpicStroke,
|
||||
};
|
||||
return ContentByCall(call, st);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
rpl::producer<> GroupCallTracker::joinClicks() const {
|
||||
|
|
|
@ -13,8 +13,31 @@ namespace Ui {
|
|||
struct GroupCallBarContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
class CloudImageView;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
struct UserpicInRow {
|
||||
not_null<PeerData*> peer;
|
||||
mutable std::shared_ptr<Data::CloudImageView> view;
|
||||
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,
|
||||
int maxElements = 0);
|
||||
|
||||
class GroupCallTracker final {
|
||||
public:
|
||||
GroupCallTracker(not_null<ChannelData*> channel);
|
||||
|
@ -22,6 +45,10 @@ public:
|
|||
[[nodiscard]] rpl::producer<Ui::GroupCallBarContent> content() const;
|
||||
[[nodiscard]] rpl::producer<> joinClicks() const;
|
||||
|
||||
[[nodiscard]] static rpl::producer<Ui::GroupCallBarContent> ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st);
|
||||
|
||||
private:
|
||||
not_null<ChannelData*> _channel;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/history_message.h"
|
||||
#include "history/view/media/history_view_media.h"
|
||||
#include "history/view/media/history_view_web_page.h"
|
||||
#include "history/view/history_view_group_call_tracker.h" // UserpicInRow.
|
||||
#include "history/history.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "core/application.h"
|
||||
|
@ -266,13 +267,8 @@ style::color FromNameFg(PeerId peerId, bool selected) {
|
|||
} // namespace
|
||||
|
||||
struct Message::CommentsButton {
|
||||
struct Userpic {
|
||||
not_null<PeerData*> peer;
|
||||
std::shared_ptr<Data::CloudImageView> view;
|
||||
InMemoryKey uniqueKey;
|
||||
};
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
std::vector<Userpic> userpics;
|
||||
std::vector<UserpicInRow> userpics;
|
||||
QImage cachedUserpics;
|
||||
ClickHandlerPtr link;
|
||||
QPoint lastPoint;
|
||||
|
@ -822,7 +818,7 @@ void Message::paintCommentsButton(
|
|||
for (auto i = 0; i != count; ++i) {
|
||||
const auto peerId = views->recentRepliers[i];
|
||||
if (i == list.size()) {
|
||||
list.push_back(CommentsButton::Userpic{
|
||||
list.push_back(UserpicInRow{
|
||||
history()->owner().peer(peerId)
|
||||
});
|
||||
} else if (list[i].peer->id != peerId) {
|
||||
|
@ -832,31 +828,12 @@ void Message::paintCommentsButton(
|
|||
while (list.size() > count) {
|
||||
list.pop_back();
|
||||
}
|
||||
const auto width = single + (limit - 1) * (single - shift);
|
||||
if (_comments->cachedUserpics.isNull()) {
|
||||
_comments->cachedUserpics = QImage(
|
||||
QSize(width, single) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
_comments->cachedUserpics.fill(Qt::transparent);
|
||||
_comments->cachedUserpics.setDevicePixelRatio(cRetinaFactor());
|
||||
|
||||
auto q = Painter(&_comments->cachedUserpics);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::historyCommentsUserpicStroke);
|
||||
auto x = (count - 1) * (single - shift);
|
||||
for (auto i = count; i != 0;) {
|
||||
auto &entry = list[--i];
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
entry.peer->paintUserpic(q, entry.view, x, 0, single);
|
||||
entry.uniqueKey = entry.peer->userpicUniqueKey(entry.view);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x, 0, single, single);
|
||||
x -= single - shift;
|
||||
}
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = single,
|
||||
.shift = shift,
|
||||
.stroke = st::historyCommentsUserpicStroke,
|
||||
};
|
||||
GenerateUserpicsInRow(_comments->cachedUserpics, list, st, limit);
|
||||
}
|
||||
p.drawImage(
|
||||
left,
|
||||
|
|
|
@ -978,7 +978,10 @@ void MainWidget::setCurrentGroupCall(Calls::GroupCall *call) {
|
|||
_currentGroupCall->stateValue(
|
||||
) | rpl::start_with_next([=](Calls::GroupCall::State state) {
|
||||
using State = Calls::GroupCall::State;
|
||||
if (state != State::Joined && state != State::Connecting) {
|
||||
if (state != State::Creating
|
||||
&& state != State::Joining
|
||||
&& state != State::Joined
|
||||
&& state != State::Connecting) {
|
||||
destroyCallTopBar();
|
||||
} else if (!_callTopBar) {
|
||||
createCallTopBar();
|
||||
|
|
|
@ -797,6 +797,10 @@ historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLin
|
|||
|
||||
historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);
|
||||
|
||||
historyGroupCallUserpicSize: 36px;
|
||||
historyGroupCallUserpicShift: 12px;
|
||||
historyGroupCallUserpicStroke: 2px;
|
||||
|
||||
largeEmojiSize: 36px;
|
||||
largeEmojiOutline: 1px;
|
||||
largeEmojiPadding: margins(0px, 0px, 0px, 0px);
|
||||
|
|
|
@ -101,9 +101,32 @@ void GroupCallBar::setupInner() {
|
|||
|
||||
void GroupCallBar::paint(Painter &p) {
|
||||
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
|
||||
|
||||
auto left = st::msgReplyBarSkip;
|
||||
if (!_content.userpics.isNull()) {
|
||||
const auto imageSize = _content.userpics.size()
|
||||
/ _content.userpics.devicePixelRatio();
|
||||
p.drawImage(
|
||||
left,
|
||||
(_inner->height() - imageSize.height()) / 2,
|
||||
_content.userpics);
|
||||
left += imageSize.width() + st::msgReplyBarSkip;
|
||||
}
|
||||
const auto titleTop = st::msgReplyPadding.top();
|
||||
const auto textTop = titleTop + st::msgServiceNameFont->height;
|
||||
const auto width = _inner->width();
|
||||
p.setPen(st::defaultMessageBar.textFg);
|
||||
p.setFont(st::defaultMessageBar.title.font);
|
||||
p.drawTextLeft(left, titleTop, width, tr::lng_group_call_title(tr::now));
|
||||
p.setPen(st::historyComposeAreaFgService);
|
||||
p.setFont(st::defaultMessageBar.text.font);
|
||||
p.drawText(_inner->rect(), tr::lng_group_call_title(tr::now), style::al_center);
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
textTop,
|
||||
width,
|
||||
(_content.count > 0
|
||||
? tr::lng_group_call_members(tr::now, lt_count, _content.count)
|
||||
: tr::lng_group_call_no_members(tr::now)));
|
||||
}
|
||||
|
||||
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
|
||||
|
|
|
@ -19,8 +19,7 @@ class PlainShadow;
|
|||
struct GroupCallBarContent {
|
||||
int count = 0;
|
||||
bool shown = false;
|
||||
bool joined = false;
|
||||
// #TODO calls userpics
|
||||
QImage userpics;
|
||||
};
|
||||
|
||||
class GroupCallBar final {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 707bdc84918eddfd8c08e776d328dab03dc04b25
|
||||
Subproject commit 3562a43685fdac00c277292ce2c83d92132cc319
|
Loading…
Add table
Reference in a new issue