Add voice chat indicator in the chats list.

This commit is contained in:
John Preston 2020-12-02 13:52:19 +03:00
parent 00e4ef7701
commit fdbe6bdeb2
15 changed files with 556 additions and 194 deletions

View file

@ -438,11 +438,12 @@ bool OnlineTextActive(not_null<UserData*> user, TimeId now) {
return OnlineTextActive(user->onlineTill, now); return OnlineTextActive(user->onlineTill, now);
} }
bool IsPeerAnOnlineUser(not_null<PeerData*> peer) { bool IsUserOnline(not_null<UserData*> user) {
if (const auto user = peer->asUser()) { return OnlineTextActive(user, base::unixtime::now());
return OnlineTextActive(user, base::unixtime::now()); }
}
return false; bool ChannelHasActiveCall(not_null<ChannelData*> channel) {
return (channel->flags() & MTPDchannel::Flag::f_call_active);
} }
} // namespace Data } // namespace Data

View file

@ -120,6 +120,7 @@ inline auto PeerFullFlagValue(
[[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now); [[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now);
[[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now); [[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now);
[[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now); [[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now);
[[nodiscard]] bool IsPeerAnOnlineUser(not_null<PeerData*> peer); [[nodiscard]] bool IsUserOnline(not_null<UserData*> user);
[[nodiscard]] bool ChannelHasActiveCall(not_null<ChannelData*> channel);
} // namespace Data } // namespace Data

View file

@ -2181,6 +2181,15 @@ void Session::updateSendActionAnimation(
_sendActionAnimationUpdate.fire(std::move(update)); _sendActionAnimationUpdate.fire(std::move(update));
} }
auto Session::speakingAnimationUpdated() const
-> rpl::producer<not_null<History*>> {
return _speakingAnimationUpdate.events();
}
void Session::updateSpeakingAnimation(not_null<History*> history) {
_speakingAnimationUpdate.fire_copy(history);
}
int Session::unreadBadge() const { int Session::unreadBadge() const {
return computeUnreadBadge(_chatsList.unreadState()); return computeUnreadBadge(_chatsList.unreadState());
} }

View file

@ -400,6 +400,9 @@ public:
[[nodiscard]] auto sendActionAnimationUpdated() const [[nodiscard]] auto sendActionAnimationUpdated() const
-> rpl::producer<SendActionAnimationUpdate>; -> rpl::producer<SendActionAnimationUpdate>;
void updateSendActionAnimation(SendActionAnimationUpdate &&update); void updateSendActionAnimation(SendActionAnimationUpdate &&update);
[[nodiscard]] auto speakingAnimationUpdated() const
-> rpl::producer<not_null<History*>>;
void updateSpeakingAnimation(not_null<History*> history);
using SendActionPainter = HistoryView::SendActionPainter; using SendActionPainter = HistoryView::SendActionPainter;
[[nodiscard]] std::shared_ptr<SendActionPainter> repliesSendActionPainter( [[nodiscard]] std::shared_ptr<SendActionPainter> repliesSendActionPainter(
@ -951,6 +954,7 @@ private:
std::unique_ptr<CredentialsWithGeneration> _passportCredentials; std::unique_ptr<CredentialsWithGeneration> _passportCredentials;
rpl::event_stream<SendActionAnimationUpdate> _sendActionAnimationUpdate; rpl::event_stream<SendActionAnimationUpdate> _sendActionAnimationUpdate;
rpl::event_stream<not_null<History*>> _speakingAnimationUpdate;
std::vector<WallPaper> _wallpapers; std::vector<WallPaper> _wallpapers;
int32 _wallpapersHash = 0; int32 _wallpapersHash = 0;

View file

@ -36,9 +36,15 @@ dialogsPadding: point(10px, 8px);
dialogsOnlineBadgeStroke: 2px; dialogsOnlineBadgeStroke: 2px;
dialogsOnlineBadgeSize: 10px; dialogsOnlineBadgeSize: 10px;
dialogsOnlineBadgeSkip: point(10px, 12px); dialogsOnlineBadgeSkip: point(0px, 2px);
dialogsOnlineBadgeDuration: 150; dialogsOnlineBadgeDuration: 150;
dialogsCallBadgeSize: 16px;
dialogsCallBadgeSkip: point(-1px, 1px);
dialogsSpeakingStrokeNumerator: 16px;
dialogsSpeakingDenominator: 8.;
dialogsImportantBarHeight: 37px; dialogsImportantBarHeight: 37px;
dialogsSkip: 8px; dialogsSkip: 8px;

View file

@ -189,6 +189,11 @@ InnerWidget::InnerWidget(
UpdateRowSection::Default | UpdateRowSection::Filtered); UpdateRowSection::Default | UpdateRowSection::Filtered);
}, lifetime()); }, lifetime());
session().data().speakingAnimationUpdated(
) | rpl::start_with_next([=](not_null<History*> history) {
updateDialogRowCornerStatus(history);
}, lifetime());
setupOnlineStatusCheck(); setupOnlineStatusCheck();
rpl::merge( rpl::merge(
@ -2959,11 +2964,43 @@ MsgId InnerWidget::lastSearchMigratedId() const {
void InnerWidget::setupOnlineStatusCheck() { void InnerWidget::setupOnlineStatusCheck() {
session().changes().peerUpdates( session().changes().peerUpdates(
Data::PeerUpdate::Flag::OnlineStatus Data::PeerUpdate::Flag::OnlineStatus
| Data::PeerUpdate::Flag::GroupCall
) | rpl::start_with_next([=](const Data::PeerUpdate &update) { ) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
userOnlineUpdated(update.peer); if (update.peer->isUser()) {
userOnlineUpdated(update.peer);
} else {
groupHasCallUpdated(update.peer);
}
}, lifetime()); }, lifetime());
} }
void InnerWidget::updateDialogRowCornerStatus(not_null<History*> history) {
const auto user = history->peer->isUser();
const auto size = user
? st::dialogsOnlineBadgeSize
: st::dialogsCallBadgeSize;
const auto stroke = st::dialogsOnlineBadgeStroke;
const auto skip = user
? st::dialogsOnlineBadgeSkip
: st::dialogsCallBadgeSkip;
const auto updateRect = QRect(
st::dialogsPhotoSize - skip.x() - size,
st::dialogsPhotoSize - skip.y() - size,
size,
size
).marginsAdded(
{ stroke, stroke, stroke, stroke }
).translated(
st::dialogsPadding
);
updateDialogRow(
RowDescriptor(
history,
FullMsgId()),
updateRect,
UpdateRowSection::Default | UpdateRowSection::Filtered);
}
void InnerWidget::userOnlineUpdated(not_null<PeerData*> peer) { void InnerWidget::userOnlineUpdated(not_null<PeerData*> peer) {
const auto user = peer->isSelf() ? nullptr : peer->asUser(); const auto user = peer->isSelf() ? nullptr : peer->asUser();
if (!user) { if (!user) {
@ -2973,32 +3010,35 @@ void InnerWidget::userOnlineUpdated(not_null<PeerData*> peer) {
if (!history) { if (!history) {
return; return;
} }
const auto size = st::dialogsOnlineBadgeSize; updateRowCornerStatusShown(
const auto stroke = st::dialogsOnlineBadgeStroke; history,
const auto skip = st::dialogsOnlineBadgeSkip; Data::OnlineTextActive(user, base::unixtime::now()));
const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize; }
const auto updateRect = QRect(
edge - skip.x() - size, void InnerWidget::groupHasCallUpdated(not_null<PeerData*> peer) {
edge - skip.y() - size, const auto group = peer->asMegagroup();
size, if (!group) {
size return;
).marginsAdded( }
{ stroke, stroke, stroke, stroke } const auto history = session().data().historyLoaded(group);
).translated( if (!history) {
st::dialogsPadding return;
); }
updateRowCornerStatusShown(
history,
group->flags() & MTPDchannel::Flag::f_call_active);
}
void InnerWidget::updateRowCornerStatusShown(
not_null<History*> history,
bool shown) {
const auto repaint = [=] { const auto repaint = [=] {
updateDialogRow( updateDialogRowCornerStatus(history);
RowDescriptor(
history,
FullMsgId()),
updateRect,
UpdateRowSection::Default | UpdateRowSection::Filtered);
}; };
repaint(); repaint();
const auto findRow = [&](not_null<History*> history) const auto findRow = [&](not_null<History*> history)
-> std::pair<Row*, int> { -> std::pair<Row*, int> {
if (state() == WidgetState::Default) { if (state() == WidgetState::Default) {
const auto row = shownDialogs()->getRow({ history }); const auto row = shownDialogs()->getRow({ history });
return { row, row ? defaultRowTop(row) : 0 }; return { row, row ? defaultRowTop(row) : 0 };
@ -3014,8 +3054,8 @@ void InnerWidget::userOnlineUpdated(not_null<PeerData*> peer) {
if (const auto &[row, top] = findRow(history); row != nullptr) { if (const auto &[row, top] = findRow(history); row != nullptr) {
const auto visible = (top < _visibleBottom) const auto visible = (top < _visibleBottom)
&& (top + st::dialogsRowHeight > _visibleTop); && (top + st::dialogsRowHeight > _visibleTop);
row->setOnline( row->updateCornerBadgeShown(
Data::OnlineTextActive(user, base::unixtime::now()), history->peer,
visible ? Fn<void()>(crl::guard(this, repaint)) : nullptr); visible ? Fn<void()>(crl::guard(this, repaint)) : nullptr);
} }
} }

View file

@ -224,6 +224,12 @@ private:
int defaultRowTop(not_null<Row*> row) const; int defaultRowTop(not_null<Row*> row) const;
void setupOnlineStatusCheck(); void setupOnlineStatusCheck();
void userOnlineUpdated(not_null<PeerData*> peer); void userOnlineUpdated(not_null<PeerData*> peer);
void groupHasCallUpdated(not_null<PeerData*> peer);
void updateRowCornerStatusShown(
not_null<History*> history,
bool shown);
void updateDialogRowCornerStatus(not_null<History*> history);
void setupShortcuts(); void setupShortcuts();
RowDescriptor computeJump( RowDescriptor computeJump(

View file

@ -258,6 +258,8 @@ void paintRow(
p.fillRect(fullRect, bg); p.fillRect(fullRect, bg);
row->paintRipple(p, 0, 0, fullWidth, &ripple->c); row->paintRipple(p, 0, 0, fullWidth, &ripple->c);
const auto history = chat.history();
if (flags & Flag::SavedMessages) { if (flags & Flag::SavedMessages) {
Ui::EmptyUserpic::PaintSavedMessages( Ui::EmptyUserpic::PaintSavedMessages(
p, p,
@ -276,7 +278,8 @@ void paintRow(
row->paintUserpic( row->paintUserpic(
p, p,
from, from,
(flags & Flag::AllowUserOnline), (flags & Flag::AllowUserOnline) ? history : nullptr,
ms,
active, active,
fullWidth); fullWidth);
} else if (hiddenSenderInfo) { } else if (hiddenSenderInfo) {
@ -306,7 +309,6 @@ void paintRow(
return; return;
} }
const auto history = chat.history();
auto namewidth = fullWidth - nameleft - st::dialogsPadding.x(); auto namewidth = fullWidth - nameleft - st::dialogsPadding.x();
auto rectForName = QRect( auto rectForName = QRect(
nameleft, nameleft,

View file

@ -71,27 +71,29 @@ QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
BasicRow::BasicRow() = default; BasicRow::BasicRow() = default;
BasicRow::~BasicRow() = default; BasicRow::~BasicRow() = default;
void BasicRow::setOnline(bool online, Fn<void()> updateCallback) const { void BasicRow::setCornerBadgeShown(
if (_online == online) { bool shown,
Fn<void()> updateCallback) const {
if (_cornerBadgeShown == shown) {
return; return;
} }
_online = online; _cornerBadgeShown = shown;
if (_onlineUserpic && _onlineUserpic->animation.animating()) { if (_cornerBadgeUserpic && _cornerBadgeUserpic->animation.animating()) {
_onlineUserpic->animation.change( _cornerBadgeUserpic->animation.change(
_online ? 1. : 0., _cornerBadgeShown ? 1. : 0.,
st::dialogsOnlineBadgeDuration); st::dialogsOnlineBadgeDuration);
} else if (updateCallback) { } else if (updateCallback) {
ensureOnlineUserpic(); ensureCornerBadgeUserpic();
_onlineUserpic->animation.start( _cornerBadgeUserpic->animation.start(
std::move(updateCallback), std::move(updateCallback),
_online ? 0. : 1., _cornerBadgeShown ? 0. : 1.,
_online ? 1. : 0., _cornerBadgeShown ? 1. : 0.,
st::dialogsOnlineBadgeDuration); st::dialogsOnlineBadgeDuration);
} }
if (!_online if (!_cornerBadgeShown
&& _onlineUserpic && _cornerBadgeUserpic
&& !_onlineUserpic->animation.animating()) { && !_cornerBadgeUserpic->animation.animating()) {
_onlineUserpic = nullptr; _cornerBadgeUserpic = nullptr;
} }
} }
@ -129,15 +131,29 @@ void BasicRow::paintRipple(
} }
} }
void BasicRow::ensureOnlineUserpic() const { void BasicRow::updateCornerBadgeShown(
if (_onlineUserpic) { not_null<PeerData*> peer,
return; Fn<void()> updateCallback) const {
} const auto shown = [&] {
_onlineUserpic = std::make_unique<OnlineUserpic>(); if (const auto user = peer->asUser()) {
return Data::IsUserOnline(user);
} else if (const auto channel = peer->asChannel()) {
return Data::ChannelHasActiveCall(channel);
}
return false;
}();
setCornerBadgeShown(shown, std::move(updateCallback));
} }
void BasicRow::PaintOnlineFrame( void BasicRow::ensureCornerBadgeUserpic() const {
not_null<OnlineUserpic*> data, if (_cornerBadgeUserpic) {
return;
}
_cornerBadgeUserpic = std::make_unique<CornerBadgeUserpic>();
}
void BasicRow::PaintCornerBadgeFrame(
not_null<CornerBadgeUserpic*> data,
not_null<PeerData*> peer, not_null<PeerData*> peer,
std::shared_ptr<Data::CloudImageView> &view) { std::shared_ptr<Data::CloudImageView> &view) {
data->frame.fill(Qt::transparent); data->frame.fill(Qt::transparent);
@ -153,21 +169,24 @@ void BasicRow::PaintOnlineFrame(
PainterHighQualityEnabler hq(q); PainterHighQualityEnabler hq(q);
q.setCompositionMode(QPainter::CompositionMode_Source); q.setCompositionMode(QPainter::CompositionMode_Source);
const auto size = st::dialogsOnlineBadgeSize; const auto size = peer->isUser()
? st::dialogsOnlineBadgeSize
: st::dialogsCallBadgeSize;
const auto stroke = st::dialogsOnlineBadgeStroke; const auto stroke = st::dialogsOnlineBadgeStroke;
const auto skip = st::dialogsOnlineBadgeSkip; const auto skip = peer->isUser()
const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize; ? st::dialogsOnlineBadgeSkip
const auto shrink = (size / 2) * (1. - data->online); : st::dialogsCallBadgeSkip;
const auto shrink = (size / 2) * (1. - data->shown);
auto pen = QPen(Qt::transparent); auto pen = QPen(Qt::transparent);
pen.setWidthF(stroke * data->online); pen.setWidthF(stroke * data->shown);
q.setPen(pen); q.setPen(pen);
q.setBrush(data->active q.setBrush(data->active
? st::dialogsOnlineBadgeFgActive ? st::dialogsOnlineBadgeFgActive
: st::dialogsOnlineBadgeFg); : st::dialogsOnlineBadgeFg);
q.drawEllipse(QRectF( q.drawEllipse(QRectF(
edge - skip.x() - size, st::dialogsPhotoSize - skip.x() - size,
edge - skip.y() - size, st::dialogsPhotoSize - skip.y() - size,
size, size,
size size
).marginsRemoved({ shrink, shrink, shrink, shrink })); ).marginsRemoved({ shrink, shrink, shrink, shrink }));
@ -176,15 +195,16 @@ void BasicRow::PaintOnlineFrame(
void BasicRow::paintUserpic( void BasicRow::paintUserpic(
Painter &p, Painter &p,
not_null<PeerData*> peer, not_null<PeerData*> peer,
bool allowOnline, History *historyForCornerBadge,
crl::time now,
bool active, bool active,
int fullWidth) const { int fullWidth) const {
setOnline(Data::IsPeerAnOnlineUser(peer)); updateCornerBadgeShown(peer);
const auto online = _onlineUserpic const auto shown = _cornerBadgeUserpic
? _onlineUserpic->animation.value(_online ? 1. : 0.) ? _cornerBadgeUserpic->animation.value(_cornerBadgeShown ? 1. : 0.)
: (_online ? 1. : 0.); : (_cornerBadgeShown ? 1. : 0.);
if (!allowOnline || online == 0.) { if (!historyForCornerBadge || shown == 0.) {
peer->paintUserpicLeft( peer->paintUserpicLeft(
p, p,
_userpic, _userpic,
@ -192,34 +212,53 @@ void BasicRow::paintUserpic(
st::dialogsPadding.y(), st::dialogsPadding.y(),
fullWidth, fullWidth,
st::dialogsPhotoSize); st::dialogsPhotoSize);
if (!allowOnline || !_online) { if (!historyForCornerBadge || !_cornerBadgeShown) {
_onlineUserpic = nullptr; _cornerBadgeUserpic = nullptr;
} }
return; return;
} }
ensureOnlineUserpic(); ensureCornerBadgeUserpic();
if (_onlineUserpic->frame.isNull()) { if (_cornerBadgeUserpic->frame.isNull()) {
_onlineUserpic->frame = QImage( _cornerBadgeUserpic->frame = QImage(
st::dialogsPhotoSize * cRetinaFactor(), st::dialogsPhotoSize * cRetinaFactor(),
st::dialogsPhotoSize * cRetinaFactor(), st::dialogsPhotoSize * cRetinaFactor(),
QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
_onlineUserpic->frame.setDevicePixelRatio(cRetinaFactor()); _cornerBadgeUserpic->frame.setDevicePixelRatio(cRetinaFactor());
} }
const auto key = peer->userpicUniqueKey(_userpic); const auto key = peer->userpicUniqueKey(_userpic);
if (_onlineUserpic->online != online if (_cornerBadgeUserpic->shown != shown
|| _onlineUserpic->key != key || _cornerBadgeUserpic->key != key
|| _onlineUserpic->active != active) { || _cornerBadgeUserpic->active != active) {
_onlineUserpic->online = online; _cornerBadgeUserpic->shown = shown;
_onlineUserpic->key = key; _cornerBadgeUserpic->key = key;
_onlineUserpic->active = active; _cornerBadgeUserpic->active = active;
PaintOnlineFrame(_onlineUserpic.get(), peer, _userpic); PaintCornerBadgeFrame(_cornerBadgeUserpic.get(), peer, _userpic);
} }
p.drawImage(st::dialogsPadding, _onlineUserpic->frame); p.drawImage(st::dialogsPadding, _cornerBadgeUserpic->frame);
if (historyForCornerBadge->peer->isUser()) {
return;
}
p.setOpacity(shown);
const auto actionPainter = historyForCornerBadge->sendActionPainter();
const auto bg = active
? st::dialogsBgActive
: st::dialogsBg;
const auto size = st::dialogsCallBadgeSize;
const auto skip = st::dialogsCallBadgeSkip;
p.translate(st::dialogsPadding);
actionPainter->paintSpeaking(
p,
st::dialogsPhotoSize - skip.x() - size,
st::dialogsPhotoSize - skip.y() - size,
fullWidth,
bg,
now);
p.translate(-st::dialogsPadding);
} }
Row::Row(Key key, int pos) : _id(key), _pos(pos) { Row::Row(Key key, int pos) : _id(key), _pos(pos) {
if (const auto history = key.history()) { if (const auto history = key.history()) {
setOnline(Data::IsPeerAnOnlineUser(history->peer)); updateCornerBadgeShown(history->peer);
} }
} }

View file

@ -34,11 +34,14 @@ public:
BasicRow(); BasicRow();
~BasicRow(); ~BasicRow();
void setOnline(bool online, Fn<void()> updateCallback = nullptr) const; void updateCornerBadgeShown(
not_null<PeerData*> peer,
Fn<void()> updateCallback = nullptr) const;
void paintUserpic( void paintUserpic(
Painter &p, Painter &p,
not_null<PeerData*> peer, not_null<PeerData*> peer,
bool allowOnline, History *historyForCornerBadge,
crl::time now,
bool active, bool active,
int fullWidth) const; int fullWidth) const;
@ -57,24 +60,27 @@ public:
} }
private: private:
struct OnlineUserpic { struct CornerBadgeUserpic {
InMemoryKey key; InMemoryKey key;
float64 online = 0.; float64 shown = 0.;
bool active = false; bool active = false;
QImage frame; QImage frame;
Ui::Animations::Simple animation; Ui::Animations::Simple animation;
}; };
void ensureOnlineUserpic() const; void setCornerBadgeShown(
static void PaintOnlineFrame( bool shown,
not_null<OnlineUserpic*> data, Fn<void()> updateCallback) const;
void ensureCornerBadgeUserpic() const;
static void PaintCornerBadgeFrame(
not_null<CornerBadgeUserpic*> data,
not_null<PeerData*> peer, not_null<PeerData*> peer,
std::shared_ptr<Data::CloudImageView> &view); std::shared_ptr<Data::CloudImageView> &view);
mutable std::shared_ptr<Data::CloudImageView> _userpic; mutable std::shared_ptr<Data::CloudImageView> _userpic;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple; mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable std::unique_ptr<OnlineUserpic> _onlineUserpic; mutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;
mutable bool _online = false; mutable bool _cornerBadgeShown = false;
}; };

View file

@ -19,18 +19,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace HistoryView { namespace HistoryView {
namespace { namespace {
constexpr auto kStatusShowClientsideTyping = 6000; constexpr auto kStatusShowClientsideTyping = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideRecordVideo = 6000; constexpr auto kStatusShowClientsideRecordVideo = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideUploadVideo = 6000; constexpr auto kStatusShowClientsideUploadVideo = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideRecordVoice = 6000; constexpr auto kStatusShowClientsideRecordVoice = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideUploadVoice = 6000; constexpr auto kStatusShowClientsideUploadVoice = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideRecordRound = 6000; constexpr auto kStatusShowClientsideRecordRound = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideUploadRound = 6000; constexpr auto kStatusShowClientsideUploadRound = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideUploadPhoto = 6000; constexpr auto kStatusShowClientsideUploadPhoto = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideUploadFile = 6000; constexpr auto kStatusShowClientsideUploadFile = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideChooseLocation = 6000; constexpr auto kStatusShowClientsideChooseLocation = 6 * crl::time(1000);
constexpr auto kStatusShowClientsideChooseContact = 6000; constexpr auto kStatusShowClientsideChooseContact = 6 * crl::time(1000);
constexpr auto kStatusShowClientsidePlayGame = 10000; constexpr auto kStatusShowClientsidePlayGame = 10 * crl::time(1000);
constexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000);
} // namespace } // namespace
@ -101,7 +102,9 @@ bool SendActionPainter::updateNeedsAnimating(
emplaceAction(Type::PlayGame, kStatusShowClientsidePlayGame); emplaceAction(Type::PlayGame, kStatusShowClientsidePlayGame);
} }
}, [&](const MTPDspeakingInGroupCallAction &) { }, [&](const MTPDspeakingInGroupCallAction &) {
// #TODO calls _speaking.emplace_or_assign(
user,
now + kStatusShowClientsideSpeaking);
}, [&](const MTPDsendMessageCancelAction &) { }, [&](const MTPDsendMessageCancelAction &) {
Unexpected("CancelAction here."); Unexpected("CancelAction here.");
}); });
@ -134,15 +137,49 @@ bool SendActionPainter::paint(
return false; return false;
} }
void SendActionPainter::paintSpeaking(
Painter &p,
int x,
int y,
int outerWidth,
style::color color,
crl::time ms) {
if (_speakingAnimation) {
_speakingAnimation.paint(
p,
color,
x,
y,
outerWidth,
ms);
} else {
Ui::SendActionAnimation::PaintSpeakingIdle(
p,
color,
x,
y,
outerWidth);
}
}
bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) { bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
if (!_weak) { if (!_weak) {
return false; return false;
} }
auto changed = force; auto sendActionChanged = false;
auto speakingChanged = false;
for (auto i = begin(_typing); i != end(_typing);) { for (auto i = begin(_typing); i != end(_typing);) {
if (now >= i->second) { if (now >= i->second) {
i = _typing.erase(i); i = _typing.erase(i);
changed = true; sendActionChanged = true;
} else {
++i;
}
}
for (auto i = begin(_speaking); i != end(_speaking);) {
if (now >= i->second) {
i = _speaking.erase(i);
speakingChanged = true;
} else { } else {
++i; ++i;
} }
@ -150,12 +187,13 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
for (auto i = begin(_sendActions); i != end(_sendActions);) { for (auto i = begin(_sendActions); i != end(_sendActions);) {
if (now >= i->second.until) { if (now >= i->second.until) {
i = _sendActions.erase(i); i = _sendActions.erase(i);
changed = true; sendActionChanged = true;
} else { } else {
++i; ++i;
} }
} }
if (changed) { const auto wasSpeakingAnimation = !!_speakingAnimation;
if (force || sendActionChanged || speakingChanged) {
QString newTypingString; QString newTypingString;
auto typingCount = _typing.size(); auto typingCount = _typing.size();
if (typingCount > 2) { if (typingCount > 2) {
@ -232,7 +270,7 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
if (typingCount > 0) { if (typingCount > 0) {
_sendActionAnimation.start(Api::SendProgressType::Typing); _sendActionAnimation.start(Api::SendProgressType::Typing);
} else if (newTypingString.isEmpty()) { } else if (newTypingString.isEmpty()) {
_sendActionAnimation.stop(); _sendActionAnimation.tryToFinish();
} }
if (_sendActionString != newTypingString) { if (_sendActionString != newTypingString) {
_sendActionString = newTypingString; _sendActionString = newTypingString;
@ -241,17 +279,34 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
_sendActionString, _sendActionString,
Ui::NameTextOptions()); Ui::NameTextOptions());
} }
if (_speaking.empty()) {
_speakingAnimation.tryToFinish();
} else {
_speakingAnimation.start(Api::SendProgressType::Speaking);
}
} else if (_speaking.empty() && _speakingAnimation) {
_speakingAnimation.tryToFinish();
} }
const auto result = (!_typing.empty() || !_sendActions.empty()); const auto sendActionResult = !_typing.empty() || !_sendActions.empty();
if (changed || (result && !anim::Disabled())) { const auto speakingResult = !_speaking.empty() || wasSpeakingAnimation;
if (force
|| sendActionChanged
|| (sendActionResult && !anim::Disabled())) {
_history->peer->owner().updateSendActionAnimation({ _history->peer->owner().updateSendActionAnimation({
_history, _history,
_sendActionAnimation.width(), _sendActionAnimation.width(),
st::normalFont->height, st::normalFont->height,
changed (force || sendActionChanged)
}); });
} }
return result; if (force
|| speakingChanged
|| (speakingResult && !anim::Disabled())) {
_history->peer->owner().updateSpeakingAnimation({
_history
});
}
return sendActionResult || speakingResult;
} }
void SendActionPainter::clear(not_null<UserData*> from) { void SendActionPainter::clear(not_null<UserData*> from) {

View file

@ -35,6 +35,13 @@ public:
int outerWidth, int outerWidth,
style::color color, style::color color,
crl::time now); crl::time now);
void paintSpeaking(
Painter &p,
int x,
int y,
int outerWidth,
style::color color,
crl::time now);
bool updateNeedsAnimating( bool updateNeedsAnimating(
crl::time now, crl::time now,
@ -48,10 +55,12 @@ private:
const not_null<History*> _history; const not_null<History*> _history;
const base::weak_ptr<Main::Session> _weak; const base::weak_ptr<Main::Session> _weak;
base::flat_map<not_null<UserData*>, crl::time> _typing; base::flat_map<not_null<UserData*>, crl::time> _typing;
base::flat_map<not_null<UserData*>, crl::time> _speaking;
base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions; base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;
QString _sendActionString; QString _sendActionString;
Ui::Text::String _sendActionText; Ui::Text::String _sendActionText;
Ui::SendActionAnimation _sendActionAnimation; Ui::SendActionAnimation _sendActionAnimation;
Ui::SendActionAnimation _speakingAnimation;
}; };

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_send_progress.h" #include "api/api_send_progress.h"
#include "ui/effects/animation_value.h" #include "ui/effects/animation_value.h"
#include "styles/style_widgets.h" #include "styles/style_widgets.h"
#include "styles/style_dialogs.h"
namespace Ui { namespace Ui {
namespace { namespace {
@ -17,6 +18,58 @@ namespace {
constexpr int kTypingDotsCount = 3; constexpr int kTypingDotsCount = 3;
constexpr int kRecordArcsCount = 4; constexpr int kRecordArcsCount = 4;
constexpr int kUploadArrowsCount = 3; constexpr int kUploadArrowsCount = 3;
constexpr int kSpeakingPartsCount = 3;
constexpr auto kSpeakingDuration = 3200;
constexpr auto kSpeakingFadeDuration = 400;
} // namespace
class SendActionAnimation::Impl {
public:
using Type = Api::SendProgressType;
Impl(int period) : _period(period), _started(crl::now()) {
}
struct MetaData {
int index;
std::unique_ptr<Impl>(*creator)();
};
virtual const MetaData *metaData() const = 0;
bool supports(Type type) const;
virtual int width() const = 0;
virtual void paint(
Painter &p,
style::color color,
int x,
int y,
int outerWidth,
crl::time now) = 0;
virtual void restartedAt(crl::time now) {
}
virtual bool finishNow() {
return true;
}
virtual ~Impl() = default;
protected:
[[nodiscard]] crl::time started() const {
return _started;
}
[[nodiscard]] int frameTime(crl::time now) const {
return anim::Disabled() ? 0 : (std::max(now - _started, crl::time(0)) % _period);
}
private:
int _period = 1;
crl::time _started = 0;
};
namespace {
using ImplementationsMap = QMap<Api::SendProgressType, const SendActionAnimation::Impl::MetaData*>; using ImplementationsMap = QMap<Api::SendProgressType, const SendActionAnimation::Impl::MetaData*>;
NeverFreedPointer<ImplementationsMap> Implementations; NeverFreedPointer<ImplementationsMap> Implementations;
@ -38,17 +91,17 @@ public:
return st::historySendActionTypingPosition.x() + kTypingDotsCount * st::historySendActionTypingDelta; return st::historySendActionTypingPosition.x() + kTypingDotsCount * st::historySendActionTypingDelta;
} }
private: void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override;
void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override;
}; };
const TypingAnimation::MetaData TypingAnimation::kMeta = { 0, &TypingAnimation::create }; const TypingAnimation::MetaData TypingAnimation::kMeta = { 0, &TypingAnimation::create };
void TypingAnimation::paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) { void TypingAnimation::paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) {
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(color); p.setBrush(color);
auto frameMs = frameTime(now);
auto position = QPointF(x + 0.5, y - 0.5) + st::historySendActionTypingPosition; auto position = QPointF(x + 0.5, y - 0.5) + st::historySendActionTypingPosition;
for (auto i = 0; i != kTypingDotsCount; ++i) { for (auto i = 0; i != kTypingDotsCount; ++i) {
auto r = st::historySendActionTypingSmallNumerator / st::historySendActionTypingDenominator; auto r = st::historySendActionTypingSmallNumerator / st::historySendActionTypingDenominator;
@ -83,15 +136,15 @@ public:
return st::historySendActionRecordPosition.x() + (kRecordArcsCount + 1) * st::historySendActionRecordDelta; return st::historySendActionRecordPosition.x() + (kRecordArcsCount + 1) * st::historySendActionRecordDelta;
} }
private: void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override;
void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override;
}; };
const RecordAnimation::MetaData RecordAnimation::kMeta = { 0, &RecordAnimation::create }; const RecordAnimation::MetaData RecordAnimation::kMeta = { 0, &RecordAnimation::create };
void RecordAnimation::paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) { void RecordAnimation::paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) {
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
const auto frameMs = frameTime(now);
auto pen = color->p; auto pen = color->p;
pen.setWidth(st::historySendActionRecordStrokeNumerator / st::historySendActionRecordDenominator); pen.setWidth(st::historySendActionRecordStrokeNumerator / st::historySendActionRecordDenominator);
pen.setJoinStyle(Qt::RoundJoin); pen.setJoinStyle(Qt::RoundJoin);
@ -127,15 +180,15 @@ public:
return st::historySendActionUploadPosition.x() + (kUploadArrowsCount + 1) * st::historySendActionUploadDelta; return st::historySendActionUploadPosition.x() + (kUploadArrowsCount + 1) * st::historySendActionUploadDelta;
} }
private: void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override;
void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override;
}; };
const UploadAnimation::MetaData UploadAnimation::kMeta = { 0, &UploadAnimation::create }; const UploadAnimation::MetaData UploadAnimation::kMeta = { 0, &UploadAnimation::create };
void UploadAnimation::paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) { void UploadAnimation::paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) {
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
const auto frameMs = frameTime(now);
auto pen = color->p; auto pen = color->p;
pen.setWidth(st::historySendActionUploadStrokeNumerator / st::historySendActionUploadDenominator); pen.setWidth(st::historySendActionUploadStrokeNumerator / st::historySendActionUploadDenominator);
pen.setJoinStyle(Qt::RoundJoin); pen.setJoinStyle(Qt::RoundJoin);
@ -159,71 +212,237 @@ void UploadAnimation::paintFrame(Painter &p, style::color color, int x, int y, i
p.translate(-position); p.translate(-position);
} }
class SpeakingAnimation : public SendActionAnimation::Impl {
public:
SpeakingAnimation();
static const MetaData kMeta;
static std::unique_ptr<Impl> create() {
return std::make_unique<SpeakingAnimation>();
}
const MetaData *metaData() const override {
return &kMeta;
}
int width() const override {
return 4 * (st::dialogsSpeakingStrokeNumerator / st::dialogsSpeakingDenominator);
}
void restartedAt(crl::time now) override;
bool finishNow() override;
static void PaintIdle(Painter &p, style::color color, int x, int y, int outerWidth);
void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override;
private:
static void PaintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs, float64 started);
crl::time _startStarted = 0;
crl::time _finishStarted = 0;
};
const SpeakingAnimation::MetaData SpeakingAnimation::kMeta = { 0, &SpeakingAnimation::create };
SpeakingAnimation::SpeakingAnimation()
: Impl(kSpeakingDuration)
, _startStarted(crl::now()) {
}
void SpeakingAnimation::restartedAt(crl::time now) {
if (!_finishStarted) {
return;
}
const auto finishFinishes = _finishStarted + kSpeakingFadeDuration;
const auto leftToFinish = (finishFinishes - now);
if (leftToFinish > 0) {
_startStarted = now - leftToFinish;
} else {
_startStarted = now;
}
_finishStarted = 0;
}
bool SpeakingAnimation::finishNow() {
const auto now = crl::now();
if (_finishStarted) {
return (_finishStarted + kSpeakingFadeDuration <= now);
} else if (_startStarted >= now) {
return true;
}
const auto startFinishes = _startStarted + kSpeakingFadeDuration;
const auto leftToStart = (startFinishes - now);
if (leftToStart > 0) {
_finishStarted = now - leftToStart;
} else {
_finishStarted = now;
}
return false;
}
void SpeakingAnimation::PaintIdle(Painter &p, style::color color, int x, int y, int outerWidth) {
PaintFrame(p, color, x, y, outerWidth, 0, 0.);
PainterHighQualityEnabler hq(p);
const auto line = st::dialogsSpeakingStrokeNumerator / (2 * st::dialogsSpeakingDenominator);
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto half = st::dialogsCallBadgeSize / 2.;
const auto center = QPointF(x + half, y + half);
auto middleSize = line;
auto sideSize = line;
auto left = center.x() - 4 * line;
p.drawRoundedRect(left, center.y() - line * 2, 2 * line, 4 * line, line, line);
left += 3 * line;
p.drawRoundedRect(left, center.y() - line * 2, 2 * line, 4 * line, line, line);
left += 3 * line;
p.drawRoundedRect(left, center.y() - line * 2, 2 * line, 4 * line, line, line);
}
void SpeakingAnimation::paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) {
const auto started = _finishStarted
? (1. - ((now - _finishStarted) / float64(kSpeakingFadeDuration)))
: (now - _startStarted) / float64(kSpeakingFadeDuration);
PaintFrame(p, color, x, y, outerWidth, frameTime(now), std::clamp(started, 0., 1.));
}
void SpeakingAnimation::PaintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs, float64 started) {
PainterHighQualityEnabler hq(p);
const auto line = st::dialogsSpeakingStrokeNumerator / (2 * st::dialogsSpeakingDenominator);
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto duration = kSpeakingDuration;
const auto stageDuration = duration / 8;
const auto fullprogress = frameMs;
const auto stage = fullprogress / stageDuration;
const auto progress = (fullprogress - stage * stageDuration) / float64(stageDuration);
const auto half = st::dialogsCallBadgeSize / 2.;
const auto center = QPointF(x + half, y + half);
const auto middleSize = [&] {
if (!started) {
return 2 * line;
}
auto result = line;
switch (stage) {
case 0: result += 4 * line * progress; break;
case 1: result += 4 * line * (1. - progress); break;
case 2: result += 2 * line * progress; break;
case 3: result += 2 * line * (1. - progress); break;
case 4: result += 4 * line * progress; break;
case 5: result += 4 * line * (1. - progress); break;
case 6: result += 4 * line * progress; break;
case 7: result += 4 * line * (1. - progress); break;
}
return (started == 1.)
? result
: (started * result) + ((1. - started) * 2 * line);
}();
const auto sideSize = [&] {
if (!started) {
return 2 * line;
}
auto result = line;
switch (stage) {
case 0: result += 2 * line * (1. - progress); break;
case 1: result += 4 * line * progress; break;
case 2: result += 4 * line * (1. - progress); break;
case 3: result += 2 * line * progress; break;
case 4: result += 2 * line * (1. - progress); break;
case 5: result += 4 * line * progress; break;
case 6: result += 4 * line * (1. - progress); break;
case 7: result += 2 * line * progress; break;
}
return (started == 1.)
? result
: (started * result) + ((1. - started) * 2 * line);
}();
auto left = center.x() - 4 * line;
p.drawRoundedRect(left, center.y() - sideSize, 2 * line, 2 * sideSize, line, line);
left += 3 * line;
p.drawRoundedRect(left, center.y() - middleSize, 2 * line, 2 * middleSize, line, line);
left += 3 * line;
p.drawRoundedRect(left, center.y() - sideSize, 2 * line, 2 * sideSize, line, line);
}
void CreateImplementationsMap() { void CreateImplementationsMap() {
if (Implementations) { if (Implementations) {
return; return;
} }
using Type = Api::SendProgressType; using Type = Api::SendProgressType;
Implementations.createIfNull(); Implementations.createIfNull();
Type recordTypes[] = { static constexpr auto kRecordTypes = {
Type::RecordVideo, Type::RecordVideo,
Type::RecordVoice, Type::RecordVoice,
Type::RecordRound, Type::RecordRound,
}; };
for_const (auto type, recordTypes) { for (const auto type : kRecordTypes) {
Implementations->insert(type, &RecordAnimation::kMeta); Implementations->insert(type, &RecordAnimation::kMeta);
} }
Type uploadTypes[] = { static constexpr auto kUploadTypes = {
Type::UploadFile, Type::UploadFile,
Type::UploadPhoto, Type::UploadPhoto,
Type::UploadVideo, Type::UploadVideo,
Type::UploadVoice, Type::UploadVoice,
Type::UploadRound, Type::UploadRound,
}; };
for_const (auto type, uploadTypes) { for (const auto type : kUploadTypes) {
Implementations->insert(type, &UploadAnimation::kMeta); Implementations->insert(type, &UploadAnimation::kMeta);
} }
Implementations->insert(Type::Speaking, &SpeakingAnimation::kMeta);
} }
} // namespace } // namespace
SendActionAnimation::SendActionAnimation() = default;
SendActionAnimation::~SendActionAnimation() = default;
bool SendActionAnimation::Impl::supports(Type type) const { bool SendActionAnimation::Impl::supports(Type type) const {
CreateImplementationsMap(); CreateImplementationsMap();
return Implementations->value(type, &TypingAnimation::kMeta) == metaData(); return Implementations->value(type, &TypingAnimation::kMeta) == metaData();
} }
void SendActionAnimation::Impl::paint(
Painter &p,
style::color color,
int x,
int y,
int outerWidth,
crl::time ms) {
paintFrame(
p,
color,
x,
y,
outerWidth,
anim::Disabled() ? 0 : (qMax(ms - _started, crl::time(0)) % _period));
}
void SendActionAnimation::start(Type type) { void SendActionAnimation::start(Type type) {
if (!_impl || !_impl->supports(type)) { if (!_impl || !_impl->supports(type)) {
_impl = createByType(type); _impl = CreateByType(type);
} else {
_impl->restartedAt(crl::now());
} }
} }
void SendActionAnimation::stop() { void SendActionAnimation::tryToFinish() {
_impl.reset(); if (!_impl) {
return;
} else if (_impl->finishNow()) {
_impl.reset();
}
} }
std::unique_ptr<SendActionAnimation::Impl> SendActionAnimation::createByType(Type type) { int SendActionAnimation::width() const {
return _impl ? _impl->width() : 0;
}
void SendActionAnimation::paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time ms) const {
if (_impl) {
_impl->paint(p, color, x, y, outerWidth, ms);
}
}
void SendActionAnimation::PaintSpeakingIdle(Painter &p, style::color color, int x, int y, int outerWidth) {
SpeakingAnimation::PaintIdle(p, color, x, y, outerWidth);
}
auto SendActionAnimation::CreateByType(Type type) -> std::unique_ptr<Impl> {
CreateImplementationsMap(); CreateImplementationsMap();
return Implementations->value(type, &TypingAnimation::kMeta)->creator(); return Implementations->value(type, &TypingAnimation::kMeta)->creator();
} }
SendActionAnimation::~SendActionAnimation() = default;
} // namespace Ui } // namespace Ui

View file

@ -16,60 +16,25 @@ namespace Ui {
class SendActionAnimation { class SendActionAnimation {
public: public:
using Type = Api::SendProgressType; using Type = Api::SendProgressType;
class Impl;
SendActionAnimation();
~SendActionAnimation();
void start(Type type); void start(Type type);
void stop(); void tryToFinish();
int width() const { int width() const;
return _impl ? _impl->width() : 0; void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time ms) const;
}
void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time ms) {
if (_impl) {
_impl->paint(p, color, x, y, outerWidth, ms);
}
}
explicit operator bool() const { explicit operator bool() const {
return _impl != nullptr; return _impl != nullptr;
} }
class Impl { static void PaintSpeakingIdle(Painter &p, style::color color, int x, int y, int outerWidth);
public:
using Type = Api::SendProgressType;
Impl(int period) : _period(period), _started(crl::now()) {
}
struct MetaData {
int index;
std::unique_ptr<Impl> (*creator)();
};
virtual const MetaData *metaData() const = 0;
bool supports(Type type) const;
virtual int width() const = 0;
void paint(
Painter &p,
style::color color,
int x,
int y,
int outerWidth,
crl::time ms);
virtual ~Impl() = default;
private:
virtual void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) = 0;
int _period = 1;
crl::time _started = 0;
};
~SendActionAnimation();
private: private:
std::unique_ptr<Impl> createByType(Type type); [[nodiscard]] static std::unique_ptr<Impl> CreateByType(Type type);
std::unique_ptr<Impl> _impl; std::unique_ptr<Impl> _impl;

@ -1 +1 @@
Subproject commit 7fd90cb38b6b1069af43039f93c991fffaf8ddea Subproject commit 87ee83bc7336b4c814e2f0dc45261ff8e280cca0