Merge pull request #475 from LorenDB/htmlFormattedNotifs

Better notifications
This commit is contained in:
DeepBlueV7.X 2021-03-18 15:46:04 +01:00 committed by GitHub
commit f6de66576c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 587 additions and 458 deletions

View file

@ -297,6 +297,9 @@ set(SRC_FILES
src/ui/UserProfile.cpp src/ui/UserProfile.cpp
src/ui/RoomSettings.cpp src/ui/RoomSettings.cpp
# Generic notification stuff
src/notifications/Manager.cpp
src/AvatarProvider.cpp src/AvatarProvider.cpp
src/BlurhashProvider.cpp src/BlurhashProvider.cpp
src/Cache.cpp src/Cache.cpp
@ -557,7 +560,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
if (APPLE) if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm) set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON) set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
endif() endif()

View file

@ -5,6 +5,7 @@
#include <QBuffer> #include <QBuffer>
#include <QPixmapCache> #include <QPixmapCache>
#include <QPointer>
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
@ -12,13 +13,14 @@
#include "Cache.h" #include "Cache.h"
#include "Logging.h" #include "Logging.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "Utils.h" #include "Utils.h"
static QPixmapCache avatar_cache; static QPixmapCache avatar_cache;
namespace AvatarProvider { namespace AvatarProvider {
void void
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
{ {
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
@ -33,43 +35,31 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
return; return;
} }
auto data = cache::image(cacheKey); MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
if (!data.isNull()) { QSize(size, size),
pixmap = QPixmap::fromImage(utils::readImage(&data)); [callback, cacheKey, recv = QPointer<QObject>(receiver)](
avatar_cache.insert(cacheKey, pixmap); QString, QSize, QImage img, QString) {
callback(pixmap); if (!recv)
return; return;
}
auto proxy = std::make_shared<AvatarProxy>(); auto proxy = std::make_shared<AvatarProxy>();
QObject::connect(proxy.get(), QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded, &AvatarProxy::avatarDownloaded,
receiver, recv,
[callback, cacheKey](QByteArray data) { [callback, cacheKey](QPixmap pm) {
QPixmap pm = QPixmap::fromImage(utils::readImage(&data)); if (!pm.isNull())
avatar_cache.insert(cacheKey, pm); avatar_cache.insert(
cacheKey, pm);
callback(pm); callback(pm);
}); });
mtx::http::ThumbOpts opts; if (img.isNull()) {
opts.width = size; emit proxy->avatarDownloaded(QPixmap{});
opts.height = size; return;
opts.mxc_url = avatarUrl.toStdString();
http::client()->get_thumbnail(
opts,
[opts, cacheKey, proxy = std::move(proxy)](const std::string &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
} else {
cache::saveImage(cacheKey.toStdString(), res);
} }
emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size())); auto pm = QPixmap::fromImage(std::move(img));
emit proxy->avatarDownloaded(pm);
}); });
} }
@ -80,8 +70,8 @@ resolve(const QString &room_id,
QObject *receiver, QObject *receiver,
AvatarCallback callback) AvatarCallback callback)
{ {
const auto avatarUrl = cache::avatarUrl(room_id, user_id); auto avatarUrl = cache::avatarUrl(room_id, user_id);
resolve(avatarUrl, size, receiver, callback); resolve(std::move(avatarUrl), size, receiver, callback);
} }
} }

View file

