// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include "Utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Cache.h" #include "Cache_p.h" #include "ChatPage.h" #include "Config.h" #include "EventAccessors.h" #include "Logging.h" #include "MatrixClient.h" #include "UserSettingsPage.h" //! Match widgets/events with a description message. namespace { template QString messageDescription(const QString &username, const QString &body, const bool isLocal, bool containsSpoiler) { using Audio = mtx::events::RoomEvent; using Emote = mtx::events::RoomEvent; using File = mtx::events::RoomEvent; using Image = mtx::events::RoomEvent; using Notice = mtx::events::RoomEvent; using Sticker = mtx::events::Sticker; using Text = mtx::events::RoomEvent; using Unknown = mtx::events::RoomEvent; using Video = mtx::events::RoomEvent; using ElementEffect = mtx::events::RoomEvent; using CallInvite = mtx::events::RoomEvent; using CallAnswer = mtx::events::RoomEvent; using CallHangUp = mtx::events::RoomEvent; using CallReject = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an audio clip"); else return QCoreApplication::translate("message-description sent:", "%1 sent an audio clip") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an image"); else return QCoreApplication::translate("message-description sent:", "%1 sent an image") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a file"); else return QCoreApplication::translate("message-description sent:", "%1 sent a file") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a video"); else return QCoreApplication::translate("message-description sent:", "%1 sent a video") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a sticker"); else return QCoreApplication::translate("message-description sent:", "%1 sent a sticker") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a notification"); else return QCoreApplication::translate("message-description sent:", "%1 sent a notification") .arg(username); } else if (std::is_same::value || std::is_same::value) { if (containsSpoiler) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a spoiler."); else return QCoreApplication::translate("message-description sent:", "%1 sent a spoiler.") .arg(username); } if (isLocal) return QCoreApplication::translate("message-description sent:", "You: %1").arg(body); else return QCoreApplication::translate("message-description sent:", "%1: %2") .arg(username, body); } else if (std::is_same::value) { if (body.isEmpty()) { // TODO: what is the best way to handle this? if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a chat effect"); else return QCoreApplication::translate("message-description sent:", "%1 sent a chat effect") .arg(username); } else { if (containsSpoiler) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a spoiler."); else return QCoreApplication::translate("message-description sent:", "%1 sent a spoiler.") .arg(username); } if (isLocal) return QCoreApplication::translate("message-description sent:", "You: %1") .arg(body); else return QCoreApplication::translate("message-description sent:", "%1: %2") .arg(username, body); } } else if (std::is_same::value) { if (containsSpoiler) { return QCoreApplication::translate("message-description sent:", "* %1 spoils something.") .arg(username); } return QStringLiteral("* %1 %2").arg(username, body); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an encrypted message"); else return QCoreApplication::translate("message-description sent:", "%1 sent an encrypted message") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You placed a call"); else return QCoreApplication::translate("message-description sent:", "%1 placed a call") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You answered a call"); else return QCoreApplication::translate("message-description sent:", "%1 answered a call") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You ended a call"); else return QCoreApplication::translate("message-description sent:", "%1 ended a call") .arg(username); } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You rejected a call"); else return QCoreApplication::translate("message-description sent:", "%1 rejected a call") .arg(username); } else { return QCoreApplication::translate("utils", "Unknown Message Type"); } } } template static DescInfo createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName) { const auto msg = std::get(event); const auto sender = QString::fromStdString(msg.sender); const auto username = displayName; const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); auto body = mtx::accessors::body(event); auto formatted_body = mtx::accessors::formatted_body(event); if (mtx::accessors::relations(event).reply_to()) { body = utils::stripReplyFromBody(body); formatted_body = utils::stripReplyFromFormattedBody(formatted_body); } // Simplistic heuristic bool containsSpoiler = formatted_body.find("( username, QString::fromStdString(body), sender == localUser, containsSpoiler), utils::descriptiveTime(ts), msg.origin_server_ts, ts}; } std::string utils::stripReplyFromBody(const std::string &bodyi) { QString body = QString::fromStdString(bodyi); if (body.startsWith(QLatin1String("> <"))) { auto segments = body.split('\n'); while (!segments.isEmpty() && segments.begin()->startsWith('>')) segments.erase(segments.cbegin()); if (!segments.empty() && segments.first().isEmpty()) segments.erase(segments.cbegin()); body = segments.join('\n'); } body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room")); return body.toStdString(); } std::string utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi) { QString formatted_body = QString::fromStdString(formatted_bodyi); static QRegularExpression replyRegex(QStringLiteral(".*"), QRegularExpression::DotMatchesEverythingOption); formatted_body.remove(replyRegex); formatted_body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room")); return formatted_body.toStdString(); } RelatedInfo utils::stripReplyFallbacks(const mtx::events::collections::TimelineEvents &event, std::string id, QString room_id_) { RelatedInfo related = {}; related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); related.related_event = std::move(id); related.type = mtx::accessors::msg_type(event); // get body, strip reply fallback, then transform the event to text, if it is a media event // etc related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); related.quoted_body = QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); related.quoted_body = utils::getQuoteBody(related); // get quoted body and strip reply fallback related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); related.quoted_formatted_body = QString::fromStdString( stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); related.room = room_id_; return related; } QString utils::localUser() { return QString::fromStdString(http::client()->user_id().to_string()); } bool utils::codepointIsEmoji(uint code) { // TODO: Be more precise here. return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f || code == 0x231a || code == 0x231b || (code >= 0x23e9 && code <= 0x23ff) || code == 0x25aa || code == 0x25ab || code == 0x25b6 || code == 0x25c0 || (code >= 0x25fb && code <= 0x25fe) || (code >= 0x2460 && code <= 0x24ff); } QString utils::replaceEmoji(const QString &body) { QString fmtBody; fmtBody.reserve(body.size()); QVector utf32_string = body.toUcs4(); bool insideFontBlock = false; bool insideTag = false; for (auto &code : utf32_string) { if (code == U'<') insideTag = true; else if (code == U'>') insideTag = false; if (!insideTag && utils::codepointIsEmoji(code)) { if (!insideFontBlock) { fmtBody += QStringLiteral("emojiFont() % (UserSettings::instance()->enlargeEmojiOnlyMessages() ? QStringLiteral("\" size=\"4\">") : QStringLiteral("\">")); insideFontBlock = true; } else if (code == 0xfe0f) { // BUG(Nico): // Workaround https://bugreports.qt.io/browse/QTBUG-97401 // See also https://github.com/matrix-org/matrix-react-sdk/pull/1458/files // Nheko bug: https://github.com/Nheko-Reborn/nheko/issues/439 continue; } } else { if (insideFontBlock) { fmtBody += QStringLiteral(""); insideFontBlock = false; } } if (QChar::requiresSurrogates(code)) { QChar emoji[] = {static_cast(QChar::highSurrogate(code)), static_cast(QChar::lowSurrogate(code))}; fmtBody.append(emoji, 2); } else { fmtBody.append(QChar(static_cast(code))); } } if (insideFontBlock) { fmtBody += QStringLiteral(""); } return fmtBody; } void utils::setScaleFactor(float factor) { if (factor < 1 || factor > 3) return; QSettings settings; settings.setValue(QStringLiteral("settings/scale_factor"), factor); } float utils::scaleFactor() { QSettings settings; return settings.value(QStringLiteral("settings/scale_factor"), -1).toFloat(); } QString utils::descriptiveTime(const QDateTime &then) { const auto now = QDateTime::currentDateTime(); const auto days = then.daysTo(now); if (days == 0) return QLocale::system().toString(then.time(), QLocale::ShortFormat); else if (days < 2) return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); else if (days < 7) return then.toString(QStringLiteral("dddd")); return QLocale::system().toString(then.date(), QLocale::ShortFormat); } DescInfo utils::getMessageDescription(const mtx::events::collections::TimelineEvents &event, const QString &localUser, const QString &displayName) { using Audio = mtx::events::RoomEvent; using Emote = mtx::events::RoomEvent; using File = mtx::events::RoomEvent; using Image = mtx::events::RoomEvent; using Notice = mtx::events::RoomEvent; using Text = mtx::events::RoomEvent; using Unknown = mtx::events::RoomEvent; using Video = mtx::events::RoomEvent; using ElementEffect = mtx::events::RoomEvent; using CallInvite = mtx::events::RoomEvent; using CallAnswer = mtx::events::RoomEvent; using CallHangUp = mtx::events::RoomEvent; using CallReject = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; if (std::holds_alternative