From fdbe6bdeb2723b3976a5398ec26713778bf3a232 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 2 Dec 2020 13:52:19 +0300 Subject: [PATCH] Add voice chat indicator in the chats list. --- .../SourceFiles/data/data_peer_values.cpp | 11 +- Telegram/SourceFiles/data/data_peer_values.h | 3 +- Telegram/SourceFiles/data/data_session.cpp | 9 + Telegram/SourceFiles/data/data_session.h | 4 + Telegram/SourceFiles/dialogs/dialogs.style | 8 +- .../dialogs/dialogs_inner_widget.cpp | 88 ++++-- .../dialogs/dialogs_inner_widget.h | 6 + .../SourceFiles/dialogs/dialogs_layout.cpp | 6 +- Telegram/SourceFiles/dialogs/dialogs_row.cpp | 137 ++++++--- Telegram/SourceFiles/dialogs/dialogs_row.h | 24 +- .../history/view/history_view_send_action.cpp | 99 ++++-- .../history/view/history_view_send_action.h | 9 + .../ui/effects/send_action_animations.cpp | 291 +++++++++++++++--- .../ui/effects/send_action_animations.h | 53 +--- Telegram/lib_ui | 2 +- 15 files changed, 556 insertions(+), 194 deletions(-) diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index d0cf78fd0..74eb650bf 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -438,11 +438,12 @@ bool OnlineTextActive(not_null user, TimeId now) { return OnlineTextActive(user->onlineTill, now); } -bool IsPeerAnOnlineUser(not_null peer) { - if (const auto user = peer->asUser()) { - return OnlineTextActive(user, base::unixtime::now()); - } - return false; +bool IsUserOnline(not_null user) { + return OnlineTextActive(user, base::unixtime::now()); +} + +bool ChannelHasActiveCall(not_null channel) { + return (channel->flags() & MTPDchannel::Flag::f_call_active); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index c913761f8..04d12b391 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -120,6 +120,7 @@ inline auto PeerFullFlagValue( [[nodiscard]] QString OnlineTextFull(not_null user, TimeId now); [[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now); [[nodiscard]] bool OnlineTextActive(not_null user, TimeId now); -[[nodiscard]] bool IsPeerAnOnlineUser(not_null peer); +[[nodiscard]] bool IsUserOnline(not_null user); +[[nodiscard]] bool ChannelHasActiveCall(not_null channel); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 38f887599..bf914276c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2181,6 +2181,15 @@ void Session::updateSendActionAnimation( _sendActionAnimationUpdate.fire(std::move(update)); } +auto Session::speakingAnimationUpdated() const +-> rpl::producer> { + return _speakingAnimationUpdate.events(); +} + +void Session::updateSpeakingAnimation(not_null history) { + _speakingAnimationUpdate.fire_copy(history); +} + int Session::unreadBadge() const { return computeUnreadBadge(_chatsList.unreadState()); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index f3d05032d..583c36ec1 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -400,6 +400,9 @@ public: [[nodiscard]] auto sendActionAnimationUpdated() const -> rpl::producer; void updateSendActionAnimation(SendActionAnimationUpdate &&update); + [[nodiscard]] auto speakingAnimationUpdated() const + -> rpl::producer>; + void updateSpeakingAnimation(not_null history); using SendActionPainter = HistoryView::SendActionPainter; [[nodiscard]] std::shared_ptr repliesSendActionPainter( @@ -951,6 +954,7 @@ private: std::unique_ptr _passportCredentials; rpl::event_stream _sendActionAnimationUpdate; + rpl::event_stream> _speakingAnimationUpdate; std::vector _wallpapers; int32 _wallpapersHash = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index a9dcfc89d..63b08e1a6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -36,9 +36,15 @@ dialogsPadding: point(10px, 8px); dialogsOnlineBadgeStroke: 2px; dialogsOnlineBadgeSize: 10px; -dialogsOnlineBadgeSkip: point(10px, 12px); +dialogsOnlineBadgeSkip: point(0px, 2px); dialogsOnlineBadgeDuration: 150; +dialogsCallBadgeSize: 16px; +dialogsCallBadgeSkip: point(-1px, 1px); + +dialogsSpeakingStrokeNumerator: 16px; +dialogsSpeakingDenominator: 8.; + dialogsImportantBarHeight: 37px; dialogsSkip: 8px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 169d13e83..63ca47022 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -189,6 +189,11 @@ InnerWidget::InnerWidget( UpdateRowSection::Default | UpdateRowSection::Filtered); }, lifetime()); + session().data().speakingAnimationUpdated( + ) | rpl::start_with_next([=](not_null history) { + updateDialogRowCornerStatus(history); + }, lifetime()); + setupOnlineStatusCheck(); rpl::merge( @@ -2959,11 +2964,43 @@ MsgId InnerWidget::lastSearchMigratedId() const { void InnerWidget::setupOnlineStatusCheck() { session().changes().peerUpdates( Data::PeerUpdate::Flag::OnlineStatus + | Data::PeerUpdate::Flag::GroupCall ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { - userOnlineUpdated(update.peer); + if (update.peer->isUser()) { + userOnlineUpdated(update.peer); + } else { + groupHasCallUpdated(update.peer); + } }, lifetime()); } +void InnerWidget::updateDialogRowCornerStatus(not_null 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 peer) { const auto user = peer->isSelf() ? nullptr : peer->asUser(); if (!user) { @@ -2973,32 +3010,35 @@ void InnerWidget::userOnlineUpdated(not_null peer) { if (!history) { return; } - const auto size = st::dialogsOnlineBadgeSize; - const auto stroke = st::dialogsOnlineBadgeStroke; - const auto skip = st::dialogsOnlineBadgeSkip; - const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize; - const auto updateRect = QRect( - edge - skip.x() - size, - edge - skip.y() - size, - size, - size - ).marginsAdded( - { stroke, stroke, stroke, stroke } - ).translated( - st::dialogsPadding - ); + updateRowCornerStatusShown( + history, + Data::OnlineTextActive(user, base::unixtime::now())); +} + +void InnerWidget::groupHasCallUpdated(not_null peer) { + const auto group = peer->asMegagroup(); + if (!group) { + return; + } + const auto history = session().data().historyLoaded(group); + if (!history) { + return; + } + updateRowCornerStatusShown( + history, + group->flags() & MTPDchannel::Flag::f_call_active); +} + +void InnerWidget::updateRowCornerStatusShown( + not_null history, + bool shown) { const auto repaint = [=] { - updateDialogRow( - RowDescriptor( - history, - FullMsgId()), - updateRect, - UpdateRowSection::Default | UpdateRowSection::Filtered); + updateDialogRowCornerStatus(history); }; repaint(); const auto findRow = [&](not_null history) - -> std::pair { + -> std::pair { if (state() == WidgetState::Default) { const auto row = shownDialogs()->getRow({ history }); return { row, row ? defaultRowTop(row) : 0 }; @@ -3014,8 +3054,8 @@ void InnerWidget::userOnlineUpdated(not_null peer) { if (const auto &[row, top] = findRow(history); row != nullptr) { const auto visible = (top < _visibleBottom) && (top + st::dialogsRowHeight > _visibleTop); - row->setOnline( - Data::OnlineTextActive(user, base::unixtime::now()), + row->updateCornerBadgeShown( + history->peer, visible ? Fn(crl::guard(this, repaint)) : nullptr); } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index ea3a59fa2..254b622c1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -224,6 +224,12 @@ private: int defaultRowTop(not_null row) const; void setupOnlineStatusCheck(); void userOnlineUpdated(not_null peer); + void groupHasCallUpdated(not_null peer); + + void updateRowCornerStatusShown( + not_null history, + bool shown); + void updateDialogRowCornerStatus(not_null history); void setupShortcuts(); RowDescriptor computeJump( diff --git a/Telegram/SourceFiles/dialogs/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/dialogs_layout.cpp index 1c7f238d5..8d52c7e4c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_layout.cpp @@ -258,6 +258,8 @@ void paintRow( p.fillRect(fullRect, bg); row->paintRipple(p, 0, 0, fullWidth, &ripple->c); + const auto history = chat.history(); + if (flags & Flag::SavedMessages) { Ui::EmptyUserpic::PaintSavedMessages( p, @@ -276,7 +278,8 @@ void paintRow( row->paintUserpic( p, from, - (flags & Flag::AllowUserOnline), + (flags & Flag::AllowUserOnline) ? history : nullptr, + ms, active, fullWidth); } else if (hiddenSenderInfo) { @@ -306,7 +309,6 @@ void paintRow( return; } - const auto history = chat.history(); auto namewidth = fullWidth - nameleft - st::dialogsPadding.x(); auto rectForName = QRect( nameleft, diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index 97e045275..ba46161dd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -71,27 +71,29 @@ QString ComposeFolderListEntryText(not_null folder) { BasicRow::BasicRow() = default; BasicRow::~BasicRow() = default; -void BasicRow::setOnline(bool online, Fn updateCallback) const { - if (_online == online) { +void BasicRow::setCornerBadgeShown( + bool shown, + Fn updateCallback) const { + if (_cornerBadgeShown == shown) { return; } - _online = online; - if (_onlineUserpic && _onlineUserpic->animation.animating()) { - _onlineUserpic->animation.change( - _online ? 1. : 0., + _cornerBadgeShown = shown; + if (_cornerBadgeUserpic && _cornerBadgeUserpic->animation.animating()) { + _cornerBadgeUserpic->animation.change( + _cornerBadgeShown ? 1. : 0., st::dialogsOnlineBadgeDuration); } else if (updateCallback) { - ensureOnlineUserpic(); - _onlineUserpic->animation.start( + ensureCornerBadgeUserpic(); + _cornerBadgeUserpic->animation.start( std::move(updateCallback), - _online ? 0. : 1., - _online ? 1. : 0., + _cornerBadgeShown ? 0. : 1., + _cornerBadgeShown ? 1. : 0., st::dialogsOnlineBadgeDuration); } - if (!_online - && _onlineUserpic - && !_onlineUserpic->animation.animating()) { - _onlineUserpic = nullptr; + if (!_cornerBadgeShown + && _cornerBadgeUserpic + && !_cornerBadgeUserpic->animation.animating()) { + _cornerBadgeUserpic = nullptr; } } @@ -129,15 +131,29 @@ void BasicRow::paintRipple( } } -void BasicRow::ensureOnlineUserpic() const { - if (_onlineUserpic) { - return; - } - _onlineUserpic = std::make_unique(); +void BasicRow::updateCornerBadgeShown( + not_null peer, + Fn updateCallback) const { + const auto shown = [&] { + 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( - not_null data, +void BasicRow::ensureCornerBadgeUserpic() const { + if (_cornerBadgeUserpic) { + return; + } + _cornerBadgeUserpic = std::make_unique(); +} + +void BasicRow::PaintCornerBadgeFrame( + not_null data, not_null peer, std::shared_ptr &view) { data->frame.fill(Qt::transparent); @@ -153,21 +169,24 @@ void BasicRow::PaintOnlineFrame( PainterHighQualityEnabler hq(q); 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 skip = st::dialogsOnlineBadgeSkip; - const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize; - const auto shrink = (size / 2) * (1. - data->online); + const auto skip = peer->isUser() + ? st::dialogsOnlineBadgeSkip + : st::dialogsCallBadgeSkip; + const auto shrink = (size / 2) * (1. - data->shown); auto pen = QPen(Qt::transparent); - pen.setWidthF(stroke * data->online); + pen.setWidthF(stroke * data->shown); q.setPen(pen); q.setBrush(data->active ? st::dialogsOnlineBadgeFgActive : st::dialogsOnlineBadgeFg); q.drawEllipse(QRectF( - edge - skip.x() - size, - edge - skip.y() - size, + st::dialogsPhotoSize - skip.x() - size, + st::dialogsPhotoSize - skip.y() - size, size, size ).marginsRemoved({ shrink, shrink, shrink, shrink })); @@ -176,15 +195,16 @@ void BasicRow::PaintOnlineFrame( void BasicRow::paintUserpic( Painter &p, not_null peer, - bool allowOnline, + History *historyForCornerBadge, + crl::time now, bool active, int fullWidth) const { - setOnline(Data::IsPeerAnOnlineUser(peer)); + updateCornerBadgeShown(peer); - const auto online = _onlineUserpic - ? _onlineUserpic->animation.value(_online ? 1. : 0.) - : (_online ? 1. : 0.); - if (!allowOnline || online == 0.) { + const auto shown = _cornerBadgeUserpic + ? _cornerBadgeUserpic->animation.value(_cornerBadgeShown ? 1. : 0.) + : (_cornerBadgeShown ? 1. : 0.); + if (!historyForCornerBadge || shown == 0.) { peer->paintUserpicLeft( p, _userpic, @@ -192,34 +212,53 @@ void BasicRow::paintUserpic( st::dialogsPadding.y(), fullWidth, st::dialogsPhotoSize); - if (!allowOnline || !_online) { - _onlineUserpic = nullptr; + if (!historyForCornerBadge || !_cornerBadgeShown) { + _cornerBadgeUserpic = nullptr; } return; } - ensureOnlineUserpic(); - if (_onlineUserpic->frame.isNull()) { - _onlineUserpic->frame = QImage( + ensureCornerBadgeUserpic(); + if (_cornerBadgeUserpic->frame.isNull()) { + _cornerBadgeUserpic->frame = QImage( st::dialogsPhotoSize * cRetinaFactor(), st::dialogsPhotoSize * cRetinaFactor(), QImage::Format_ARGB32_Premultiplied); - _onlineUserpic->frame.setDevicePixelRatio(cRetinaFactor()); + _cornerBadgeUserpic->frame.setDevicePixelRatio(cRetinaFactor()); } const auto key = peer->userpicUniqueKey(_userpic); - if (_onlineUserpic->online != online - || _onlineUserpic->key != key - || _onlineUserpic->active != active) { - _onlineUserpic->online = online; - _onlineUserpic->key = key; - _onlineUserpic->active = active; - PaintOnlineFrame(_onlineUserpic.get(), peer, _userpic); + if (_cornerBadgeUserpic->shown != shown + || _cornerBadgeUserpic->key != key + || _cornerBadgeUserpic->active != active) { + _cornerBadgeUserpic->shown = shown; + _cornerBadgeUserpic->key = key; + _cornerBadgeUserpic->active = active; + 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) { if (const auto history = key.history()) { - setOnline(Data::IsPeerAnOnlineUser(history->peer)); + updateCornerBadgeShown(history->peer); } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index 6871a38f0..0bbfaf8fa 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -34,11 +34,14 @@ public: BasicRow(); ~BasicRow(); - void setOnline(bool online, Fn updateCallback = nullptr) const; + void updateCornerBadgeShown( + not_null peer, + Fn updateCallback = nullptr) const; void paintUserpic( Painter &p, not_null peer, - bool allowOnline, + History *historyForCornerBadge, + crl::time now, bool active, int fullWidth) const; @@ -57,24 +60,27 @@ public: } private: - struct OnlineUserpic { + struct CornerBadgeUserpic { InMemoryKey key; - float64 online = 0.; + float64 shown = 0.; bool active = false; QImage frame; Ui::Animations::Simple animation; }; - void ensureOnlineUserpic() const; - static void PaintOnlineFrame( - not_null data, + void setCornerBadgeShown( + bool shown, + Fn updateCallback) const; + void ensureCornerBadgeUserpic() const; + static void PaintCornerBadgeFrame( + not_null data, not_null peer, std::shared_ptr &view); mutable std::shared_ptr _userpic; mutable std::unique_ptr _ripple; - mutable std::unique_ptr _onlineUserpic; - mutable bool _online = false; + mutable std::unique_ptr _cornerBadgeUserpic; + mutable bool _cornerBadgeShown = false; }; diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp index 8719aacfb..c7bd84f83 100644 --- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp +++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp @@ -19,18 +19,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -constexpr auto kStatusShowClientsideTyping = 6000; -constexpr auto kStatusShowClientsideRecordVideo = 6000; -constexpr auto kStatusShowClientsideUploadVideo = 6000; -constexpr auto kStatusShowClientsideRecordVoice = 6000; -constexpr auto kStatusShowClientsideUploadVoice = 6000; -constexpr auto kStatusShowClientsideRecordRound = 6000; -constexpr auto kStatusShowClientsideUploadRound = 6000; -constexpr auto kStatusShowClientsideUploadPhoto = 6000; -constexpr auto kStatusShowClientsideUploadFile = 6000; -constexpr auto kStatusShowClientsideChooseLocation = 6000; -constexpr auto kStatusShowClientsideChooseContact = 6000; -constexpr auto kStatusShowClientsidePlayGame = 10000; +constexpr auto kStatusShowClientsideTyping = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideRecordVideo = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideUploadVideo = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideRecordVoice = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideUploadVoice = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideRecordRound = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideUploadRound = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideUploadPhoto = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideUploadFile = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideChooseLocation = 6 * crl::time(1000); +constexpr auto kStatusShowClientsideChooseContact = 6 * crl::time(1000); +constexpr auto kStatusShowClientsidePlayGame = 10 * crl::time(1000); +constexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000); } // namespace @@ -101,7 +102,9 @@ bool SendActionPainter::updateNeedsAnimating( emplaceAction(Type::PlayGame, kStatusShowClientsidePlayGame); } }, [&](const MTPDspeakingInGroupCallAction &) { - // #TODO calls + _speaking.emplace_or_assign( + user, + now + kStatusShowClientsideSpeaking); }, [&](const MTPDsendMessageCancelAction &) { Unexpected("CancelAction here."); }); @@ -134,15 +137,49 @@ bool SendActionPainter::paint( 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) { if (!_weak) { return false; } - auto changed = force; + auto sendActionChanged = false; + auto speakingChanged = false; for (auto i = begin(_typing); i != end(_typing);) { if (now >= i->second) { 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 { ++i; } @@ -150,12 +187,13 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) { for (auto i = begin(_sendActions); i != end(_sendActions);) { if (now >= i->second.until) { i = _sendActions.erase(i); - changed = true; + sendActionChanged = true; } else { ++i; } } - if (changed) { + const auto wasSpeakingAnimation = !!_speakingAnimation; + if (force || sendActionChanged || speakingChanged) { QString newTypingString; auto typingCount = _typing.size(); if (typingCount > 2) { @@ -232,7 +270,7 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) { if (typingCount > 0) { _sendActionAnimation.start(Api::SendProgressType::Typing); } else if (newTypingString.isEmpty()) { - _sendActionAnimation.stop(); + _sendActionAnimation.tryToFinish(); } if (_sendActionString != newTypingString) { _sendActionString = newTypingString; @@ -241,17 +279,34 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) { _sendActionString, 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()); - if (changed || (result && !anim::Disabled())) { + const auto sendActionResult = !_typing.empty() || !_sendActions.empty(); + const auto speakingResult = !_speaking.empty() || wasSpeakingAnimation; + if (force + || sendActionChanged + || (sendActionResult && !anim::Disabled())) { _history->peer->owner().updateSendActionAnimation({ _history, _sendActionAnimation.width(), 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 from) { diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.h b/Telegram/SourceFiles/history/view/history_view_send_action.h index 2ad370eb9..4eb559809 100644 --- a/Telegram/SourceFiles/history/view/history_view_send_action.h +++ b/Telegram/SourceFiles/history/view/history_view_send_action.h @@ -35,6 +35,13 @@ public: int outerWidth, style::color color, crl::time now); + void paintSpeaking( + Painter &p, + int x, + int y, + int outerWidth, + style::color color, + crl::time now); bool updateNeedsAnimating( crl::time now, @@ -48,10 +55,12 @@ private: const not_null _history; const base::weak_ptr _weak; base::flat_map, crl::time> _typing; + base::flat_map, crl::time> _speaking; base::flat_map, Api::SendProgress> _sendActions; QString _sendActionString; Ui::Text::String _sendActionText; Ui::SendActionAnimation _sendActionAnimation; + Ui::SendActionAnimation _speakingAnimation; }; diff --git a/Telegram/SourceFiles/ui/effects/send_action_animations.cpp b/Telegram/SourceFiles/ui/effects/send_action_animations.cpp index 46d9d0ffe..487a3ea59 100644 --- a/Telegram/SourceFiles/ui/effects/send_action_animations.cpp +++ b/Telegram/SourceFiles/ui/effects/send_action_animations.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_send_progress.h" #include "ui/effects/animation_value.h" #include "styles/style_widgets.h" +#include "styles/style_dialogs.h" namespace Ui { namespace { @@ -17,6 +18,58 @@ namespace { constexpr int kTypingDotsCount = 3; constexpr int kRecordArcsCount = 4; 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(*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; NeverFreedPointer Implementations; @@ -38,17 +91,17 @@ public: return st::historySendActionTypingPosition.x() + kTypingDotsCount * st::historySendActionTypingDelta; } -private: - void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override; + void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override; }; 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); p.setPen(Qt::NoPen); p.setBrush(color); + auto frameMs = frameTime(now); auto position = QPointF(x + 0.5, y - 0.5) + st::historySendActionTypingPosition; for (auto i = 0; i != kTypingDotsCount; ++i) { auto r = st::historySendActionTypingSmallNumerator / st::historySendActionTypingDenominator; @@ -83,15 +136,15 @@ public: return st::historySendActionRecordPosition.x() + (kRecordArcsCount + 1) * st::historySendActionRecordDelta; } -private: - void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override; + void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override; }; 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); + const auto frameMs = frameTime(now); auto pen = color->p; pen.setWidth(st::historySendActionRecordStrokeNumerator / st::historySendActionRecordDenominator); pen.setJoinStyle(Qt::RoundJoin); @@ -127,15 +180,15 @@ public: return st::historySendActionUploadPosition.x() + (kUploadArrowsCount + 1) * st::historySendActionUploadDelta; } -private: - void paintFrame(Painter &p, style::color color, int x, int y, int outerWidth, int frameMs) override; + void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time now) override; }; 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); + const auto frameMs = frameTime(now); auto pen = color->p; pen.setWidth(st::historySendActionUploadStrokeNumerator / st::historySendActionUploadDenominator); pen.setJoinStyle(Qt::RoundJoin); @@ -159,71 +212,237 @@ void UploadAnimation::paintFrame(Painter &p, style::color color, int x, int y, i p.translate(-position); } +class SpeakingAnimation : public SendActionAnimation::Impl { +public: + SpeakingAnimation(); + + static const MetaData kMeta; + static std::unique_ptr create() { + return std::make_unique(); + } + 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() { if (Implementations) { return; } using Type = Api::SendProgressType; Implementations.createIfNull(); - Type recordTypes[] = { + static constexpr auto kRecordTypes = { Type::RecordVideo, Type::RecordVoice, Type::RecordRound, }; - for_const (auto type, recordTypes) { + for (const auto type : kRecordTypes) { Implementations->insert(type, &RecordAnimation::kMeta); } - Type uploadTypes[] = { + static constexpr auto kUploadTypes = { Type::UploadFile, Type::UploadPhoto, Type::UploadVideo, Type::UploadVoice, Type::UploadRound, }; - for_const (auto type, uploadTypes) { + for (const auto type : kUploadTypes) { Implementations->insert(type, &UploadAnimation::kMeta); } + Implementations->insert(Type::Speaking, &SpeakingAnimation::kMeta); } } // namespace +SendActionAnimation::SendActionAnimation() = default; + +SendActionAnimation::~SendActionAnimation() = default; + bool SendActionAnimation::Impl::supports(Type type) const { CreateImplementationsMap(); 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) { if (!_impl || !_impl->supports(type)) { - _impl = createByType(type); + _impl = CreateByType(type); + } else { + _impl->restartedAt(crl::now()); } } -void SendActionAnimation::stop() { - _impl.reset(); +void SendActionAnimation::tryToFinish() { + if (!_impl) { + return; + } else if (_impl->finishNow()) { + _impl.reset(); + } } -std::unique_ptr 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 { CreateImplementationsMap(); return Implementations->value(type, &TypingAnimation::kMeta)->creator(); } -SendActionAnimation::~SendActionAnimation() = default; - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/send_action_animations.h b/Telegram/SourceFiles/ui/effects/send_action_animations.h index 502f59415..b24581d6f 100644 --- a/Telegram/SourceFiles/ui/effects/send_action_animations.h +++ b/Telegram/SourceFiles/ui/effects/send_action_animations.h @@ -16,60 +16,25 @@ namespace Ui { class SendActionAnimation { public: using Type = Api::SendProgressType; + class Impl; + + SendActionAnimation(); + ~SendActionAnimation(); void start(Type type); - void stop(); + void tryToFinish(); - int width() const { - return _impl ? _impl->width() : 0; - } - 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); - } - } + int width() const; + void paint(Painter &p, style::color color, int x, int y, int outerWidth, crl::time ms) const; explicit operator bool() const { return _impl != nullptr; } - class Impl { - public: - using Type = Api::SendProgressType; - - Impl(int period) : _period(period), _started(crl::now()) { - } - - struct MetaData { - int index; - std::unique_ptr (*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(); + static void PaintSpeakingIdle(Painter &p, style::color color, int x, int y, int outerWidth); private: - std::unique_ptr createByType(Type type); + [[nodiscard]] static std::unique_ptr CreateByType(Type type); std::unique_ptr _impl; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 7fd90cb38..87ee83bc7 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 7fd90cb38b6b1069af43039f93c991fffaf8ddea +Subproject commit 87ee83bc7336b4c814e2f0dc45261ff8e280cca0