diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index a146a991..72570d4a 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -364,7 +364,9 @@ Item {
onClicked: Rooms.resetCurrentRoom()
}
- ParticleSystem { id: confettiParticleSystem
+ ParticleSystem {
+ id: confettiParticleSystem
+
Component.onCompleted: pause();
paused: !shouldEffectsRun
}
@@ -420,6 +422,42 @@ Item {
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 {
anchors.fill: parent
roomid: room ? room.roomId : ""
@@ -428,7 +466,7 @@ Item {
Timer {
id: effectsTimer
onTriggered: shouldEffectsRun = false;
- interval: confettiEmitter.lifeSpan
+ interval: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan)
repeat: false
running: false
}
@@ -471,7 +509,25 @@ Item {
if (!Settings.fancyEffects)
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
diff --git a/src/CommandCompleter.cpp b/src/CommandCompleter.cpp
index 2ec427d6..a0fb101d 100644
--- a/src/CommandCompleter.cpp
+++ b/src/CommandCompleter.cpp
@@ -87,6 +87,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return QStringLiteral("/confetti ");
case RainbowConfetti:
return QStringLiteral("/rainbowconfetti ");
+ case Rainfall:
+ return QStringLiteral("/rainfall ");
+ case RainbowRain:
+ return QStringLiteral("/rainbowrain ");
case Goto:
return QStringLiteral("/goto ");
case ConvertToDm:
@@ -156,6 +160,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return tr("/confetti [message]");
case RainbowConfetti:
return tr("/rainbowconfetti [message]");
+ case Rainfall:
+ return tr("/rainfall [message]");
+ case RainbowRain:
+ return tr("/rainbowrain [message]");
case Goto:
return tr("/goto <message reference>");
case ConvertToDm:
@@ -225,6 +233,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
return tr("Send a message with confetti.");
case RainbowConfetti:
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:
return tr("Go to a specific message using an event id, index or matrix: link");
case ConvertToDm:
diff --git a/src/CommandCompleter.h b/src/CommandCompleter.h
index fcbbe3e5..be5250b8 100644
--- a/src/CommandCompleter.h
+++ b/src/CommandCompleter.h
@@ -46,6 +46,8 @@ public:
RainbowNotice,
Confetti,
RainbowConfetti,
+ Rainfall,
+ RainbowRain,
Goto,
ConvertToDm,
ConvertToRoom,
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index b27128e0..cb7c3919 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -281,6 +281,8 @@ InputBar::updateTextContentProperties(const QString &t)
QStringLiteral("rainbownotice"),
QStringLiteral("confetti"),
QStringLiteral("rainbowconfetti"),
+ QStringLiteral("rain"),
+ QStringLiteral("rainbowrain"),
QStringLiteral("goto"),
QStringLiteral("converttodm"),
QStringLiteral("converttoroom")};
@@ -624,6 +626,30 @@ InputBar::confetti(const QString &body, bool rainbowify)
}
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
InputBar::image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
@@ -890,6 +916,10 @@ InputBar::command(const QString &command, QString args)
confetti(args, false);
} else if (command == QLatin1String("rainbowconfetti")) {
confetti(args, true);
+ } else if (command == QLatin1String("rainfall")) {
+ rainfall(args, false);
+ } else if (command == QLatin1String("rainbowrain")) {
+ rainfall(args, true);
} else if (command == QLatin1String("goto")) {
// Goto has three different modes:
// 1 - Going directly to a given event ID
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index acafd964..a34427ba 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -242,6 +242,7 @@ private:
void emote(const QString &body, bool rainbowify);
void notice(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);
void image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index ae6ecc9a..ba10c3c6 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1081,19 +1081,29 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Text>>(e)) {
if (auto msg = QString::fromStdString(
std::get<RoomEvent<mtx::events::msg::Text>>(e).content.body);
- msg.contains("🎉") || msg.contains("🎊"))
+ msg.contains("🎉") || msg.contains("🎊")) {
needsSpecialEffects_ = true;
+ specialEffects_.setFlag(Confetti);
+ }
} else if (std::holds_alternative<RoomEvent<mtx::events::msg::Unknown>>(e)) {
if (auto msg = QString::fromStdString(
std::get<RoomEvent<mtx::events::msg::Unknown>>(e).content.body);
- msg.contains("🎉") || msg.contains("🎊"))
+ msg.contains("🎉") || msg.contains("🎊")) {
+ needsSpecialEffects_ = true;
+ specialEffects_.setFlag(Confetti);
+ } else if (std::get<RoomEvent<mtx::events::msg::Unknown>>(e).content.msgtype ==
+ "io.element.effect.rainfall") {
needsSpecialEffects_ = true;
- } else if (std::holds_alternative<RoomEvent<mtx::events::msg::Confetti>>(e))
+ specialEffects_.setFlag(Rainfall);
+ }
+ } else if (std::holds_alternative<RoomEvent<mtx::events::msg::Confetti>>(e)) {
needsSpecialEffects_ = true;
+ specialEffects_.setFlag(Confetti);
+ }
}
if (needsSpecialEffects_)
- emit confetti();
+ triggerSpecialEffects();
if (avatarChanged)
emit roomAvatarUrlChanged();
@@ -2056,7 +2066,14 @@ TimelineModel::triggerSpecialEffects()
{
if (needsSpecialEffects_) {
// 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;
}
}
@@ -2066,6 +2083,10 @@ TimelineModel::markSpecialEffectsDone()
{
needsSpecialEffects_ = false;
emit confettiDone();
+ emit rainfallDone();
+
+ specialEffects_.setFlag(Confetti, false);
+ specialEffects_.setFlag(Rainfall, false);
}
QString
@@ -2928,7 +2949,8 @@ TimelineModel::setEdit(const QString &newEdit)
if (msgType == mtx::events::MessageType::Text ||
msgType == mtx::events::MessageType::Notice ||
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 editText = relInfo.quoted_body;
@@ -2950,8 +2972,14 @@ TimelineModel::setEdit(const QString &newEdit)
if (msgType == mtx::events::MessageType::Emote)
input()->setText("/me " + editText);
else if (msgType == mtx::events::MessageType::Confetti)
- input()->setText("/confetti" + editText);
- else
+ input()->setText("/confetti " + editText);
+ 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);
} else {
input()->setText(QLatin1String(""));
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index d71012c1..ce3dc9e4 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -267,6 +267,13 @@ public:
};
Q_ENUM(Roles);
+ enum SpecialEffect
+ {
+ Confetti,
+ Rainfall,
+ };
+ Q_DECLARE_FLAGS(SpecialEffects, SpecialEffect)
+
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -451,6 +458,8 @@ signals:
void scrollToIndex(int index);
void confetti();
void confettiDone();
+ void rainfall();
+ void rainfallDone();
void lastMessageChanged();
void notificationsChanged();
@@ -522,8 +531,8 @@ private:
std::string last_event_id;
std::string fullyReadEventId_;
- // TODO (Loren): This should hopefully handle more than just confetti in the future
bool needsSpecialEffects_ = false;
+ QFlags<SpecialEffect> specialEffects_;
std::unique_ptr<RoomSummary, DeleteLaterDeleter> parentSummary = nullptr;
bool parentChecked = false;
@@ -531,6 +540,8 @@ private:
friend void EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room);
};
+Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineModel::SpecialEffects)
+
template<class T>
void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
|