Implement basic effect animation.

This commit is contained in:
John Preston 2024-05-07 14:58:03 +04:00
parent f762634036
commit a19e71324b
19 changed files with 258 additions and 19 deletions

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_peer_values.h"
#include "data/data_saved_sublist.h"
#include "data/stickers/data_custom_emoji.h"
@ -594,6 +595,9 @@ void Reactions::preloadImageFor(const ReactionId &id) {
} else {
generateImage(set, i->title);
}
if (set.effect) {
preloadEffect(*i);
}
} else if (set.effect && !_waitingForEffects) {
_waitingForEffects = true;
refreshEffects();
@ -603,6 +607,15 @@ void Reactions::preloadImageFor(const ReactionId &id) {
}
}
void Reactions::preloadEffect(const Reaction &effect) {
if (effect.aroundAnimation) {
effect.aroundAnimation->createMediaView()->checkStickerLarge();
} else {
const auto premium = effect.selectAnimation;
premium->loadVideoThumbnail(premium->stickerSetOrigin());
}
}
void Reactions::preloadAnimationsFor(const ReactionId &id) {
const auto custom = id.custom();
const auto document = custom ? _owner->document(custom).get() : nullptr;

View file

@ -212,6 +212,7 @@ private:
[[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableEffect &entry);
void preloadEffect(const Reaction &effect);
void preloadImageFor(const ReactionId &id);
[[nodiscard]] QImage resolveImageFor(const ReactionId &id);
void loadImage(

View file

@ -321,6 +321,8 @@ enum class MessageFlag : uint64 {
ReactionsAreTags = (1ULL << 43),
ShortcutMessage = (1ULL << 44),
EffectWatchedLocal = (1ULL << 45),
};
inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>;

View file

@ -674,6 +674,11 @@ void InnerWidget::elementStartPremium(
void InnerWidget::elementCancelPremium(not_null<const Element*> view) {
}
void InnerWidget::elementStartEffect(
not_null<const Element*> view,
Element *replacing) {
}
QString InnerWidget::elementAuthorRank(not_null<const Element*> view) {
return {};
}

View file

@ -136,6 +136,9 @@ public:
HistoryView::Element *replacing) override;
void elementCancelPremium(
not_null<const HistoryView::Element*> view) override;
void elementStartEffect(
not_null<const HistoryView::Element*> view,
HistoryView::Element *replacing) override;
QString elementAuthorRank(
not_null<const HistoryView::Element*> view) override;

View file

@ -304,12 +304,18 @@ public:
_widget->elementStartPremium(view, replacing);
}
}
void elementCancelPremium(not_null<const Element*> view) override {
if (_widget) {
_widget->elementCancelPremium(view);
}
}
void elementStartEffect(
not_null<const Element*> view,
Element *replacing) override {
if (_widget) {
_widget->elementStartEffect(view, replacing);
}
}
QString elementAuthorRank(not_null<const Element*> view) override {
return {};
@ -950,6 +956,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
_translateTracker->startBunch();
auto readTill = (HistoryItem*)nullptr;
auto readContents = base::flat_set<not_null<HistoryItem*>>();
auto startEffects = base::flat_set<not_null<const Element*>>();
const auto markingAsViewed = _widget->markingContentsRead();
const auto guard = gsl::finally([&] {
if (_pinnedItem) {
@ -958,6 +965,11 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
_translateTracker->finishBunch();
if (readTill && _widget->markingMessagesRead()) {
session().data().histories().readInboxTill(readTill);
if (!startEffects.empty()) {
for (const auto &view : startEffects) {
_emojiInteractions->playEffectOnRead(view);
}
}
}
if (markingAsViewed && !readContents.empty()) {
session().api().markContentsRead(readContents);
@ -991,6 +1003,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
session().sponsoredMessages().view(item->fullId());
} else if (isUnread) {
readTill = item;
if (item->hasUnwatchedEffect()) {
startEffects.emplace(view);
}
}
if (markingAsViewed && item->hasViews()) {
session().api().views().scheduleIncrement(item);
@ -3568,6 +3583,12 @@ void HistoryInner::elementCancelPremium(not_null<const Element*> view) {
_emojiInteractions->cancelPremiumEffect(view);
}
void HistoryInner::elementStartEffect(
not_null<const Element*> view,
Element *replacing) {
_emojiInteractions->playEffect(view);
}
auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {};

View file

@ -170,6 +170,9 @@ public:
not_null<const Element*> view,
Element *replacing);
void elementCancelPremium(not_null<const Element*> view);
void elementStartEffect(
not_null<const Element*> view,
Element *replacing);
void updateBotInfo(bool recount = true);

View file

@ -687,6 +687,9 @@ HistoryItem::HistoryItem(
if (isHistoryEntry() && IsClientMsgId(id)) {
_history->registerClientSideMessage(this);
}
if (_effectId) {
_history->owner().reactions().preloadEffectImageFor(_effectId);
}
}
HistoryItem::HistoryItem(
@ -1283,6 +1286,21 @@ bool HistoryItem::hasUnreadReaction() const {
return (_flags & MessageFlag::HasUnreadReaction);
}
bool HistoryItem::hasUnwatchedEffect() const {
return !out()
&& effectId()
&& !(_flags & MessageFlag::EffectWatchedLocal)
&& unread(history());
}
bool HistoryItem::markEffectWatched() {
if (!hasUnwatchedEffect()) {
return false;
}
_flags |= MessageFlag::EffectWatchedLocal;
return true;
}
bool HistoryItem::mentionsMe() const {
if (Has<HistoryServicePinned>()
&& !Core::App().settings().notifyAboutPinned()) {

View file

@ -241,6 +241,8 @@ public:
[[nodiscard]] bool mentionsMe() const;
[[nodiscard]] bool isUnreadMention() const;
[[nodiscard]] bool hasUnreadReaction() const;
[[nodiscard]] bool hasUnwatchedEffect() const;
bool markEffectWatched();
[[nodiscard]] bool isUnreadMedia() const;
[[nodiscard]] bool isIncomingUnreadMedia() const;
[[nodiscard]] bool hasUnreadMediaFlag() const;

View file

@ -1389,7 +1389,10 @@ int HistoryWidget::itemTopForHighlight(
const auto itemTop = _list->itemTop(view);
Assert(itemTop >= 0);
const auto reactionCenter = view->data()->hasUnreadReaction()
const auto item = view->data();
const auto unwatchedEffect = item->hasUnwatchedEffect();
const auto showReactions = item->hasUnreadReaction() || unwatchedEffect;
const auto reactionCenter = showReactions
? view->reactionButtonParameters({}, {}).center.y()
: -1;
@ -2376,8 +2379,6 @@ void HistoryWidget::showHistory(
}
}
session().data().reactions().refreshEffects();
_scroll->hide();
_list = _scroll->setOwnedWidget(
object_ptr<HistoryInner>(this, _scroll, controller(), _history));

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "chat_helpers/emoji_interactions.h"
#include "core/click_handler_types.h"
#include "main/main_session.h"
#include "lottie/lottie_icon.h"
@ -104,10 +105,11 @@ bool BottomInfo::isWide() const {
}
TextState BottomInfo::textState(
not_null<const HistoryItem*> item,
not_null<const Message*> view,
QPoint position) const {
const auto item = view->data();
auto result = TextState(item);
if (const auto link = replayEffectLink(item, position)) {
if (const auto link = replayEffectLink(view, position)) {
result.link = link;
return result;
}
@ -158,7 +160,7 @@ TextState BottomInfo::textState(
}
ClickHandlerPtr BottomInfo::replayEffectLink(
not_null<const HistoryItem*> item,
not_null<const Message*> view,
QPoint position) const {
if (!_effect) {
return nullptr;
@ -189,7 +191,7 @@ ClickHandlerPtr BottomInfo::replayEffectLink(
st::msgDateFont->height);
if (image.contains(position)) {
if (!_replayLink) {
_replayLink = replayEffectLink(item);
_replayLink = replayEffectLink(view);
}
return _replayLink;
}
@ -200,15 +202,17 @@ ClickHandlerPtr BottomInfo::replayEffectLink(
}
ClickHandlerPtr BottomInfo::replayEffectLink(
not_null<const HistoryItem*> item) const {
not_null<const Message*> view) const {
const auto item = view->data();
const auto itemId = item->fullId();
const auto sessionId = item->history()->session().uniqueId();
return std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
const auto weak = base::make_weak(view);
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
controller->showToast("playing nice effect..");
AssertIsDebug();
if (const auto strong = weak.get()) {
strong->delegate()->elementStartEffect(strong, nullptr);
}
}
});
}

View file

@ -62,7 +62,7 @@ public:
[[nodiscard]] int firstLineWidth() const;
[[nodiscard]] bool isWide() const;
[[nodiscard]] TextState textState(
not_null<const HistoryItem*> item,
not_null<const Message*> view,
QPoint position) const;
[[nodiscard]] bool isSignedAuthorElided() const;
@ -106,10 +106,10 @@ private:
[[nodiscard]] Effect prepareEffectWithId(EffectId id);
[[nodiscard]] ClickHandlerPtr replayEffectLink(
not_null<const HistoryItem*> item,
not_null<const Message*> view,
QPoint position) const;
[[nodiscard]] ClickHandlerPtr replayEffectLink(
not_null<const HistoryItem*> item) const;
not_null<const Message*> view) const;
const not_null<::Data::Reactions*> _reactionsOwner;
Data _data;

View file

@ -192,6 +192,11 @@ void DefaultElementDelegate::elementCancelPremium(
not_null<const Element*> view) {
}
void DefaultElementDelegate::elementStartEffect(
not_null<const Element*> view,
Element *replacing) {
}
QString DefaultElementDelegate::elementAuthorRank(
not_null<const Element*> view) {
return {};

View file

@ -113,6 +113,9 @@ public:
not_null<const Element*> view,
Element *replacing) = 0;
virtual void elementCancelPremium(not_null<const Element*> view) = 0;
virtual void elementStartEffect(
not_null<const Element*> view,
Element *replacing) = 0;
virtual QString elementAuthorRank(not_null<const Element*> view) = 0;
virtual ~ElementDelegate() {
@ -163,6 +166,9 @@ public:
not_null<const Element*> view,
Element *replacing) override;
void elementCancelPremium(not_null<const Element*> view) override;
void elementStartEffect(
not_null<const Element*> view,
Element *replacing) override;
QString elementAuthorRank(not_null<const Element*> view) override;
};

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_sticker.h"
#include "history/history.h"
#include "history/history_item.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "chat_helpers/emoji_interactions.h"
#include "chat_helpers/stickers_lottie.h"
@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_message_reactions.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_single_player.h"
#include "base/random.h"
@ -45,8 +47,8 @@ constexpr auto kDropDelayedAfterDelay = crl::time(2000);
EmojiInteractions::EmojiInteractions(
not_null<Main::Session*> session,
Fn<int(not_null<const Element*>)> itemTop)
: _session(session)
, _itemTop(std::move(itemTop)) {
: _session(session)
, _itemTop(std::move(itemTop)) {
_session->data().viewRemoved(
) | rpl::filter([=] {
return !_plays.empty() || !_delayed.empty();
@ -56,6 +58,11 @@ EmojiInteractions::EmojiInteractions(
ranges::remove(_delayed, view, &Delayed::view),
end(_delayed));
}, _lifetime);
_session->data().reactions().effectsUpdates(
) | rpl::start_with_next([=] {
checkPendingEffects();
}, _lifetime);
}
EmojiInteractions::~EmojiInteractions() = default;
@ -143,6 +150,121 @@ void EmojiInteractions::play(
false);
}
void EmojiInteractions::playEffectOnRead(not_null<const Element*> view) {
if (view->data()->markEffectWatched()) {
playEffect(view);
}
}
void EmojiInteractions::playEffect(not_null<const Element*> view) {
if (const auto resolved = resolveEffect(view)) {
playEffect(view, resolved);
} else if (view->data()->effectId()) {
if (resolved.document && !_downloadLifetime) {
_downloadLifetime = _session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
checkPendingEffects();
});
}
addPendingEffect(view);
}
}
EmojiInteractions::ResolvedEffect EmojiInteractions::resolveEffect(
not_null<const Element*> view) {
const auto item = view->data();
const auto effectId = item->effectId();
if (!effectId) {
return {};
}
using Type = Data::Reactions::Type;
const auto &effects = _session->data().reactions().list(Type::Effects);
const auto i = ranges::find(
effects,
Data::ReactionId{ effectId },
&Data::Reaction::id);
if (i == end(effects)) {
return {};
}
auto document = (DocumentData*)nullptr;
auto content = QByteArray();
auto filepath = QString();
if ((document = i->aroundAnimation)) {
content = document->createMediaView()->bytes();
filepath = document->filepath();
} else {
document = i->selectAnimation;
content = document->createMediaView()->videoThumbnailContent();
}
return {
.emoticon = i->title,
.document = document,
.content = content,
.filepath = filepath,
};
}
void EmojiInteractions::playEffect(
not_null<const Element*> view,
const ResolvedEffect &resolved) {
play(
resolved.emoticon,
view,
resolved.document,
resolved.content,
resolved.filepath,
false,
false);
}
void EmojiInteractions::addPendingEffect(not_null<const Element*> view) {
auto found = false;
const auto predicate = [&](base::weak_ptr<const Element> weak) {
const auto strong = weak.get();
if (strong == view) {
found = true;
}
return !strong;
};
_pendingEffects.erase(
ranges::remove_if(_pendingEffects, predicate),
end(_pendingEffects));
if (!found) {
_pendingEffects.push_back(view);
}
}
void EmojiInteractions::checkPendingEffects() {
auto waitingDownload = false;
const auto predicate = [&](base::weak_ptr<const Element> weak) {
const auto strong = weak.get();
if (!strong) {
return true;
}
const auto resolved = resolveEffect(strong);
if (resolved) {
playEffect(strong, resolved);
return true;
} else if (!strong->data()->effectId()) {
return true;
} else if (resolved.document) {
waitingDownload = true;
}
return false;
};
_pendingEffects.erase(
ranges::remove_if(_pendingEffects, predicate),
end(_pendingEffects));
if (!waitingDownload) {
_downloadLifetime.destroy();
} else if (!_downloadLifetime) {
_downloadLifetime = _session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
checkPendingEffects();
});
}
}
void EmojiInteractions::play(
QString emoticon,
not_null<const Element*> view,

View file

@ -43,6 +43,9 @@ public:
void cancelPremiumEffect(not_null<const Element*> view);
void visibleAreaUpdated(int visibleTop, int visibleBottom);
void playEffectOnRead(not_null<const Element*> view);
void playEffect(not_null<const Element*> view);
void paint(QPainter &p);
[[nodiscard]] rpl::producer<QRect> updateRequests() const;
[[nodiscard]] rpl::producer<QString> playStarted() const;
@ -68,6 +71,16 @@ private:
crl::time shouldHaveStartedAt = 0;
bool incoming = false;
};
struct ResolvedEffect {
QString emoticon;
DocumentData *document = nullptr;
QByteArray content;
QString filepath;
explicit operator bool() const {
return document && (!content.isEmpty() || !filepath.isEmpty());
}
};
[[nodiscard]] QRect computeRect(const Play &play) const;
@ -85,6 +98,14 @@ private:
bool incoming,
bool premium);
void checkDelayed();
void addPendingEffect(not_null<const Element*> view);
[[nodiscard]] ResolvedEffect resolveEffect(
not_null<const Element*> view);
void playEffect(
not_null<const Element*> view,
const ResolvedEffect &resolved);
void checkPendingEffects();
const not_null<Main::Session*> _session;
const Fn<int(not_null<const Element*>)> _itemTop;
@ -97,6 +118,9 @@ private:
rpl::event_stream<QRect> _updateRequests;
rpl::event_stream<QString> _playStarted;
std::vector<base::weak_ptr<const Element>> _pendingEffects;
rpl::lifetime _downloadLifetime;
rpl::lifetime _lifetime;
};

View file

@ -1857,6 +1857,12 @@ void ListWidget::elementCancelPremium(not_null<const Element*> view) {
_emojiInteractions->cancelPremiumEffect(view);
}
void ListWidget::elementStartEffect(
not_null<const Element*> view,
Element *replacing) {
_emojiInteractions->playEffect(view);
}
QString ListWidget::elementAuthorRank(not_null<const Element*> view) {
return _delegate->listElementAuthorRank(view);
}

View file

@ -419,6 +419,9 @@ public:
not_null<const Element*> view,
Element *replacing) override;
void elementCancelPremium(not_null<const Element*> view) override;
void elementStartEffect(
not_null<const Element*> view,
Element *replacing) override;
QString elementAuthorRank(not_null<const Element*> view) override;
void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w);

View file

@ -3038,7 +3038,7 @@ TextState Message::bottomInfoTextState(
const auto infoLeft = infoRight - size.width();
const auto infoTop = infoBottom - size.height();
return _bottomInfo.textState(
data(),
this,
point - QPoint{ infoLeft, infoTop });
}