@ -8,19 +8,19 @@
#include <QPixmap> #include <QPixmap>
#include <functional> #include <functional>
using AvatarCallback = std::function<void(QPixmap)>;
class AvatarProxy : public QObject class AvatarProxy : public QObject
{ {
Q_OBJECT Q_OBJECT
signals: signals:
void avatarDownloaded(const QByteArray &data); void avatarDownloaded(QPixmap pm);
}; };
using AvatarCallback = std::function<void(QPixmap)>;
namespace AvatarProvider { namespace AvatarProvider {
void void
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb);
void void
resolve(const QString &room_id, resolve(const QString &room_id,
const QString &user_id, const QString &user_id,

View file

@ -55,9 +55,6 @@ constexpr auto BATCH_SIZE = 100;
//! Format: room_id -> RoomInfo //! Format: room_id -> RoomInfo
constexpr auto ROOMS_DB("rooms"); constexpr auto ROOMS_DB("rooms");
constexpr auto INVITES_DB("invites"); constexpr auto INVITES_DB("invites");
//! Keeps already downloaded media for reuse.
//! Format: matrix_url -> binary data.
constexpr auto MEDIA_DB("media");
//! Information that must be kept between sync requests. //! Information that must be kept between sync requests.
constexpr auto SYNC_STATE_DB("sync_state"); constexpr auto SYNC_STATE_DB("sync_state");
//! Read receipts per room/event. //! Read receipts per room/event.
@ -244,7 +241,6 @@ Cache::setup()
syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
@ -700,82 +696,6 @@ Cache::secret(const std::string &name)
return secret.toStdString(); return secret.toStdString();
} }
//
// Media Management
//
void
Cache::saveImage(const std::string &url, const std::string &img_data)
{
if (url.empty() || img_data.empty())
return;
try {
auto txn = lmdb::txn::begin(env_);
mediaDb_.put(txn, url, img_data);
txn.commit();
} catch (const lmdb::error &e) {
nhlog::db()->critical("saveImage: {}", e.what());
}
}
void
Cache::saveImage(const QString &url, const QByteArray &image)
{
saveImage(url.toStdString(), std::string(image.constData(), image.length()));
}
QByteArray
Cache::image(lmdb::txn &txn, const std::string &url)
{
if (url.empty())
return QByteArray();
try {
std::string_view image;
bool res = mediaDb_.get(txn, url, image);
if (!res)
return QByteArray();
return QByteArray(image.data(), (int)image.size());
} catch (const lmdb::error &e) {
nhlog::db()->critical("image: {}, {}", e.what(), url);
}
return QByteArray();
}
QByteArray
Cache::image(const QString &url)
{
if (url.isEmpty())
return QByteArray();
auto key = url.toStdString();
try {
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
std::string_view image;
bool res = mediaDb_.get(txn, key, image);
txn.commit();
if (!res)
return QByteArray();
return QByteArray(image.data(), (int)image.size());
} catch (const lmdb::error &e) {
nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
}
return QByteArray();
}
void void
Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
{ {
@ -860,7 +780,6 @@ Cache::deleteData()
lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, syncStateDb_);
lmdb::dbi_close(env_, roomsDb_); lmdb::dbi_close(env_, roomsDb_);
lmdb::dbi_close(env_, invitesDb_); lmdb::dbi_close(env_, invitesDb_);
lmdb::dbi_close(env_, mediaDb_);
lmdb::dbi_close(env_, readReceiptsDb_); lmdb::dbi_close(env_, readReceiptsDb_);
lmdb::dbi_close(env_, notificationsDb_); lmdb::dbi_close(env_, notificationsDb_);
@ -2470,50 +2389,6 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
return QString(); return QString();
} }
QImage
Cache::getRoomAvatar(const QString &room_id)
{
return getRoomAvatar(room_id.toStdString());
}
QImage
Cache::getRoomAvatar(const std::string &room_id)
{
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
std::string_view response;
if (!roomsDb_.get(txn, room_id, response)) {
txn.commit();
return QImage();
}
std::string media_url;
try {
RoomInfo info = json::parse(response);
media_url = std::move(info.avatar_url);
if (media_url.empty()) {
txn.commit();
return QImage();
}
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse room info: {}, {}",
e.what(),
std::string(response.data(), response.size()));
}
if (!mediaDb_.get(txn, media_url, response)) {
txn.commit();
return QImage();
}
txn.commit();
return QImage::fromData(QByteArray(response.data(), (int)response.size()));
}
std::vector<std::string> std::vector<std::string>
Cache::joinedRooms() Cache::joinedRooms()
{ {
@ -2615,8 +2490,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
MemberInfo tmp = json::parse(user_data); MemberInfo tmp = json::parse(user_data);
members.emplace_back( members.emplace_back(
RoomMember{QString::fromStdString(std::string(user_id)), RoomMember{QString::fromStdString(std::string(user_id)),
QString::fromStdString(tmp.name), QString::fromStdString(tmp.name)});
QImage::fromData(image(txn, tmp.avatar_url))});
} catch (const json::exception &e) { } catch (const json::exception &e) {
nhlog::db()->warn("{}", e.what()); nhlog::db()->warn("{}", e.what());
} }
@ -4240,18 +4114,6 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
} }
//! Retrieves the saved room avatar.
QImage
getRoomAvatar(const QString &id)
{
return instance_->getRoomAvatar(id);
}
QImage
getRoomAvatar(const std::string &id)
{
return instance_->getRoomAvatar(id);
}
void void
updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
{ {
@ -4276,27 +4138,6 @@ lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
return instance_->lastInvisibleEventAfter(room_id, event_id); return instance_->lastInvisibleEventAfter(room_id, event_id);
} }
QByteArray
image(const QString &url)
{
return instance_->image(url);
}
QByteArray
image(lmdb::txn &txn, const std::string &url)
{
return instance_->image(txn, url);
}
void
saveImage(const std::string &url, const std::string &data)
{
instance_->saveImage(url, data);
}
void
saveImage(const QString &url, const QByteArray &data)
{
instance_->saveImage(url, data);
}
RoomInfo RoomInfo
singleRoomInfo(const std::string &room_id) singleRoomInfo(const std::string &room_id)
{ {

View file

@ -6,8 +6,6 @@
#pragma once #pragma once
#include <QDateTime> #include <QDateTime>
#include <QDir>
#include <QImage>
#include <QString> #include <QString>
#if __has_include(<lmdbxx/lmdb++.h>) #if __has_include(<lmdbxx/lmdb++.h>)
@ -135,12 +133,6 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
const std::string &room_id, const std::string &room_id,
const std::string &user_id); const std::string &user_id);
//! Retrieves the saved room avatar.
QImage
getRoomAvatar(const QString &id);
QImage
getRoomAvatar(const std::string &id);
//! Adds a user to the read list for the given event. //! Adds a user to the read list for the given event.
//! //!
//! There should be only one user id present in a receipt list per room. //! There should be only one user id present in a receipt list per room.
@ -162,20 +154,6 @@ getEventIndex(const std::string &room_id, std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>> std::optional<std::pair<uint64_t, std::string>>
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
QByteArray
image(const QString &url);
QByteArray
image(lmdb::txn &txn, const std::string &url);
inline QByteArray
image(const std::string &url)
{
return image(QString::fromStdString(url));
}
void
saveImage(const std::string &url, const std::string &data);
void
saveImage(const QString &url, const QByteArray &data);
RoomInfo RoomInfo
singleRoomInfo(const std::string &room_id); singleRoomInfo(const std::string &room_id);
std::map<QString, RoomInfo> std::map<QString, RoomInfo>

View file

@ -25,7 +25,6 @@ struct RoomMember
{ {
QString user_id; QString user_id;
QString display_name; QString display_name;
QImage avatar;
}; };
//! Used to uniquely identify a list of read receipts. //! Used to uniquely identify a list of read receipts.

View file

@ -118,10 +118,6 @@ public:
const std::string &room_id, const std::string &room_id,
const std::string &user_id); const std::string &user_id);
//! Retrieves the saved room avatar.
QImage getRoomAvatar(const QString &id);
QImage getRoomAvatar(const std::string &id);
//! Adds a user to the read list for the given event. //! Adds a user to the read list for the given event.
//! //!
//! There should be only one user id present in a receipt list per room. //! There should be only one user id present in a receipt list per room.
@ -137,11 +133,6 @@ public:
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
UserReceipts readReceipts(const QString &event_id, const QString &room_id); UserReceipts readReceipts(const QString &event_id, const QString &room_id);
QByteArray image(const QString &url);
QByteArray image(lmdb::txn &txn, const std::string &url);
void saveImage(const std::string &url, const std::string &data);
void saveImage(const QString &url, const QByteArray &data);
RoomInfo singleRoomInfo(const std::string &room_id); RoomInfo singleRoomInfo(const std::string &room_id);
std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res);
@ -528,7 +519,6 @@ private:
lmdb::dbi syncStateDb_; lmdb::dbi syncStateDb_;
lmdb::dbi roomsDb_; lmdb::dbi roomsDb_;
lmdb::dbi invitesDb_; lmdb::dbi invitesDb_;
lmdb::dbi mediaDb_;
lmdb::dbi readReceiptsDb_; lmdb::dbi readReceiptsDb_;
lmdb::dbi notificationsDb_; lmdb::dbi notificationsDb_;

