summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/qml/TimelineView.qml62
-rw-r--r--src/CommandCompleter.cpp12
-rw-r--r--src/CommandCompleter.h2
-rw-r--r--src/timeline/InputBar.cpp30
-rw-r--r--src/timeline/InputBar.h1
-rw-r--r--src/timeline/TimelineModel.cpp44
-rw-r--r--src/timeline/TimelineModel.h13
7 files changed, 152 insertions, 12 deletions
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)