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);
}
bool IsPeerAnOnlineUser(not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return OnlineTextActive(user, base::unixtime::now());
}
return false;
bool IsUserOnline(not_null<UserData*> user) {
return OnlineTextActive(user, base::unixtime::now());
}
bool ChannelHasActiveCall(not_null<ChannelData*> channel) {
return (channel->flags() & MTPDchannel::Flag::f_call_active);
}
} // namespace Data

View file

@ -120,6 +120,7 @@ inline auto PeerFullFlagValue(
[[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now);
[[nodiscard]] bool OnlineTextActive(TimeId online, 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

View file

@ -2181,6 +2181,15 @@ void Session::updateSendActionAnimation(
_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 {
return computeUnreadBadge(_chatsList.unreadState());
}

View file

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

View file

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

View file

@ -189,6 +189,11 @@ InnerWidget::InnerWidget(
UpdateRowSection::Default | UpdateRowSection::Filtered);
}, lifetime());
session().data().speakingAnimationUpdated(
) | rpl::start_with_next([=](not_null<History*> 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*> 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) {
const auto user = peer->isSelf() ? nullptr : peer->asUser();
if (!user) {
@ -2973,32 +3010,35 @@ void InnerWidget::userOnlineUpdated(not_null<PeerData*> 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<PeerData*> 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*> history,
bool shown) {
const auto repaint = [=] {
updateDialogRow(
RowDescriptor(
history,
FullMsgId()),
updateRect,
UpdateRowSection::Default | UpdateRowSection::Filtered);
updateDialogRowCornerStatus(history);
};
repaint();
const auto findRow = [&](not_null<History*> history)
-> std::pair<Row*, int> {
-> std::pair<Row*, int> {
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<PeerData*> 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<void()>(crl::guard(this, repaint)) : nullptr);
}
}

View file

@ -224,6 +224,12 @@ private:
int defaultRowTop(not_null<Row*> row) const;
void setupOnlineStatusCheck();
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();
RowDescriptor computeJump(

View file

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

View file

@ -71,27 +71,29 @@ QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
BasicRow::BasicRow() = default;
BasicRow::~BasicRow() = default;
void BasicRow::setOnline(bool online, Fn<void()> updateCallback) const {
if (_online == online) {
void BasicRow::setCornerBadgeShown(
bool shown,
Fn<void()> 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<OnlineUserpic>();
void BasicRow::updateCornerBadgeShown(
not_null<PeerData*> peer,
Fn<void()> 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<OnlineUserpic*> data,
void BasicRow::ensureCornerBadgeUserpic() const {
if (_cornerBadgeUserpic) {
return;
}
_cornerBadgeUserpic = std::make_unique<CornerBadgeUserpic>();
}
void BasicRow::PaintCornerBadgeFrame(
not_null<CornerBadgeUserpic*> data,
not_null<PeerData*> peer,
std::shared_ptr<Data::CloudImageView> &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<PeerData*> 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);
}
}

View file

@ -34,11 +34,14 @@ public:
BasicRow();
~BasicRow();
void setOnline(bool online, Fn<void()> updateCallback = nullptr) const;
void updateCornerBadgeShown(
not_null<PeerData*> peer,
Fn<void()> updateCallback = nullptr) const;
void paintUserpic(
Painter &p,
not_null<PeerData*> 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<OnlineUserpic*> data,
void setCornerBadgeShown(
bool shown,
Fn<void()> updateCallback) const;
void ensureCornerBadgeUserpic() const;
static void PaintCornerBadgeFrame(
not_null<CornerBadgeUserpic*> data,
not_null<PeerData*> peer,
std::shared_ptr<Data::CloudImageView> &view);
mutable std::shared_ptr<Data::CloudImageView> _userpic;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable std::unique_ptr<OnlineUserpic> _onlineUserpic;
mutable bool _online = false;
mutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;
mutable bool _cornerBadgeShown = false;
};

View file

@ -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<UserData*> from) {

View file

@ -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*> _history;
const base::weak_ptr<Main::Session> _weak;
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;
QString _sendActionString;
Ui::Text::String _sendActionText;
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 "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<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*>;
NeverFreedPointer<ImplementationsMap> 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<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() {
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::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();
return Implementations->value(type, &TypingAnimation::kMeta)->creator();
}
SendActionAnimation::~SendActionAnimation() = default;
} // namespace Ui

View file

@ -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<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();
static void PaintSpeakingIdle(Painter &p, style::color color, int x, int y, int outerWidth);
private:
std::unique_ptr<Impl> createByType(Type type);
[[nodiscard]] static std::unique_ptr<Impl> CreateByType(Type type);
std::unique_ptr<Impl> _impl;

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