View file

@ -6,6 +6,7 @@
#include "Cache.h" #include "Cache.h"
#include "Logging.h" #include "Logging.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "Splitter.h" #include "Splitter.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
@ -253,37 +254,16 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
void void
CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
{ {
auto savedImgData = cache::image(avatarUrl); MxcImageProvider::download(
if (!savedImgData.isNull()) { QString(avatarUrl).remove(QStringLiteral("mxc://")),
QPixmap pix; QSize(96, 96),
pix.loadFromData(savedImgData); [this, id](QString, QSize, QImage img, QString) {
emit avatarRetrieved(id, pix); if (img.isNull()) {
nhlog::net()->warn("failed to download avatar: {})", id.toStdString());
return; return;
} }
if (avatarUrl.isEmpty()) emit avatarRetrieved(id, QPixmap::fromImage(img));
return;
mtx::http::ThumbOpts opts;
opts.mxc_url = avatarUrl.toStdString();
http::client()->get_thumbnail(
opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
cache::saveImage(opts.mxc_url, res);
auto data = QByteArray(res.data(), (int)res.size());
QPixmap pix;
pix.loadFromData(data);
emit avatarRetrieved(id, pix);
}); });
} }

View file

@ -4,106 +4,201 @@
#include "MxcImageProvider.h" #include "MxcImageProvider.h"
#include <optional>
#include <mtxclient/crypto/client.hpp> #include <mtxclient/crypto/client.hpp>
#include "Cache.h" #include <QByteArray>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include "Logging.h" #include "Logging.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Utils.h" #include "Utils.h"
QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
pool.start(response);
return response;
}
void
MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info)
{
infos.insert(QString::fromStdString(info.url), info);
}
void void
MxcImageResponse::run() MxcImageResponse::run()
{ {
if (m_requestedSize.isValid() && !m_encryptionInfo) { MxcImageProvider::download(
QString fileName = QString("%1_%2x%3_crop") m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
.arg(m_id) if (image.isNull()) {
.arg(m_requestedSize.width()) m_error = "Failed to download image.";
.arg(m_requestedSize.height()); } else {
m_image = image;
auto data = cache::image(fileName); }
if (!data.isNull()) {
m_image = utils::readImage(&data);
if (!m_image.isNull()) {
m_image = m_image.scaled(
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_image.setText("mxc url", "mxc://" + m_id);
if (!m_image.isNull()) {
emit finished(); emit finished();
});
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
if (temp != infos.end())
encryptionInfo = *temp;
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
QString("%1_%2x%3_crop")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height());
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
QDir().mkpath(fileInfo.absolutePath());
if (fileInfo.exists()) {
QImage image(fileInfo.absoluteFilePath());
if (!image.isNull()) {
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (!image.isNull()) {
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return; return;
} }
} }
} }
mtx::http::ThumbOpts opts; mtx::http::ThumbOpts opts;
opts.mxc_url = "mxc://" + m_id.toStdString(); opts.mxc_url = "mxc://" + id.toStdString();
opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
opts.method = "crop"; opts.method = "crop";
http::client()->get_thumbnail( http::client()->get_thumbnail(
opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { opts,
[fileInfo, requestedSize, then, id](const std::string &res,
mtx::http::RequestErr err) {
if (err || res.empty()) { if (err || res.empty()) {
nhlog::net()->error("Failed to download image {}", then(id, QSize(), {}, "");
m_id.toStdString());
m_error = "Failed download";
emit finished();
return; return;
} }
auto data = QByteArray(res.data(), (int)res.size()); auto data = QByteArray(res.data(), (int)res.size());
cache::saveImage(fileName, data); QImage image = utils::readImage(data);
m_image = utils::readImage(&data); if (!image.isNull()) {
if (!m_image.isNull()) { image = image.scaled(
m_image = m_image.scaled( requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} }
m_image.setText("mxc url", "mxc://" + m_id); image.setText("mxc url", "mxc://" + id);
if (image.save(fileInfo.absoluteFilePath(), "png"))
nhlog::ui()->debug("Wrote: {}",
fileInfo.absoluteFilePath().toStdString());
else
nhlog::ui()->debug("Failed to write: {}",
fileInfo.absoluteFilePath().toStdString());
emit finished(); then(id, requestedSize, image, fileInfo.absoluteFilePath());
}); });
} else { } else {
auto data = cache::image(m_id); try {
QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
QFileInfo fileInfo(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
QDir().mkpath(fileInfo.absolutePath());
if (!data.isNull()) { if (fileInfo.exists()) {
m_image = utils::readImage(&data); if (encryptionInfo) {
m_image.setText("mxc url", "mxc://" + m_id); QFile f(fileInfo.absoluteFilePath());
f.open(QIODevice::ReadOnly);
if (!m_image.isNull()) { QByteArray fileData = f.readAll();
emit finished(); auto tempData =
mtx::crypto::to_string(mtx::crypto::decrypt_file(
fileData.toStdString(), encryptionInfo.value()));
auto data =
QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
image.setText("mxc url", "mxc://" + id);
if (!image.isNull()) {
then(id,
requestedSize,
image,
fileInfo.absoluteFilePath());
return; return;
} }
} else {
QImage image(fileInfo.absoluteFilePath());
if (!image.isNull()) {
then(id,
requestedSize,
image,
fileInfo.absoluteFilePath());
return;
}
}
} }
http::client()->download( http::client()->download(
"mxc://" + m_id.toStdString(), "mxc://" + id.toStdString(),
[this](const std::string &res, [fileInfo, requestedSize, then, id, encryptionInfo](
const std::string &res,
const std::string &, const std::string &,
const std::string &originalFilename, const std::string &originalFilename,
mtx::http::RequestErr err) { mtx::http::RequestErr err) {
if (err) { if (err) {
nhlog::net()->error("Failed to download image {}", then(id, QSize(), {}, "");
m_id.toStdString());
m_error = "Failed download";
emit finished();
return; return;
} }
auto temp = res; auto tempData = res;
if (m_encryptionInfo) QFile f(fileInfo.absoluteFilePath());
temp = mtx::crypto::to_string( if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); then(id, QSize(), {}, "");
return;
}
f.write(tempData.data(), tempData.size());
f.close();
auto data = QByteArray(temp.data(), (int)temp.size()); if (encryptionInfo) {
cache::saveImage(m_id, data); tempData =
m_image = utils::readImage(&data); mtx::crypto::to_string(mtx::crypto::decrypt_file(
m_image.setText("original filename", tempData, encryptionInfo.value()));
auto data =
QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
image.setText("original filename",
QString::fromStdString(originalFilename)); QString::fromStdString(originalFilename));
m_image.setText("mxc url", "mxc://" + m_id); image.setText("mxc url", "mxc://" + id);
then(
id, requestedSize, image, fileInfo.absoluteFilePath());
return;
}
emit finished(); QImage image(fileInfo.absoluteFilePath());
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);
image.save(fileInfo.absoluteFilePath());
then(id, requestedSize, image, fileInfo.absoluteFilePath());
}); });
} catch (std::exception &e) {
nhlog::net()->error("Exception while downloading media: {}", e.what());
}
} }
} }

