diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 339f79bbb..f66ac5678 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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."; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index afb4668f8..0ad1c75ea 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -496,3 +496,7 @@ groupCallSettings: CallButton(callMicrophoneMute) { groupCallButtonSkip: 43px; groupCallButtonBottomSkip: 134px; groupCallMuteBottomSkip: 149px; + +groupCallTopBarUserpicSize: 28px; +groupCallTopBarUserpicShift: 8px; +groupCallTopBarUserpicStroke: 2px; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp index 1a20e599e..b61969c26 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp @@ -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 &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 channel) : _channel(channel) { } +rpl::producer GroupCallTracker::ContentByCall( + not_null call, + const UserpicsInRowStyle &st) { + struct State { + std::vector 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, + not_null call) { + const auto already = int(state->userpics.size()); + const auto &participants = call->participants(); + if (already >= kLimit || participants.size() <= already) { + return false; + } + std::array 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, + not_null 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, + not_null call, + not_null 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, + not_null call, + not_null 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(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(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->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 GroupCallTracker::content() const { const auto channel = _channel; return rpl::combine( @@ -29,16 +284,26 @@ rpl::producer 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 { + 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 { diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h index 1dc6c4f94..e93cbf432 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h @@ -13,8 +13,31 @@ namespace Ui { struct GroupCallBarContent; } // namespace Ui +namespace Data { +class GroupCall; +class CloudImageView; +} // namespace Data + namespace HistoryView { +struct UserpicInRow { + not_null peer; + mutable std::shared_ptr view; + mutable InMemoryKey uniqueKey; +}; + +struct UserpicsInRowStyle { + int size = 0; + int shift = 0; + int stroke = 0; +}; + +void GenerateUserpicsInRow( + QImage &result, + const std::vector &list, + const UserpicsInRowStyle &st, + int maxElements = 0); + class GroupCallTracker final { public: GroupCallTracker(not_null channel); @@ -22,6 +45,10 @@ public: [[nodiscard]] rpl::producer content() const; [[nodiscard]] rpl::producer<> joinClicks() const; + [[nodiscard]] static rpl::producer ContentByCall( + not_null call, + const UserpicsInRowStyle &st); + private: not_null _channel; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 67f888493..71ac89589 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -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 peer; - std::shared_ptr view; - InMemoryKey uniqueKey; - }; std::unique_ptr ripple; - std::vector userpics; + std::vector 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, diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index addf7088f..bf47f7455 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -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(); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 3317d8fac..d65f8d885 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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); diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index b7a73e4db..584f902e6 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -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) { diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.h b/Telegram/SourceFiles/ui/chat/group_call_bar.h index fe0ec25ae..1796330c1 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.h +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.h @@ -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 { diff --git a/Telegram/lib_base b/Telegram/lib_base index 707bdc849..3562a4368 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 707bdc84918eddfd8c08e776d328dab03dc04b25 +Subproject commit 3562a43685fdac00c277292ce2c83d92132cc319