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 "); 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")}; @@ -623,6 +625,30 @@ InputBar::confetti(const QString &body, bool rainbowify) 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 InputBar::image(const QString &filename, const std::optional &file, @@ -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 &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>(e)) { if (auto msg = QString::fromStdString( std::get>(e).content.body); - msg.contains("🎉") || msg.contains("🎊")) + msg.contains("🎉") || msg.contains("🎊")) { needsSpecialEffects_ = true; + specialEffects_.setFlag(Confetti); + } } else if (std::holds_alternative>(e)) { if (auto msg = QString::fromStdString( std::get>(e).content.body); - msg.contains("🎉") || msg.contains("🎊")) + msg.contains("🎉") || msg.contains("🎊")) { needsSpecialEffects_ = true; - } else if (std::holds_alternative>(e)) + specialEffects_.setFlag(Confetti); + } else if (std::get>(e).content.msgtype == + "io.element.effect.rainfall") { + needsSpecialEffects_ = true; + specialEffects_.setFlag(Rainfall); + } + } else if (std::holds_alternative>(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>(&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 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 specialEffects_; std::unique_ptr parentSummary = nullptr; bool parentChecked = false; @@ -531,6 +540,8 @@ private: friend void EventStore::refetchOnlineKeyBackupKeys(TimelineModel *room); }; +Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineModel::SpecialEffects) + template void TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)