View file

@ -10,21 +10,18 @@
#include <QImage> #include <QImage>
#include <QThreadPool> #include <QThreadPool>
#include <mtx/common.hpp> #include <functional>
#include <boost/optional.hpp> #include <mtx/common.hpp>
class MxcImageResponse class MxcImageResponse
: public QQuickImageResponse : public QQuickImageResponse
, public QRunnable , public QRunnable
{ {
public: public:
MxcImageResponse(const QString &id, MxcImageResponse(const QString &id, const QSize &requestedSize)
const QSize &requestedSize,
boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
: m_id(id) : m_id(id)
, m_requestedSize(requestedSize) , m_requestedSize(requestedSize)
, m_encryptionInfo(encryptionInfo)
{ {
setAutoDelete(false); setAutoDelete(false);
} }
@ -40,7 +37,6 @@ public:
QString m_id, m_error; QString m_id, m_error;
QSize m_requestedSize; QSize m_requestedSize;
QImage m_image; QImage m_image;
boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
}; };
class MxcImageProvider class MxcImageProvider
@ -50,24 +46,13 @@ class MxcImageProvider
Q_OBJECT Q_OBJECT
public slots: public slots:
QQuickImageResponse *requestImageResponse(const QString &id, QQuickImageResponse *requestImageResponse(const QString &id,
const QSize &requestedSize) override const QSize &requestedSize) override;
{
boost::optional<mtx::crypto::EncryptedFile> info;
auto temp = infos.find("mxc://" + id);
if (temp != infos.end())
info = *temp;
MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
pool.start(response); static void download(const QString &id,
return response; const QSize &requestedSize,
} std::function<void(QString, QSize, QImage, QString)> then);
void addEncryptionInfo(mtx::crypto::EncryptedFile info)
{
infos.insert(QString::fromStdString(info.url), info);
}
private: private:
QThreadPool pool; QThreadPool pool;
QHash<QString, mtx::crypto::EncryptedFile> infos;
}; };

