Track speaking state in participants list.

This commit is contained in:
John Preston 2020-11-29 12:20:33 +03:00
parent 3a5b625d64
commit 058199aa0d
6 changed files with 285 additions and 148 deletions

View File

@ -760,7 +760,7 @@ public:
_content->reorderRows([&]( _content->reorderRows([&](
auto &&begin, auto &&begin,
auto &&end) { auto &&end) {
std::sort(begin, end, [&](auto &&a, auto &&b) { std::stable_sort(begin, end, [&](auto &&a, auto &&b) {
return compare(*a, *b); return compare(*a, *b);
}); });
}); });

View File

@ -34,6 +34,8 @@ namespace Calls {
namespace { namespace {
constexpr auto kMaxInvitePerSlice = 10; constexpr auto kMaxInvitePerSlice = 10;
constexpr auto kCheckLastSpokeInterval = 3 * crl::time(1000);
constexpr auto kSpeakLevelThreshold = 0.2;
} // namespace } // namespace
@ -43,7 +45,8 @@ GroupCall::GroupCall(
const MTPInputGroupCall &inputCall) const MTPInputGroupCall &inputCall)
: _delegate(delegate) : _delegate(delegate)
, _channel(channel) , _channel(channel)
, _api(&_channel->session().mtp()) { , _api(&_channel->session().mtp())
, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) {
const auto id = inputCall.c_inputGroupCall().vid().v; const auto id = inputCall.c_inputGroupCall().vid().v;
if (id) { if (id) {
if (const auto call = _channel->call(); call && call->id() == id) { if (const auto call = _channel->call(); call && call->id() == id) {
@ -119,9 +122,11 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) {
using Update = Data::GroupCall::ParticipantUpdate; using Update = Data::GroupCall::ParticipantUpdate;
_channel->call()->participantUpdated( _channel->call()->participantUpdated(
) | rpl::filter([=](const Update &update) { ) | rpl::filter([=](const Update &update) {
return (_instance != nullptr) && update.removed; return (_instance != nullptr) && !update.now;
}) | rpl::start_with_next([=](const Update &update) { }) | rpl::start_with_next([=](const Update &update) {
_instance->removeSsrcs({ update.participant.source }); Expects(update.was.has_value());
_instance->removeSsrcs({ update.was->source });
}, _lifetime); }, _lifetime);
} }
@ -404,7 +409,9 @@ void GroupCall::createAndStartController() {
crl::on_main(weak, [=] { setInstanceConnected(connected); }); crl::on_main(weak, [=] { setInstanceConnected(connected); });
}, },
.audioLevelsUpdated = [=](const AudioLevels &data) { .audioLevelsUpdated = [=](const AudioLevels &data) {
if (!data.empty()) {
crl::on_main(weak, [=] { audioLevelsUpdated(data); }); crl::on_main(weak, [=] { audioLevelsUpdated(data); });
}
}, },
.myAudioLevelUpdated = [=](float level) { .myAudioLevelUpdated = [=](float level) {
if (*myLevel != level) { // Don't send many 0 while we're muted. if (*myLevel != level) { // Don't send many 0 while we're muted.
@ -446,22 +453,77 @@ void GroupCall::createAndStartController() {
//raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
} }
void GroupCall::myLevelUpdated(float level) { void GroupCall::handleLevelsUpdated(
_levelUpdates.fire(LevelUpdate{ gsl::span<const std::pair<std::uint32_t, float>> data) {
.source = _mySsrc, Expects(!data.empty());
.value = level,
.self = true
});
}
void GroupCall::audioLevelsUpdated( auto check = false;
const std::vector<std::pair<std::uint32_t, float>> &data) { auto checkNow = false;
const auto now = crl::now();
for (const auto &[source, level] : data) { for (const auto &[source, level] : data) {
_levelUpdates.fire(LevelUpdate{ _levelUpdates.fire(LevelUpdate{
.source = source, .source = source,
.value = level, .value = level,
.self = (source == _mySsrc) .self = (source == _mySsrc)
}); });
if (level <= kSpeakLevelThreshold) {
continue;
}
check = true;
const auto i = _lastSpoke.find(source);
if (i == _lastSpoke.end()) {
_lastSpoke.emplace(source, now);
checkNow = true;
} else {
if (i->second + kCheckLastSpokeInterval / 3 <= now) {
checkNow = true;
}
i->second = now;
}
}
if (checkNow) {
checkLastSpoke();
} else if (check && !_lastSpokeCheckTimer.isActive()) {
_lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2);
}
}
void GroupCall::myLevelUpdated(float level) {
const auto pair = std::pair<std::uint32_t, float>{ _mySsrc, level };
handleLevelsUpdated({ &pair, &pair + 1 });
}
void GroupCall::audioLevelsUpdated(
const std::vector<std::pair<std::uint32_t, float>> &data) {
handleLevelsUpdated(gsl::make_span(data));
}
void GroupCall::checkLastSpoke() {
const auto real = _channel->call();
if (!real || real->id() != _id) {
return;
}
auto hasRecent = false;
const auto now = crl::now();
auto list = base::take(_lastSpoke);
for (auto i = list.begin(); i != list.end();) {
const auto [source, when] = *i;
if (when + kCheckLastSpokeInterval >= now) {
hasRecent = true;
++i;
} else {
i = list.erase(i);
}
real->applyLastSpoke(source, when, now);
}
_lastSpoke = std::move(list);
if (!hasRecent) {
_lastSpokeCheckTimer.cancel();
} else if (!_lastSpokeCheckTimer.isActive()) {
_lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3);
} }
} }

View File

@ -124,7 +124,10 @@ private:
void myLevelUpdated(float level); void myLevelUpdated(float level);
void audioLevelsUpdated( void audioLevelsUpdated(
const std::vector<std::pair<std::uint32_t, float>> &data); const std::vector<std::pair<std::uint32_t, float>> &data);
void handleLevelsUpdated(
gsl::span<const std::pair<std::uint32_t, float>> data);
void setInstanceConnected(bool connected); void setInstanceConnected(bool connected);
void checkLastSpoke();
[[nodiscard]] MTPInputGroupCall inputCall() const; [[nodiscard]] MTPInputGroupCall inputCall() const;
@ -145,6 +148,8 @@ private:
std::unique_ptr<tgcalls::GroupInstanceImpl> _instance; std::unique_ptr<tgcalls::GroupInstanceImpl> _instance;
rpl::event_stream<LevelUpdate> _levelUpdates; rpl::event_stream<LevelUpdate> _levelUpdates;
base::flat_map<uint32, crl::time> _lastSpoke;
base::Timer _lastSpokeCheckTimer;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View File

@ -30,7 +30,6 @@ namespace Calls {
namespace { namespace {
constexpr auto kLevelThreshold = 0.2; constexpr auto kLevelThreshold = 0.2;
constexpr auto kLevelActiveTimeout = crl::time(1000);
struct UpdateLevelResult { struct UpdateLevelResult {
bool levelChanged = false; bool levelChanged = false;
@ -49,10 +48,13 @@ public:
}; };
void updateState(const Data::GroupCall::Participant *participant); void updateState(const Data::GroupCall::Participant *participant);
UpdateLevelResult updateLevel(float level); //UpdateLevelResult updateLevel(float level);
[[nodiscard]] State state() const { [[nodiscard]] State state() const {
return _state; return _state;
} }
[[nodiscard]] bool speaking() const {
return _speaking;
}
void addActionRipple(QPoint point, Fn<void()> updateCallback) override; void addActionRipple(QPoint point, Fn<void()> updateCallback) override;
void stopLastActionRipple() override; void stopLastActionRipple() override;
@ -83,8 +85,7 @@ public:
private: private:
void refreshStatus() override; void refreshStatus() override;
void refreshStatus(crl::time now); void setSpeaking(bool speaking);
void resetSpeakingState();
[[nodiscard]] static State ComputeState( [[nodiscard]] static State ComputeState(
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
@ -95,8 +96,8 @@ private:
State _state = State::Inactive; State _state = State::Inactive;
not_null<ChannelData*> _channel; not_null<ChannelData*> _channel;
not_null<const style::IconButton*> _st; not_null<const style::IconButton*> _st;
float _level = 0.; bool _speaking = false;
crl::time _markInactiveAt = 0; //float _level = 0.;
std::unique_ptr<Ui::RippleAnimation> _actionRipple; std::unique_ptr<Ui::RippleAnimation> _actionRipple;
@ -130,16 +131,18 @@ private:
const Data::GroupCall::Participant &participant) const; const Data::GroupCall::Participant &participant) const;
void prepareRows(not_null<Data::GroupCall*> real); void prepareRows(not_null<Data::GroupCall*> real);
void repaintByTimer(); //void repaintByTimer();
void setupListChangeViewers(not_null<GroupCall*> call); void setupListChangeViewers(not_null<GroupCall*> call);
void subscribeToChanges(not_null<Data::GroupCall*> real); void subscribeToChanges(not_null<Data::GroupCall*> real);
void updateRow( void updateRow(
const Data::GroupCall::Participant &participant); const std::optional<Data::GroupCall::Participant> &was,
const Data::GroupCall::Participant &now);
void updateRow( void updateRow(
not_null<Row*> row, not_null<Row*> row,
const Data::GroupCall::Participant *participant) const; const Data::GroupCall::Participant *participant) const;
void updateRowLevel(not_null<UserData*> user, float level); void checkSpeakingRowPosition(not_null<Row*> row);
//void updateRowLevel(not_null<UserData*> user, float level);
Row *findRow(not_null<UserData*> user) const; Row *findRow(not_null<UserData*> user) const;
[[nodiscard]] Data::GroupCall *resolvedRealCall() const; [[nodiscard]] Data::GroupCall *resolvedRealCall() const;
@ -155,8 +158,8 @@ private:
rpl::variable<int> _fullCount = 1; rpl::variable<int> _fullCount = 1;
Ui::BoxPointer _addBox; Ui::BoxPointer _addBox;
base::flat_map<not_null<UserData*>, crl::time> _repaintByTimer; //base::flat_map<not_null<UserData*>, crl::time> _repaintByTimer;
base::Timer _repaintTimer; //base::Timer _repaintTimer;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
@ -178,54 +181,61 @@ void Row::updateState(const Data::GroupCall::Participant *participant) {
setCustomStatus(QString()); setCustomStatus(QString());
} }
_state = State::Inactive; _state = State::Inactive;
resetSpeakingState(); setSpeaking(false);
} else if (!participant->muted) { } else if (!participant->muted) {
_state = State::Active; _state = State::Active;
setSpeaking(participant->speaking);
} else if (participant->canSelfUnmute) { } else if (participant->canSelfUnmute) {
_state = State::Inactive; _state = State::Inactive;
resetSpeakingState(); setSpeaking(false);
} else { } else {
_state = State::Muted; _state = State::Muted;
resetSpeakingState(); setSpeaking(false);
} }
_st = ComputeIconStyle(_state); _st = ComputeIconStyle(_state);
} }
void Row::resetSpeakingState() { void Row::setSpeaking(bool speaking) {
_markInactiveAt = 0; if (_speaking == speaking) {
updateLevel(0.); return;
}
_speaking = speaking;
refreshStatus();
//if (!_speaking) {
// updateLevel(0.);
//}
} }
UpdateLevelResult Row::updateLevel(float level) { //UpdateLevelResult Row::updateLevel(float level) {
if (_level == level) { // if (_level == level) {
return UpdateLevelResult{ .nextUpdateTime = _markInactiveAt }; // return UpdateLevelResult{ .nextUpdateTime = _markInactiveAt };
} // }
const auto now = crl::now(); // const auto now = crl::now();
const auto stillActive = (now < _markInactiveAt); // const auto stillActive = (now < _markInactiveAt);
const auto wasActive = (_level >= kLevelThreshold) && stillActive; // const auto wasActive = (_level >= kLevelThreshold) && stillActive;
const auto nowActive = (level >= kLevelThreshold); // const auto nowActive = (level >= kLevelThreshold);
if (nowActive) { // if (nowActive) {
_markInactiveAt = now + kLevelActiveTimeout; // _markInactiveAt = now + kLevelActiveTimeout;
if (_state != State::Active) { // if (_state != State::Active) {
_state = State::Active; // _state = State::Active;
_st = ComputeIconStyle(_state); // _st = ComputeIconStyle(_state);
} // }
} // }
_level = level; // _level = level;
const auto changed = wasActive != (nowActive || stillActive); // const auto changed = wasActive != (nowActive || stillActive);
if (!changed) { // if (!changed) {
return UpdateLevelResult{ // return UpdateLevelResult{
.levelChanged = true, // .levelChanged = true,
.nextUpdateTime = _markInactiveAt, // .nextUpdateTime = _markInactiveAt,
}; // };
} // }
refreshStatus(now); // refreshStatus(now);
return UpdateLevelResult{ // return UpdateLevelResult{
.levelChanged = true, // .levelChanged = true,
.stateChanged = true, // .stateChanged = true,
.nextUpdateTime = _markInactiveAt, // .nextUpdateTime = _markInactiveAt,
}; // };
} //}
void Row::paintAction( void Row::paintAction(
Painter &p, Painter &p,
@ -251,16 +261,11 @@ void Row::paintAction(
} }
void Row::refreshStatus() { void Row::refreshStatus() {
refreshStatus(crl::now());
}
void Row::refreshStatus(crl::time now) {
const auto active = (now < _markInactiveAt);
setCustomStatus( setCustomStatus(
(active (_speaking
? tr::lng_group_call_active(tr::now) ? tr::lng_group_call_active(tr::now)
: tr::lng_group_call_inactive(tr::now)), : tr::lng_group_call_inactive(tr::now)),
active); _speaking);
} }
Row::State Row::ComputeState( Row::State Row::ComputeState(
@ -315,8 +320,8 @@ void Row::stopLastActionRipple() {
MembersController::MembersController(not_null<GroupCall*> call) MembersController::MembersController(not_null<GroupCall*> call)
: _call(call) : _call(call)
, _channel(call->channel()) , _channel(call->channel()) {
, _repaintTimer([=] { repaintByTimer(); }) { //, _repaintTimer([=] { repaintByTimer(); }) {
setupListChangeViewers(call); setupListChangeViewers(call);
} }
@ -345,21 +350,21 @@ void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
} }
}, _lifetime); }, _lifetime);
call->levelUpdates( //call->levelUpdates(
) | rpl::start_with_next([=](const LevelUpdate &update) { //) | rpl::start_with_next([=](const LevelUpdate &update) {
const auto findUserBySource = [&](uint32 source) -> UserData* { // const auto findUserBySource = [&](uint32 source) -> UserData* {
if (const auto real = resolvedRealCall()) { // if (const auto real = resolvedRealCall()) {
return real->userBySource(source); // return real->userBySource(source);
} // }
return nullptr; // return nullptr;
}; // };
const auto user = update.self // const auto user = update.self
? _channel->session().user().get() // ? _channel->session().user().get()
: findUserBySource(update.source); // : findUserBySource(update.source);
if (user) { // if (user) {
updateRowLevel(user, update.value); // updateRowLevel(user, update.value);
} // }
}, _lifetime); //}, _lifetime);
} }
void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) { void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
@ -379,8 +384,10 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
using Update = Data::GroupCall::ParticipantUpdate; using Update = Data::GroupCall::ParticipantUpdate;
real->participantUpdated( real->participantUpdated(
) | rpl::start_with_next([=](const Update &update) { ) | rpl::start_with_next([=](const Update &update) {
const auto user = update.participant.user; Expects(update.was.has_value() || update.now.has_value());
if (update.removed) {
const auto user = update.was ? update.was->user : update.now->user;
if (!update.now) {
if (const auto row = findRow(user)) { if (const auto row = findRow(user)) {
if (user->isSelf()) { if (user->isSelf()) {
row->updateState(nullptr); row->updateState(nullptr);
@ -391,21 +398,60 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
} }
} }
} else { } else {
updateRow(update.participant); updateRow(update.was, *update.now);
} }
}, _lifetime); }, _lifetime);
} }
void MembersController::updateRow( void MembersController::updateRow(
const Data::GroupCall::Participant &participant) { const std::optional<Data::GroupCall::Participant> &was,
if (const auto row = findRow(participant.user)) { const Data::GroupCall::Participant &now) {
updateRow(row, &participant); if (const auto row = findRow(now.user)) {
} else if (auto row = createRow(participant)) { if (now.speaking && (!was || !was->speaking)) {
checkSpeakingRowPosition(row);
}
updateRow(row, &now);
} else if (auto row = createRow(now)) {
if (now.speaking) {
delegate()->peerListPrependRow(std::move(row));
} else {
delegate()->peerListAppendRow(std::move(row)); delegate()->peerListAppendRow(std::move(row));
}
delegate()->peerListRefreshRows(); delegate()->peerListRefreshRows();
} }
} }
void MembersController::checkSpeakingRowPosition(not_null<Row*> row) {
// Check if there are non-speaking rows above this one.
const auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
const auto above = delegate()->peerListRowAt(i);
if (above == row) {
// All rows above are speaking.
return;
} else if (!static_cast<Row*>(above.get())->speaking()) {
break;
}
}
// Someone started speaking and has a non-speaking row above him. Sort.
const auto proj = [&](const PeerListRow &other) {
if (&other == row.get()) {
// Bring this new one to the top.
return 0;
} else if (static_cast<const Row&>(other).speaking()) {
// Bring all the speaking ones below him.
return 1;
} else {
return 2;
}
};
delegate()->peerListSortRows([&](
const PeerListRow &a,
const PeerListRow &b) {
return proj(a) < proj(b);
});
}
void MembersController::updateRow( void MembersController::updateRow(
not_null<Row*> row, not_null<Row*> row,
const Data::GroupCall::Participant *participant) const { const Data::GroupCall::Participant *participant) const {
@ -413,47 +459,47 @@ void MembersController::updateRow(
delegate()->peerListUpdateRow(row); delegate()->peerListUpdateRow(row);
} }
void MembersController::updateRowLevel( //void MembersController::updateRowLevel(
not_null<UserData*> user, // not_null<UserData*> user,
float level) { // float level) {
if (const auto row = findRow(user)) { // if (const auto row = findRow(user)) {
const auto result = row->updateLevel(level); // const auto result = row->updateLevel(level);
if (result.stateChanged) { // if (result.stateChanged) {
// #TODO calls reorder. // // #TODO calls reorder.
} // }
if (result.stateChanged) { // if (result.stateChanged) {
delegate()->peerListUpdateRow(row); // delegate()->peerListUpdateRow(row);
} // }
if (result.nextUpdateTime) { // if (result.nextUpdateTime) {
_repaintByTimer[user] = result.nextUpdateTime; // _repaintByTimer[user] = result.nextUpdateTime;
if (!_repaintTimer.isActive()) { // if (!_repaintTimer.isActive()) {
_repaintTimer.callOnce(kLevelActiveTimeout); // _repaintTimer.callOnce(kLevelActiveTimeout);
} // }
} else if (_repaintByTimer.remove(user) && _repaintByTimer.empty()) { // } else if (_repaintByTimer.remove(user) && _repaintByTimer.empty()) {
_repaintTimer.cancel(); // _repaintTimer.cancel();
} // }
} // }
} //}
void MembersController::repaintByTimer() { //void MembersController::repaintByTimer() {
const auto now = crl::now(); // const auto now = crl::now();
auto next = crl::time(0); // auto next = crl::time(0);
for (auto i = begin(_repaintByTimer); i != end(_repaintByTimer);) { // for (auto i = begin(_repaintByTimer); i != end(_repaintByTimer);) {
if (i->second > now) { // if (i->second > now) {
if (!next || next > i->second) { // if (!next || next > i->second) {
next = i->second; // next = i->second;
} // }
} else if (const auto row = findRow(i->first)) { // } else if (const auto row = findRow(i->first)) {
delegate()->peerListUpdateRow(row); // delegate()->peerListUpdateRow(row);
i = _repaintByTimer.erase(i); // i = _repaintByTimer.erase(i);
continue; // continue;
} // }
++i; // ++i;
} // }
if (next) { // if (next) {
_repaintTimer.callOnce(next - now); // _repaintTimer.callOnce(next - now);
} // }
} //}
Row *MembersController::findRow(not_null<UserData*> user) const { Row *MembersController::findRow(not_null<UserData*> user) const {
return static_cast<Row*>(delegate()->peerListFindRow(user->id)); return static_cast<Row*>(delegate()->peerListFindRow(user->id));
@ -681,7 +727,7 @@ void GroupMembers::setupButtons(not_null<GroupCall*> call) {
using namespace rpl::mappers; using namespace rpl::mappers;
_addMember->showOn(Data::CanWriteValue( _addMember->showOn(Data::CanWriteValue(
call->channel() call->channel().get()
)); ));
_addMember->addClickHandler([=] { // TODO throttle(ripple duration) _addMember->addClickHandler([=] { // TODO throttle(ripple duration)
_addMemberRequests.fire({}); _addMemberRequests.fire({});

View File

@ -17,6 +17,7 @@ namespace Data {
namespace { namespace {
constexpr auto kRequestPerPage = 30; constexpr auto kRequestPerPage = 30;
constexpr auto kSpeakStatusKeptFor = crl::time(1000);
} // namespace } // namespace
@ -186,8 +187,7 @@ void GroupCall::applyParticipantsSlice(
if (data.is_left()) { if (data.is_left()) {
if (i != end(_participants)) { if (i != end(_participants)) {
auto update = ParticipantUpdate{ auto update = ParticipantUpdate{
.participant = *i, .was = *i,
.removed = true,
}; };
_userBySource.erase(i->source); _userBySource.erase(i->source);
_participants.erase(i); _participants.erase(i);
@ -200,10 +200,15 @@ void GroupCall::applyParticipantsSlice(
} }
return; return;
} }
const auto was = (i != end(_participants))
? std::make_optional(*i)
: std::nullopt;
const auto value = Participant{ const auto value = Participant{
.user = user, .user = user,
.date = data.vdate().v, .date = data.vdate().v,
.lastActive = was ? was->lastActive : 0,
.source = uint32(data.vsource().v), .source = uint32(data.vsource().v),
.speaking = !data.is_muted() && (was ? was->speaking : false),
.muted = data.is_muted(), .muted = data.is_muted(),
.canSelfUnmute = !data.is_muted() || data.is_can_self_unmute(), .canSelfUnmute = !data.is_muted() || data.is_can_self_unmute(),
}; };
@ -220,17 +225,11 @@ void GroupCall::applyParticipantsSlice(
*i = value; *i = value;
} }
_participantUpdates.fire({ _participantUpdates.fire({
.participant = value, .was = was,
.now = value,
}); });
}); });
} }
ranges::sort(_participants, std::greater<>(), [](const Participant &p) {
return p.lastSpoke
? p.lastSpoke
: p.lastActive
? p.lastActive
: p.date;
});
_fullCount = fullCount; _fullCount = fullCount;
} }
@ -248,15 +247,39 @@ void GroupCall::applyParticipantsMutes(
user, user,
&Participant::user); &Participant::user);
if (i != end(_participants)) { if (i != end(_participants)) {
const auto was = *i;
i->muted = data.is_muted(); i->muted = data.is_muted();
i->canSelfUnmute = !i->muted || data.is_can_self_unmute(); i->canSelfUnmute = !i->muted || data.is_can_self_unmute();
if (i->muted) {
i->speaking = false;
}
_participantUpdates.fire({ _participantUpdates.fire({
.participant = *i, .was = was,
.now = *i,
}); });
} }
}); });
} }
}
void GroupCall::applyLastSpoke(uint32 source, crl::time when, crl::time now) {
const auto i = _userBySource.find(source);
if (i == end(_userBySource)) {
// #TODO calls load participant by source from server.
return;
}
const auto j = ranges::find(_participants, i->second, &Participant::user);
Assert(j != end(_participants));
const auto speaking = (when + kSpeakStatusKeptFor >= now) && !j->muted;
if (j->speaking != speaking) {
const auto was = *j;
j->speaking = speaking;
_participantUpdates.fire({
.was = was,
.now = *j,
});
}
} }
void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) { void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) {

View File

@ -25,14 +25,14 @@ public:
not_null<UserData*> user; not_null<UserData*> user;
TimeId date = 0; TimeId date = 0;
TimeId lastActive = 0; TimeId lastActive = 0;
TimeId lastSpoke = 0;
uint32 source = 0; uint32 source = 0;
bool speaking = false;
bool muted = false; bool muted = false;
bool canSelfUnmute = false; bool canSelfUnmute = false;
}; };
struct ParticipantUpdate { struct ParticipantUpdate {
Participant participant; std::optional<Participant> was;
bool removed = false; std::optional<Participant> now;
}; };
[[nodiscard]] auto participants() const [[nodiscard]] auto participants() const
@ -48,6 +48,7 @@ public:
void applyUpdate(const MTPDupdateGroupCallParticipants &update); void applyUpdate(const MTPDupdateGroupCallParticipants &update);
void applyUpdateChecked( void applyUpdateChecked(
const MTPDupdateGroupCallParticipants &update); const MTPDupdateGroupCallParticipants &update);
void applyLastSpoke(uint32 source, crl::time when, crl::time now);
[[nodiscard]] int fullCount() const; [[nodiscard]] int fullCount() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const; [[nodiscard]] rpl::producer<int> fullCountValue() const;