Add rainfall effect

This is a proof-of-concept example of inplementing a msgtype not found
in the spec.
This commit is contained in:
Loren Burkholder 2023-03-07 13:11:00 -05:00
parent 0096226aeb
commit 296385e6fe
7 changed files with 152 additions and 12 deletions

View file

@ -364,7 +364,9 @@ Item {
onClicked: Rooms.resetCurrentRoom() onClicked: Rooms.resetCurrentRoom()
} }
ParticleSystem { id: confettiParticleSystem ParticleSystem {
id: confettiParticleSystem
Component.onCompleted: pause(); Component.onCompleted: pause();
paused: !shouldEffectsRun paused: !shouldEffectsRun
} }
@ -420,6 +422,42 @@ Item {
angle: 90 angle: 90
} }
ParticleSystem {
id: rainfallParticleSystem
Component.onCompleted: pause();
paused: !shouldEffectsRun
}
Emitter {
id: rainfallEmitter
width: parent.width
enabled: false
anchors.horizontalCenter: parent.horizontalCenter
y: -60
emitRate: parent.width / 50
lifeSpan: 10000
system: rainfallParticleSystem
velocity: PointDirection {
x: 0
y: 300
xVariation: 0
yVariation: 75
}
ItemParticle {
system: rainfallParticleSystem
fade: false
delegate: Rectangle {
width: 2
height: 30 + 30 * Math.random()
radius: 2
color: "#0099ff"
}
}
}
NhekoDropArea { NhekoDropArea {
anchors.fill: parent anchors.fill: parent
roomid: room ? room.roomId : "" roomid: room ? room.roomId : ""
@ -428,7 +466,7 @@ Item {
Timer { Timer {
id: effectsTimer id: effectsTimer
onTriggered: shouldEffectsRun = false; onTriggered: shouldEffectsRun = false;
interval: confettiEmitter.lifeSpan interval: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan)
repeat: false repeat: false
running: false running: false
} }
@ -471,7 +509,25 @@ Item {
if (!Settings.fancyEffects) if (!Settings.fancyEffects)
return return
effectsTimer.start(); effectsTimer.restart();
}
function onRainfall()
{
if (!Settings.fancyEffects)
return
shouldEffectsRun = true;
rainfallEmitter.pulse(parent.height * 7.5)
room.markSpecialEffectsDone()
}
function onRainfallDone()
{
if (!Settings.fancyEffects)
return
effectsTimer.restart();
} }
target: room target: room

View file

@ -87,6 +87,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return QStringLiteral("/confetti "); return QStringLiteral("/confetti ");
case RainbowConfetti: case RainbowConfetti:
return QStringLiteral("/rainbowconfetti "); return QStringLiteral("/rainbowconfetti ");
case Rainfall:
return QStringLiteral("/rainfall ");
case RainbowRain:
return QStringLiteral("/rainbowrain ");
case Goto: case Goto:
return QStringLiteral("/goto "); return QStringLiteral("/goto ");
case ConvertToDm: case ConvertToDm:
@ -156,6 +160,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return tr("/confetti [message]"); return tr("/confetti [message]");
case RainbowConfetti: case RainbowConfetti:
return tr("/rainbowconfetti [message]"); return tr("/rainbowconfetti [message]");
case Rainfall:
return tr("/rainfall [message]");
case RainbowRain:
return tr("/rainbowrain [message]");
case Goto: case Goto:
return tr("/goto <message reference>"); return tr("/goto <message reference>");
case ConvertToDm: case ConvertToDm:
@ -225,6 +233,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return tr("Send a message with confetti."); return tr("Send a message with confetti.");
case RainbowConfetti: case RainbowConfetti:
return tr("Send a message in rainbow colors with confetti."); return tr("Send a message in rainbow colors with confetti.");
case Rainfall:
return tr("Send a message with rain.");
case RainbowRain:
return tr("Send a message in rainbow colors with rain.");
case Goto: case Goto:
return tr("Go to a specific message using an event id, index or matrix: link"); return tr("Go to a specific message using an event id, index or matrix: link");
case ConvertToDm: case ConvertToDm:

View file

@ -46,6 +46,8 @@ public:
RainbowNotice, RainbowNotice,
Confetti, Confetti,
RainbowConfetti, RainbowConfetti,
Rainfall,
RainbowRain,
Goto, Goto,
ConvertToDm, ConvertToDm,
ConvertToRoom, ConvertToRoom,

View file

@ -281,6 +281,8 @@ InputBar::updateTextContentProperties(const QString &t)
QStringLiteral("rainbownotice"), QStringLiteral("rainbownotice"),
QStringLiteral("confetti"), QStringLiteral("confetti"),
QStringLiteral("rainbowconfetti"), QStringLiteral("rainbowconfetti"),
QStringLiteral("rain"),
QStringLiteral("rainbowrain"),
QStringLiteral("goto"), QStringLiteral("goto"),
QStringLiteral("converttodm"), QStringLiteral("converttodm"),
QStringLiteral("converttoroom")}; QStringLiteral("converttoroom")};
@ -623,6 +625,30 @@ InputBar::confetti(const QString &body, bool rainbowify)
room->sendMessageEvent(confetti, mtx::events::EventType::RoomMessage); room->sendMessageEvent(confetti, mtx::events::EventType::RoomMessage);
} }
void
InputBar::rainfall(const QString &body, bool rainbowify)
{
auto html = utils::markdownToHtml(body, rainbowify);
mtx::events::msg::Unknown rain;
rain.msgtype = "io.element.effect.rainfall";
rain.body = body.trimmed().toStdString();
if (html != body.trimmed().toHtmlEscaped() &&
ChatPage::instance()->userSettings()->markdown()) {
nlohmann::json j;
j["formatted_body"] = html.toStdString();
j["format"] = "org.matrix.custom.html";
rain.content = j.dump();
// Remove markdown links by completer
rain.body = replaceMatrixToMarkdownLink(body.trimmed()).toStdString();
}
rain.relations = generateRelations();
room->sendMessageEvent(rain, mtx::events::EventType::RoomMessage);
}
void void
InputBar::image(const QString &filename, InputBar::image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,
@ -890,6 +916,10 @@ InputBar::command(const QString &command, QString args)
confetti(args, false); confetti(args, false);
} else if (command == QLatin1String("rainbowconfetti")) { } else if (command == QLatin1String("rainbowconfetti")) {
confetti(args, true); confetti(args, true);
} else if (command == QLatin1String("rainfall")) {
rainfall(args, false);
} else if (command == QLatin1String("rainbowrain")) {
rainfall(args, true);
} else if (command == QLatin1String("goto")) { } else if (command == QLatin1String("goto")) {
// Goto has three different modes: // Goto has three different modes:
// 1 - Going directly to a given event ID // 1 - Going directly to a given event ID

View file

@ -242,6 +242,7 @@ private:
void emote(const QString &body, bool rainbowify); void emote(const QString &body, bool rainbowify);
void notice(const QString &body, bool rainbowify); void notice(const QString &body, bool rainbowify);
void confetti(const QString &body, bool rainbowify); void confetti(const QString &body, bool rainbowify);
void rainfall(const QString &body, bool rainbowify);
bool command(const QString &name, QString args); bool command(const QString &name, QString args);
void image(const QString &filename, void image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,

View file

@ -1081,19 +1081,29 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Text>>(e)) { } else if (std::holds_alternative<RoomEvent<mtx::events::msg::Text>>(e)) {
if (auto msg = QString::fromStdString( if (auto msg = QString::fromStdString(
std::get<RoomEvent<mtx::events::msg::Text>>(e).content.body); std::get<RoomEvent<mtx::events::msg::Text>>(e).content.body);
msg.contains("🎉") || msg.contains("🎊")) msg.contains("🎉") || msg.contains("🎊")) {
needsSpecialEffects_ = true; needsSpecialEffects_ = true;
specialEffects_.setFlag(Confetti);
}
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Unknown>>(e)) { } else if (std::holds_alternative<RoomEvent<mtx::events::msg::Unknown>>(e)) {
if (auto msg = QString::fromStdString( if (auto msg = QString::fromStdString(
std::get<RoomEvent<mtx::events::msg::Unknown>>(e).content.body); std::get<RoomEvent<mtx::events::msg::Unknown>>(e).content.body);
msg.contains("🎉") || msg.contains("🎊")) msg.contains("🎉") || msg.contains("🎊")) {
needsSpecialEffects_ = true; needsSpecialEffects_ = true;
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Confetti>>(e)) specialEffects_.setFlag(Confetti);
} else if (std::get<RoomEvent<mtx::events::msg::Unknown>>(e).content.msgtype ==
"io.element.effect.rainfall") {
needsSpecialEffects_ = true;
specialEffects_.setFlag(Rainfall);
}
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Confetti>>(e)) {
needsSpecialEffects_ = true; needsSpecialEffects_ = true;
specialEffects_.setFlag(Confetti);
}
} }
if (needsSpecialEffects_) if (needsSpecialEffects_)
emit confetti(); triggerSpecialEffects();
if (avatarChanged) if (avatarChanged)
emit roomAvatarUrlChanged(); emit roomAvatarUrlChanged();
@ -2056,7 +2066,14 @@ TimelineModel::triggerSpecialEffects()
{ {
if (needsSpecialEffects_) { if (needsSpecialEffects_) {
// Note (Loren): Without the timer, this apparently emits before QML is ready // Note (Loren): Without the timer, this apparently emits before QML is ready
QTimer::singleShot(1, this, [this] { emit confetti(); }); if (specialEffects_.testFlag(Confetti)) {
QTimer::singleShot(1, this, [this] { emit confetti(); });
specialEffects_.setFlag(Confetti, false);
}
if (specialEffects_.testFlag(Rainfall)) {
QTimer::singleShot(1, this, [this] { emit rainfall(); });
specialEffects_.setFlag(Rainfall, false);
}
needsSpecialEffects_ = false; needsSpecialEffects_ = false;
} }
} }
@ -2066,6 +2083,10 @@ TimelineModel::markSpecialEffectsDone()
{ {
needsSpecialEffects_ = false; needsSpecialEffects_ = false;
emit confettiDone(); emit confettiDone();
emit rainfallDone();
specialEffects_.setFlag(Confetti, false);
specialEffects_.setFlag(Rainfall, false);
} }
QString QString
@ -2928,7 +2949,8 @@ TimelineModel::setEdit(const QString &newEdit)
if (msgType == mtx::events::MessageType::Text || if (msgType == mtx::events::MessageType::Text ||
msgType == mtx::events::MessageType::Notice || msgType == mtx::events::MessageType::Notice ||
msgType == mtx::events::MessageType::Emote || msgType == mtx::events::MessageType::Emote ||
msgType == mtx::events::MessageType::Confetti) { msgType == mtx::events::MessageType::Confetti ||
msgType == mtx::events::MessageType::Unknown) {
auto relInfo = relatedInfo(newEdit); auto relInfo = relatedInfo(newEdit);
auto editText = relInfo.quoted_body; auto editText = relInfo.quoted_body;
@ -2950,8 +2972,14 @@ TimelineModel::setEdit(const QString &newEdit)
if (msgType == mtx::events::MessageType::Emote) if (msgType == mtx::events::MessageType::Emote)
input()->setText("/me " + editText); input()->setText("/me " + editText);
else if (msgType == mtx::events::MessageType::Confetti) else if (msgType == mtx::events::MessageType::Confetti)
input()->setText("/confetti" + editText); input()->setText("/confetti " + editText);
else else if (msgType == mtx::events::MessageType::Unknown) {
if (auto u = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Unknown>>(&e);
u && u->content.msgtype == "io.element.effect.rainfall")
input()->setText("/rainfall " + editText);
else
input()->setText(editText);
} else
input()->setText(editText); input()->setText(editText);
} else { } else {
input()->setText(QLatin1String("")); input()->setText(QLatin1String(""));

View file

@ -267,6 +267,13 @@ public:
}; };
Q_ENUM(Roles); Q_ENUM(Roles);
enum SpecialEffect
{
Confetti,
Rainfall,
};
Q_DECLARE_FLAGS(SpecialEffects, SpecialEffect)
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@ -451,6 +458,8 @@ signals:
void scrollToIndex(int index); void scrollToIndex(int index);
void confetti(); void confetti();
void confettiDone(); void confettiDone();
void rainfall();
void rainfallDone();
void lastMessageChanged(); void lastMessageChanged();
void notificationsChanged(); void notificationsChanged();
@ -522,8 +531,8 @@ private:
std::string last_event_id; std::string last_event_id;
std::string fullyReadEventId_; std::string fullyReadEventId_;
// TODO (Loren): This should hopefully handle more than just confetti in the future
bool needsSpecialEffects_ = false; bool needsSpecialEffects_ = false;
QFlags<SpecialEffect> specialEffects_;
std::unique_ptr<RoomSummary, DeleteLaterDeleter> parentSummary = nullptr; std::unique_ptr<RoomSummary, DeleteLaterDeleter> parentSummary = nullptr;
bool parentChecked = false; bool parentChecked = false;
@ -531,6 +540,8 @@ private:
friend void EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room); friend void EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room);
}; };
Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineModel::SpecialEffects)
template<class T> template<class T>
void void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType) TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)