View file

@ -8,7 +8,6 @@
#include <QMenu> #include <QMenu>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QSettings>
#include <QtGlobal> #include <QtGlobal>
#include "AvatarProvider.h" #include "AvatarProvider.h"

View file

@ -24,6 +24,7 @@
#include "Cache.h" #include "Cache.h"
#include "Config.h" #include "Config.h"
#include "EventAccessors.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
@ -50,6 +51,35 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin
ts}; ts};
} }
RelatedInfo
utils::stripReplyFallbacks(const TimelineEvent &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));
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
while (related.quoted_body.startsWith(">"))
related.quoted_body.remove(plainQuote);
if (related.quoted_body.startsWith("\n"))
related.quoted_body.remove(0, 1);
related.quoted_body = utils::getQuoteBody(related);
related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
// get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body.remove(QRegularExpression(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
related.quoted_formatted_body.replace("@room", "@\u2060aroom");
related.room = room_id_;
return related;
}
QString QString
utils::localUser() utils::localUser()
{ {
@ -688,11 +718,17 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
} }
QImage QImage
utils::readImage(const QByteArray *data) utils::readImage(const QByteArray &data)
{ {
QBuffer buf; QBuffer buf;
buf.setData(*data); buf.setData(data);
QImageReader reader(&buf); QImageReader reader(&buf);
reader.setAutoTransform(true); reader.setAutoTransform(true);
return reader.read(); return reader.read();
} }
bool
utils::isReply(const mtx::events::collections::TimelineEvents &e)
{
return mtx::accessors::relations(e).reply_to().has_value();
}

View file

@ -40,6 +40,9 @@ namespace utils {
using TimelineEvent = mtx::events::collections::TimelineEvents; using TimelineEvent = mtx::events::collections::TimelineEvents;
RelatedInfo
stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_);
bool bool
codepointIsEmoji(uint code); codepointIsEmoji(uint code);
@ -309,5 +312,8 @@ restoreCombobox(QComboBox *combo, const QString &value);
//! Read image respecting exif orientation //! Read image respecting exif orientation
QImage QImage
readImage(const QByteArray *data); readImage(const QByteArray &data);
bool
isReply(const mtx::events::collections::TimelineEvents &e);
} }

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "notifications/Manager.h"
#include "Cache.h"
#include "EventAccessors.h"
#include "Utils.h"
QString
NotificationsManager::getMessageTemplate(const mtx::responses::Notification &notification)
{
const auto sender =
cache::displayName(QString::fromStdString(notification.room_id),
QString::fromStdString(mtx::accessors::sender(notification.event)));
// TODO: decrypt this message if the decryption setting is on in the UserSettings
if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&notification.event);
msg != nullptr) {
return tr("%1 sent an encrypted message").arg(sender);
}
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) {
return tr("* %1 %2",
"Format an emote message in a notification, %1 is the sender, %2 the "
"message")
.arg(sender);
} else if (utils::isReply(notification.event)) {
return tr("%1 replied: %2",
"Format a reply in a notification. %1 is the sender, %2 the message")
.arg(sender);
} else {
return tr("%1: %2",
"Format a normal message in a notification. %1 is the sender, %2 the "
"message")
.arg(sender);
}
}

View file

