Add recent members userpics to group call bar.

This commit is contained in:
John Preston 2020-11-29 15:29:25 +03:00
parent 058199aa0d
commit e3a73378e7
10 changed files with 348 additions and 43 deletions

View file

@ -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.";

View file

@ -496,3 +496,7 @@ groupCallSettings: CallButton(callMicrophoneMute) {
groupCallButtonSkip: 43px;
groupCallButtonBottomSkip: 134px;
groupCallMuteBottomSkip: 149px;
groupCallTopBarUserpicSize: 28px;
groupCallTopBarUserpicShift: 8px;
groupCallTopBarUserpicStroke: 2px;

View file

@ -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 {

View file

@ -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;

View file

@ -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,

View file

@ -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();

View file

@ -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);

View file

@ -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) {

View file

@ -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