@ -10,7 +10,12 @@
#include <mtx/responses/notifications.hpp> #include <mtx/responses/notifications.hpp>
// convenience definition
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
#define NHEKO_DBUS_SYS
#endif
#if defined(NHEKO_DBUS_SYS)
#include <QtDBus/QDBusArgument> #include <QtDBus/QDBusArgument>
#include <QtDBus/QDBusInterface> #include <QtDBus/QDBusInterface>
#endif #endif
@ -38,20 +43,51 @@ public:
signals: signals:
void notificationClicked(const QString roomId, const QString eventId); void notificationClicked(const QString roomId, const QString eventId);
void sendNotificationReply(const QString roomId, const QString eventId, const QString body); void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
void systemPostNotificationCb(const QString &room_id,
const QString &event_id,
const QString &roomName,
const QString &text,
const QImage &icon);
public slots: public slots:
void removeNotification(const QString &roomId, const QString &eventId); void removeNotification(const QString &roomId, const QString &eventId);
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) #if defined(NHEKO_DBUS_SYS)
public: public:
void closeNotifications(QString roomId); void closeNotifications(QString roomId);
private: private:
QDBusInterface dbus; QDBusInterface dbus;
void systemPostNotification(const QString &room_id,
const QString &event_id,
const QString &roomName,
const QString &text,
const QImage &icon);
void closeNotification(uint id); void closeNotification(uint id);
// notification ID to (room ID, event ID) // notification ID to (room ID, event ID)
QMap<uint, roomEventId> notificationIds; QMap<uint, roomEventId> notificationIds;
const bool hasMarkup_;
const bool hasImages_;
#endif
#if defined(Q_OS_MACOS)
private:
// Objective-C(++) doesn't like to do lots of regular C++, so the actual notification
// posting is split out
void objCxxPostNotification(const QString &title,
const QString &subtitle,
const QString &informativeText,
const QImage &bodyImage);
#endif
#if defined(Q_OS_WINDOWS)
private:
void systemPostNotification(const QString &line1,
const QString &line2,
const QString &iconPath);
#endif #endif
// these slots are platform specific (D-Bus only) // these slots are platform specific (D-Bus only)
@ -60,9 +96,12 @@ private slots:
void actionInvoked(uint id, QString action); void actionInvoked(uint id, QString action);
void notificationClosed(uint id, uint reason); void notificationClosed(uint id, uint reason);
void notificationReplied(uint id, QString reply); void notificationReplied(uint id, QString reply);
private:
QString getMessageTemplate(const mtx::responses::Notification &notification);
}; };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) #if defined(NHEKO_DBUS_SYS)
QDBusArgument & QDBusArgument &
operator<<(QDBusArgument &arg, const QImage &image); operator<<(QDBusArgument &arg, const QImage &image);
const QDBusArgument & const QDBusArgument &

View file

@ -1,3 +1,8 @@
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "notifications/Manager.h" #include "notifications/Manager.h"
#include <QDBusConnection> #include <QDBusConnection>
@ -7,12 +12,19 @@
#include <QDBusPendingReply> #include <QDBusPendingReply>
#include <QDebug> #include <QDebug>
#include <QImage> #include <QImage>
#include <QRegularExpression>
#include <QStringBuilder>
#include <QTextDocumentFragment>
#include <functional>
#include <variant>
#include <mtx/responses/notifications.hpp>
#include "Cache.h" #include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "MatrixClient.h" #include "MxcImageProvider.h"
#include "Utils.h" #include "Utils.h"
#include <mtx/responses/notifications.hpp>
NotificationsManager::NotificationsManager(QObject *parent) NotificationsManager::NotificationsManager(QObject *parent)
: QObject(parent) : QObject(parent)
@ -21,6 +33,18 @@ NotificationsManager::NotificationsManager(QObject *parent)
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
QDBusConnection::sessionBus(), QDBusConnection::sessionBus(),
this) this)
, hasMarkup_{std::invoke([this]() -> bool {
for (auto x : dbus.call("GetCapabilities").arguments())
if (x.toStringList().contains("body-markup"))
return true;
return false;
})}
, hasImages_{std::invoke([this]() -> bool {
for (auto x : dbus.call("GetCapabilities").arguments())
if (x.toStringList().contains("body-images"))
return true;
return false;
})}
{ {
qDBusRegisterMetaType<QImage>(); qDBusRegisterMetaType<QImage>();
@ -42,12 +66,13 @@ NotificationsManager::NotificationsManager(QObject *parent)
"NotificationReplied", "NotificationReplied",
this, this,
SLOT(notificationReplied(uint, QString))); SLOT(notificationReplied(uint, QString)));
}
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name> connect(this,
// SPDX-FileCopyrightText: 2021 Nheko Contributors &NotificationsManager::systemPostNotificationCb,
// this,
// SPDX-License-Identifier: GPL-3.0-or-later &NotificationsManager::systemPostNotification,
Qt::QueuedConnection);
}
void void
NotificationsManager::postNotification(const mtx::responses::Notification &notification, NotificationsManager::postNotification(const mtx::responses::Notification &notification,
@ -55,10 +80,78 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
{ {
const auto room_id = QString::fromStdString(notification.room_id); const auto room_id = QString::fromStdString(notification.room_id);
const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
const auto sender = cache::displayName( const auto room_name =
room_id, QString::fromStdString(mtx::accessors::sender(notification.event))); QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
const auto text = utils::event_body(notification.event);
auto postNotif = [this, room_id, event_id, room_name, icon](QString text) {
emit systemPostNotificationCb(room_id, event_id, room_name, text, icon);
};
QString template_ = getMessageTemplate(notification);
// TODO: decrypt this message if the decryption setting is on in the UserSettings
if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
notification.event)) {
postNotif(template_);
return;
}
if (hasMarkup_) {
if (hasImages_ && mtx::accessors::msg_type(notification.event) ==
mtx::events::MessageType::Image) {
MxcImageProvider::download(
QString::fromStdString(mtx::accessors::url(notification.event))
.remove("mxc://"),
QSize(200, 80),
[postNotif, notification, template_](
QString, QSize, QImage, QString imgPath) {
if (imgPath.isEmpty())
postNotif(template_
.arg(utils::stripReplyFallbacks(
notification.event, {}, {})
.quoted_formatted_body)
.replace("<em>", "<i>")
.replace("</em>", "</i>")
.replace("<strong>", "<b>")
.replace("</strong>", "</b>"));
else
postNotif(template_.arg(
QStringLiteral("<br><img src=\"file:///") % imgPath %
"\" alt=\"" %
mtx::accessors::formattedBodyWithFallback(
notification.event) %
"\">"));
});
return;
}
postNotif(
template_
.arg(
utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body)
.replace("<em>", "<i>")
.replace("</em>", "</i>")
.replace("<strong>", "<b>")
.replace("</strong>", "</b>"));
return;
}
postNotif(
template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body));
}
/**
* This function is based on code from
* https://github.com/rohieb/StratumsphereTrayIcon
* Copyright (C) 2012 Roland Hieber <rohieb@rohieb.name>
* Licensed under the GNU General Public License, version 3
*/
void
NotificationsManager::systemPostNotification(const QString &room_id,
const QString &event_id,
const QString &roomName,
const QString &text,
const QImage &icon)
{
QVariantMap hints; QVariantMap hints;
hints["image-data"] = icon; hints["image-data"] = icon;
hints["sound-name"] = "message-new-instant"; hints["sound-name"] = "message-new-instant";
@ -66,14 +159,8 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
argumentList << "nheko"; // app_name argumentList << "nheko"; // app_name
argumentList << (uint)0; // replace_id argumentList << (uint)0; // replace_id
argumentList << ""; // app_icon argumentList << ""; // app_icon
argumentList << QString::fromStdString( argumentList << roomName; // summary
cache::singleRoomInfo(notification.room_id).name); // summary argumentList << text; // body
// body
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
argumentList << "* " + sender + " " + text;
else
argumentList << sender + ": " + text;
// The list of actions has always the action name and then a localized version of that // The list of actions has always the action name and then a localized version of that
// action. Currently we just use an empty string for that. // action. Currently we just use an empty string for that.
@ -84,10 +171,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
argumentList << hints; // hints argumentList << hints; // hints
argumentList << (int)-1; // timeout in ms argumentList << (int)-1; // timeout in ms
static QDBusInterface notifyApp("org.freedesktop.Notifications", QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList);
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications");
QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList);
auto watcher = new QDBusPendingCallWatcher{call, this}; auto watcher = new QDBusPendingCallWatcher{call, this};
connect( connect(
watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() { watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
@ -103,10 +187,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
void void
NotificationsManager::closeNotification(uint id) NotificationsManager::closeNotification(uint id)
{ {
static QDBusInterface closeCall("org.freedesktop.Notifications", auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications");
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
auto watcher = new QDBusPendingCallWatcher{call, this}; auto watcher = new QDBusPendingCallWatcher{call, this};
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() { connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
if (watcher->reply().type() == QDBusMessage::ErrorMessage) { if (watcher->reply().type() == QDBusMessage::ErrorMessage) {

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Manager.h"
#include <QRegularExpression>
#include <QTextDocumentFragment>
#include "Cache.h"
#include "EventAccessors.h"
#include "MxcImageProvider.h"
#include "Utils.h"
#include <mtx/responses/notifications.hpp>
#include <variant>
static QString
formatNotification(const mtx::responses::Notification &notification)
{
return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
}
void
NotificationsManager::postNotification(const mtx::responses::Notification &notification,
const QImage &icon)
{
Q_UNUSED(icon)
const auto room_name =
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
const auto sender =
cache::displayName(QString::fromStdString(notification.room_id),
QString::fromStdString(mtx::accessors::sender(notification.event)));
const auto isEncrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&notification.event) != nullptr;
const auto isReply = utils::isReply(notification.event);
if (isEncrypted) {
// TODO: decrypt this message if the decryption setting is on in the UserSettings
const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message")
: tr("%1 sent an encrypted message"))
.arg(sender);
objCxxPostNotification(room_name, messageInfo, "", QImage());
} else {
const QString messageInfo =
(isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
MxcImageProvider::download(
QString::fromStdString(mtx::accessors::url(notification.event))
.remove("mxc://"),
QSize(200, 80),
[this, notification, room_name, messageInfo](
QString, QSize, QImage image, QString) {
objCxxPostNotification(room_name,
messageInfo,
formatNotification(notification),
image);
});
else
objCxxPostNotification(
room_name, messageInfo, formatNotification(notification), QImage());
}
}

View file

@ -1,13 +1,10 @@
#include "notifications/Manager.h" #include "notifications/Manager.h"
#include <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#include <QtMac> #import <AppKit/NSImage.h>
#include "Cache.h" #include <QtMac>
#include "EventAccessors.h" #include <QImage>
#include "MatrixClient.h"
#include "Utils.h"
#include <mtx/responses/notifications.hpp>
@interface NSUserNotification (CFIPrivate) @interface NSUserNotification (CFIPrivate)
- (void)set_identityImage:(NSImage *)image; - (void)set_identityImage:(NSImage *)image;
@ -19,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
} }
void void
NotificationsManager::postNotification(const mtx::responses::Notification &notification, NotificationsManager::objCxxPostNotification(const QString &title,
const QImage &icon) const QString &subtitle,
const QString &informativeText,
const QImage &bodyImage)
{ {
Q_UNUSED(icon);
const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event)));
const auto text = utils::event_body(notification.event);
NSUserNotification *notif = [[NSUserNotification alloc] init]; NSUserNotification *notif = [[NSUserNotification alloc] init];
notif.title = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name).toNSString(); notif.title = title.toNSString();
notif.subtitle = QString("%1 sent a message").arg(sender).toNSString(); notif.subtitle = subtitle.toNSString();
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) notif.informativeText = informativeText.toNSString();
notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString();
else
notif.informativeText = text.toNSString();
notif.soundName = NSUserNotificationDefaultSoundName; notif.soundName = NSUserNotificationDefaultSoundName;
if (!bodyImage.isNull())
notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize];
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif]; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
[notif autorelease]; [notif autorelease];
} }

View file

@ -5,11 +5,15 @@
#include "notifications/Manager.h" #include "notifications/Manager.h"
#include "wintoastlib.h" #include "wintoastlib.h"
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTextDocumentFragment>
#include <variant>
#include "Cache.h" #include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "MatrixClient.h"
#include "Utils.h" #include "Utils.h"
#include <mtx/responses/notifications.hpp>
using namespace WinToastLib; using namespace WinToastLib;
@ -45,34 +49,57 @@ void
NotificationsManager::postNotification(const mtx::responses::Notification &notification, NotificationsManager::postNotification(const mtx::responses::Notification &notification,
const QImage &icon) const QImage &icon)
{ {
Q_UNUSED(icon)
const auto room_name = const auto room_name =
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
const auto sender = const auto sender =
cache::displayName(QString::fromStdString(notification.room_id), cache::displayName(QString::fromStdString(notification.room_id),
QString::fromStdString(mtx::accessors::sender(notification.event))); QString::fromStdString(mtx::accessors::sender(notification.event)));
const auto text = utils::event_body(notification.event);
const auto isEncrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&notification.event) != nullptr;
const auto isReply = utils::isReply(notification.event);
auto formatNotification = [this, notification, sender] {
const auto template_ = getMessageTemplate(notification);
if (std::holds_alternative<
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
notification.event)) {
return template_;
}
return template_.arg(
utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body);
};
const auto line1 =
(room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name);
const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message")
: tr("%1 sent an encrypted message"))
: formatNotification());
auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
room_name + "-room-avatar.png";
if (!icon.save(iconPath))
iconPath.clear();
systemPostNotification(line1, line2, iconPath);
}
void
NotificationsManager::systemPostNotification(const QString &line1,
const QString &line2,
const QString &iconPath)
{
if (!isInitialized) if (!isInitialized)
init(); init();
auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
if (room_name != sender) templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine);
templ.setTextField(QString("%1 - %2").arg(sender).arg(room_name).toStdWString(), templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine);
WinToastTemplate::FirstLine);
else if (!iconPath.isNull())
templ.setTextField(QString("%1").arg(sender).toStdWString(), templ.setImagePath(iconPath.toStdWString());
WinToastTemplate::FirstLine);
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
templ.setTextField(
QString("* ").append(sender).append(" ").append(text).toStdWString(),
WinToastTemplate::SecondLine);
else
templ.setTextField(QString("%1").arg(text).toStdWString(),
WinToastTemplate::SecondLine);
// TODO: implement room or user avatar
// templ.setImagePath(L"C:/example.png");
WinToast::instance()->showToast(templ, new CustomHandler()); WinToast::instance()->showToast(templ, new CustomHandler());
} }

View file

@ -302,7 +302,9 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
// NOTE(Nico): rich replies always need a formatted_body! // NOTE(Nico): rich replies always need a formatted_body!
text.format = "org.matrix.custom.html"; text.format = "org.matrix.custom.html";
if (ChatPage::instance()->userSettings()->markdown()) if ((ChatPage::instance()->userSettings()->markdown() &&
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
useMarkdown == MarkdownOverride::ON)
text.formatted_body = text.formatted_body =
utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg)) utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg))
.toStdString(); .toStdString();
@ -572,7 +574,7 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
auto mimeClass = mime.split("/")[0]; auto mimeClass = mime.split("/")[0];
nhlog::ui()->debug("Mime: {}", mime.toStdString()); nhlog::ui()->debug("Mime: {}", mime.toStdString());
if (mimeClass == "image") { if (mimeClass == "image") {
QImage img = utils::readImage(&data); QImage img = utils::readImage(data);
dimensions = img.size(); dimensions = img.size();
if (img.height() > 200 && img.width() > 360) if (img.height() > 200 && img.width() > 360)

View file

@ -369,7 +369,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
bool isReply = relations(event).reply_to().has_value(); bool isReply = utils::isReply(event);
auto formattedBody_ = QString::fromStdString(formatted_body(event)); auto formattedBody_ = QString::fromStdString(formatted_body(event));
if (formattedBody_.isEmpty()) { if (formattedBody_.isEmpty()) {
@ -870,30 +870,7 @@ TimelineModel::relatedInfo(QString id)
if (!event) if (!event)
return {}; return {};
RelatedInfo related = {}; return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
related.quoted_user = QString::fromStdString(mtx::accessors::sender(*event));
related.related_event = id.toStdString();
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));
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
while (related.quoted_body.startsWith(">"))
related.quoted_body.remove(plainQuote);
if (related.quoted_body.startsWith("\n"))
related.quoted_body.remove(0, 1);
related.quoted_body = utils::getQuoteBody(related);
related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
// get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(*event);
related.quoted_formatted_body.remove(QRegularExpression(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
related.quoted_formatted_body.replace("@room", "@\u2060aroom");
related.room = room_id_;
return related;
} }
void void