mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 12:38:48 +03:00
Merge pull request #475 from LorenDB/htmlFormattedNotifs
Better notifications
This commit is contained in:
commit
f6de66576c
21 changed files with 587 additions and 458 deletions
|
@ -297,6 +297,9 @@ set(SRC_FILES
|
|||
src/ui/UserProfile.cpp
|
||||
src/ui/RoomSettings.cpp
|
||||
|
||||
# Generic notification stuff
|
||||
src/notifications/Manager.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
src/Cache.cpp
|
||||
|
@ -557,7 +560,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
|||
|
||||
if (APPLE)
|
||||
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")
|
||||
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
endif()
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <QBuffer>
|
||||
#include <QPixmapCache>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
|
@ -12,13 +13,14 @@
|
|||
#include "Cache.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "MxcImageProvider.h"
|
||||
#include "Utils.h"
|
||||
|
||||
static QPixmapCache avatar_cache;
|
||||
|
||||
namespace AvatarProvider {
|
||||
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);
|
||||
|
||||
|
@ -33,44 +35,32 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
|
|||
return;
|
||||
}
|
||||
|
||||
auto data = cache::image(cacheKey);
|
||||
if (!data.isNull()) {
|
||||
pixmap = QPixmap::fromImage(utils::readImage(&data));
|
||||
avatar_cache.insert(cacheKey, pixmap);
|
||||
callback(pixmap);
|
||||
return;
|
||||
}
|
||||
MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
|
||||
QSize(size, size),
|
||||
[callback, cacheKey, recv = QPointer<QObject>(receiver)](
|
||||
QString, QSize, QImage img, QString) {
|
||||
if (!recv)
|
||||
return;
|
||||
|
||||
auto proxy = std::make_shared<AvatarProxy>();
|
||||
QObject::connect(proxy.get(),
|
||||
&AvatarProxy::avatarDownloaded,
|
||||
receiver,
|
||||
[callback, cacheKey](QByteArray data) {
|
||||
QPixmap pm = QPixmap::fromImage(utils::readImage(&data));
|
||||
avatar_cache.insert(cacheKey, pm);
|
||||
callback(pm);
|
||||
});
|
||||
auto proxy = std::make_shared<AvatarProxy>();
|
||||
QObject::connect(proxy.get(),
|
||||
&AvatarProxy::avatarDownloaded,
|
||||
recv,
|
||||
[callback, cacheKey](QPixmap pm) {
|
||||
if (!pm.isNull())
|
||||
avatar_cache.insert(
|
||||
cacheKey, pm);
|
||||
callback(pm);
|
||||
});
|
||||
|
||||
mtx::http::ThumbOpts opts;
|
||||
opts.width = size;
|
||||
opts.height = size;
|
||||
opts.mxc_url = avatarUrl.toStdString();
|
||||
if (img.isNull()) {
|
||||
emit proxy->avatarDownloaded(QPixmap{});
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -80,8 +70,8 @@ resolve(const QString &room_id,
|
|||
QObject *receiver,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,19 +8,19 @@
|
|||
#include <QPixmap>
|
||||
#include <functional>
|
||||
|
||||
using AvatarCallback = std::function<void(QPixmap)>;
|
||||
|
||||
class AvatarProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void avatarDownloaded(const QByteArray &data);
|
||||
void avatarDownloaded(QPixmap pm);
|
||||
};
|
||||
|
||||
using AvatarCallback = std::function<void(QPixmap)>;
|
||||
|
||||
namespace AvatarProvider {
|
||||
void
|
||||
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
||||
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
||||
void
|
||||
resolve(const QString &room_id,
|
||||
const QString &user_id,
|
||||
|
|
161
src/Cache.cpp
161
src/Cache.cpp
|
@ -55,9 +55,6 @@ constexpr auto BATCH_SIZE = 100;
|
|||
//! Format: room_id -> RoomInfo
|
||||
constexpr auto ROOMS_DB("rooms");
|
||||
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.
|
||||
constexpr auto SYNC_STATE_DB("sync_state");
|
||||
//! Read receipts per room/event.
|
||||
|
@ -244,7 +241,6 @@ Cache::setup()
|
|||
syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
|
||||
roomsDb_ = lmdb::dbi::open(txn, ROOMS_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);
|
||||
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
|
||||
|
||||
|
@ -700,82 +696,6 @@ Cache::secret(const std::string &name)
|
|||
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
|
||||
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_, roomsDb_);
|
||||
lmdb::dbi_close(env_, invitesDb_);
|
||||
lmdb::dbi_close(env_, mediaDb_);
|
||||
lmdb::dbi_close(env_, readReceiptsDb_);
|
||||
lmdb::dbi_close(env_, notificationsDb_);
|
||||
|
||||
|
@ -2470,50 +2389,6 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
|
|||
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>
|
||||
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);
|
||||
members.emplace_back(
|
||||
RoomMember{QString::fromStdString(std::string(user_id)),
|
||||
QString::fromStdString(tmp.name),
|
||||
QImage::fromData(image(txn, tmp.avatar_url))});
|
||||
QString::fromStdString(tmp.name)});
|
||||
} catch (const json::exception &e) {
|
||||
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);
|
||||
}
|
||||
|
||||
//! 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
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
singleRoomInfo(const std::string &room_id)
|
||||
{
|
||||
|
|
22
src/Cache.h
22
src/Cache.h
|
@ -6,8 +6,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
|
||||
#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 &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.
|
||||
//!
|
||||
//! 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>>
|
||||
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
|
||||
singleRoomInfo(const std::string &room_id);
|
||||
std::map<QString, RoomInfo>
|
||||
|
|
|
@ -25,7 +25,6 @@ struct RoomMember
|
|||
{
|
||||
QString user_id;
|
||||
QString display_name;
|
||||
QImage avatar;
|
||||
};
|
||||
|
||||
//! Used to uniquely identify a list of read receipts.
|
||||
|
|
|
@ -118,10 +118,6 @@ public:
|
|||
const std::string &room_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.
|
||||
//!
|
||||
//! 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>>;
|
||||
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);
|
||||
std::vector<std::string> roomsWithStateUpdates(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 roomsDb_;
|
||||
lmdb::dbi invitesDb_;
|
||||
lmdb::dbi mediaDb_;
|
||||
lmdb::dbi readReceiptsDb_;
|
||||
lmdb::dbi notificationsDb_;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "Cache.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "MxcImageProvider.h"
|
||||
#include "Splitter.h"
|
||||
#include "UserSettingsPage.h"
|
||||
|
||||
|
@ -253,37 +254,16 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
|
|||
void
|
||||
CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
|
||||
{
|
||||
auto savedImgData = cache::image(avatarUrl);
|
||||
if (!savedImgData.isNull()) {
|
||||
QPixmap pix;
|
||||
pix.loadFromData(savedImgData);
|
||||
emit avatarRetrieved(id, pix);
|
||||
return;
|
||||
}
|
||||
|
||||
if (avatarUrl.isEmpty())
|
||||
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);
|
||||
MxcImageProvider::download(
|
||||
QString(avatarUrl).remove(QStringLiteral("mxc://")),
|
||||
QSize(96, 96),
|
||||
[this, id](QString, QSize, QImage img, QString) {
|
||||
if (img.isNull()) {
|
||||
nhlog::net()->warn("failed to download avatar: {})", id.toStdString());
|
||||
return;
|
||||
}
|
||||
|
||||
cache::saveImage(opts.mxc_url, res);
|
||||
|
||||
auto data = QByteArray(res.data(), (int)res.size());
|
||||
|
||||
QPixmap pix;
|
||||
pix.loadFromData(data);
|
||||
|
||||
emit avatarRetrieved(id, pix);
|
||||
emit avatarRetrieved(id, QPixmap::fromImage(img));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,106 +4,201 @@
|
|||
|
||||
#include "MxcImageProvider.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <mtxclient/crypto/client.hpp>
|
||||
|
||||
#include "Cache.h"
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.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
|
||||
MxcImageResponse::run()
|
||||
{
|
||||
if (m_requestedSize.isValid() && !m_encryptionInfo) {
|
||||
QString fileName = QString("%1_%2x%3_crop")
|
||||
.arg(m_id)
|
||||
.arg(m_requestedSize.width())
|
||||
.arg(m_requestedSize.height());
|
||||
MxcImageProvider::download(
|
||||
m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
|
||||
if (image.isNull()) {
|
||||
m_error = "Failed to download image.";
|
||||
} else {
|
||||
m_image = image;
|
||||
}
|
||||
emit finished();
|
||||
});
|
||||
}
|
||||
|
||||
auto data = cache::image(fileName);
|
||||
if (!data.isNull()) {
|
||||
m_image = utils::readImage(&data);
|
||||
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 (!m_image.isNull()) {
|
||||
m_image = m_image.scaled(
|
||||
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
m_image.setText("mxc url", "mxc://" + m_id);
|
||||
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 (!m_image.isNull()) {
|
||||
emit finished();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mtx::http::ThumbOpts opts;
|
||||
opts.mxc_url = "mxc://" + m_id.toStdString();
|
||||
opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1;
|
||||
opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1;
|
||||
opts.mxc_url = "mxc://" + id.toStdString();
|
||||
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
|
||||
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
|
||||
opts.method = "crop";
|
||||
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()) {
|
||||
nhlog::net()->error("Failed to download image {}",
|
||||
m_id.toStdString());
|
||||
m_error = "Failed download";
|
||||
emit finished();
|
||||
then(id, QSize(), {}, "");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto data = QByteArray(res.data(), (int)res.size());
|
||||
cache::saveImage(fileName, data);
|
||||
m_image = utils::readImage(&data);
|
||||
if (!m_image.isNull()) {
|
||||
m_image = m_image.scaled(
|
||||
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
auto data = QByteArray(res.data(), (int)res.size());
|
||||
QImage image = utils::readImage(data);
|
||||
if (!image.isNull()) {
|
||||
image = image.scaled(
|
||||
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 {
|
||||
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()) {
|
||||
m_image = utils::readImage(&data);
|
||||
m_image.setText("mxc url", "mxc://" + m_id);
|
||||
if (fileInfo.exists()) {
|
||||
if (encryptionInfo) {
|
||||
QFile f(fileInfo.absoluteFilePath());
|
||||
f.open(QIODevice::ReadOnly);
|
||||
|
||||
if (!m_image.isNull()) {
|
||||
emit finished();
|
||||
return;
|
||||
QByteArray fileData = f.readAll();
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
QImage image(fileInfo.absoluteFilePath());
|
||||
if (!image.isNull()) {
|
||||
then(id,
|
||||
requestedSize,
|
||||
image,
|
||||
fileInfo.absoluteFilePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http::client()->download(
|
||||
"mxc://" + id.toStdString(),
|
||||
[fileInfo, requestedSize, then, id, encryptionInfo](
|
||||
const std::string &res,
|
||||
const std::string &,
|
||||
const std::string &originalFilename,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
then(id, QSize(), {}, "");
|
||||
return;
|
||||
}
|
||||
|
||||
auto tempData = res;
|
||||
QFile f(fileInfo.absoluteFilePath());
|
||||
if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
|
||||
then(id, QSize(), {}, "");
|
||||
return;
|
||||
}
|
||||
f.write(tempData.data(), tempData.size());
|
||||
f.close();
|
||||
|
||||
if (encryptionInfo) {
|
||||
tempData =
|
||||
mtx::crypto::to_string(mtx::crypto::decrypt_file(
|
||||
tempData, encryptionInfo.value()));
|
||||
auto data =
|
||||
QByteArray(tempData.data(), (int)tempData.size());
|
||||
QImage image = utils::readImage(data);
|
||||
image.setText("original filename",
|
||||
QString::fromStdString(originalFilename));
|
||||
image.setText("mxc url", "mxc://" + id);
|
||||
then(
|
||||
id, requestedSize, image, fileInfo.absoluteFilePath());
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
http::client()->download(
|
||||
"mxc://" + m_id.toStdString(),
|
||||
[this](const std::string &res,
|
||||
const std::string &,
|
||||
const std::string &originalFilename,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->error("Failed to download image {}",
|
||||
m_id.toStdString());
|
||||
m_error = "Failed download";
|
||||
emit finished();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto temp = res;
|
||||
if (m_encryptionInfo)
|
||||
temp = mtx::crypto::to_string(
|
||||
mtx::crypto::decrypt_file(temp, m_encryptionInfo.value()));
|
||||
|
||||
auto data = QByteArray(temp.data(), (int)temp.size());
|
||||
cache::saveImage(m_id, data);
|
||||
m_image = utils::readImage(&data);
|
||||
m_image.setText("original filename",
|
||||
QString::fromStdString(originalFilename));
|
||||
m_image.setText("mxc url", "mxc://" + m_id);
|
||||
|
||||
emit finished();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,21 +10,18 @@
|
|||
#include <QImage>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <mtx/common.hpp>
|
||||
#include <functional>
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
#include <mtx/common.hpp>
|
||||
|
||||
class MxcImageResponse
|
||||
: public QQuickImageResponse
|
||||
, public QRunnable
|
||||
{
|
||||
public:
|
||||
MxcImageResponse(const QString &id,
|
||||
const QSize &requestedSize,
|
||||
boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
|
||||
MxcImageResponse(const QString &id, const QSize &requestedSize)
|
||||
: m_id(id)
|
||||
, m_requestedSize(requestedSize)
|
||||
, m_encryptionInfo(encryptionInfo)
|
||||
{
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
@ -40,7 +37,6 @@ public:
|
|||
QString m_id, m_error;
|
||||
QSize m_requestedSize;
|
||||
QImage m_image;
|
||||
boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
|
||||
};
|
||||
|
||||
class MxcImageProvider
|
||||
|
@ -50,24 +46,13 @@ class MxcImageProvider
|
|||
Q_OBJECT
|
||||
public slots:
|
||||
QQuickImageResponse *requestImageResponse(const QString &id,
|
||||
const QSize &requestedSize) override
|
||||
{
|
||||
boost::optional<mtx::crypto::EncryptedFile> info;
|
||||
auto temp = infos.find("mxc://" + id);
|
||||
if (temp != infos.end())
|
||||
info = *temp;
|
||||
const QSize &requestedSize) override;
|
||||
|
||||
MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info);
|
||||
pool.start(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
void addEncryptionInfo(mtx::crypto::EncryptedFile info)
|
||||
{
|
||||
infos.insert(QString::fromStdString(info.url), info);
|
||||
}
|
||||
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
|
||||
static void download(const QString &id,
|
||||
const QSize &requestedSize,
|
||||
std::function<void(QString, QSize, QImage, QString)> then);
|
||||
|
||||
private:
|
||||
QThreadPool pool;
|
||||
QHash<QString, mtx::crypto::EncryptedFile> infos;
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QSettings>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "AvatarProvider.h"
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
#include "Cache.h"
|
||||
#include "Config.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "UserSettingsPage.h"
|
||||
|
||||
|
@ -50,6 +51,35 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin
|
|||
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
|
||||
utils::localUser()
|
||||
{
|
||||
|
@ -688,11 +718,17 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
|
|||
}
|
||||
|
||||
QImage
|
||||
utils::readImage(const QByteArray *data)
|
||||
utils::readImage(const QByteArray &data)
|
||||
{
|
||||
QBuffer buf;
|
||||
buf.setData(*data);
|
||||
buf.setData(data);
|
||||
QImageReader reader(&buf);
|
||||
reader.setAutoTransform(true);
|
||||
return reader.read();
|
||||
}
|
||||
|
||||
bool
|
||||
utils::isReply(const mtx::events::collections::TimelineEvents &e)
|
||||
{
|
||||
return mtx::accessors::relations(e).reply_to().has_value();
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ namespace utils {
|
|||
|
||||
using TimelineEvent = mtx::events::collections::TimelineEvents;
|
||||
|
||||
RelatedInfo
|
||||
stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_);
|
||||
|
||||
bool
|
||||
codepointIsEmoji(uint code);
|
||||
|
||||
|
@ -309,5 +312,8 @@ restoreCombobox(QComboBox *combo, const QString &value);
|
|||
|
||||
//! Read image respecting exif orientation
|
||||
QImage
|
||||
readImage(const QByteArray *data);
|
||||
readImage(const QByteArray &data);
|
||||
|
||||
bool
|
||||
isReply(const mtx::events::collections::TimelineEvents &e);
|
||||
}
|
||||
|
|
40
src/notifications/Manager.cpp
Normal file
40
src/notifications/Manager.cpp
Normal 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 ¬ification)
|
||||
{
|
||||
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>>(
|
||||
¬ification.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);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,12 @@
|
|||
|
||||
#include <mtx/responses/notifications.hpp>
|
||||
|
||||
// convenience definition
|
||||
#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/QDBusInterface>
|
||||
#endif
|
||||
|
@ -38,20 +43,51 @@ public:
|
|||
signals:
|
||||
void notificationClicked(const QString roomId, const QString eventId);
|
||||
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:
|
||||
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:
|
||||
void closeNotifications(QString roomId);
|
||||
|
||||
private:
|
||||
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);
|
||||
|
||||
// notification ID to (room ID, event ID)
|
||||
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
|
||||
|
||||
// these slots are platform specific (D-Bus only)
|
||||
|
@ -60,9 +96,12 @@ private slots:
|
|||
void actionInvoked(uint id, QString action);
|
||||
void notificationClosed(uint id, uint reason);
|
||||
void notificationReplied(uint id, QString reply);
|
||||
|
||||
private:
|
||||
QString getMessageTemplate(const mtx::responses::Notification ¬ification);
|
||||
};
|
||||
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
|
||||
#if defined(NHEKO_DBUS_SYS)
|
||||
QDBusArgument &
|
||||
operator<<(QDBusArgument &arg, const QImage &image);
|
||||
const QDBusArgument &
|
||||
|
|
|
@ -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 <QDBusConnection>
|
||||
|
@ -7,12 +12,19 @@
|
|||
#include <QDBusPendingReply>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringBuilder>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include <functional>
|
||||
#include <variant>
|
||||
|
||||
#include <mtx/responses/notifications.hpp>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "MxcImageProvider.h"
|
||||
#include "Utils.h"
|
||||
#include <mtx/responses/notifications.hpp>
|
||||
|
||||
NotificationsManager::NotificationsManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
|
@ -21,6 +33,18 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
|||
"org.freedesktop.Notifications",
|
||||
QDBusConnection::sessionBus(),
|
||||
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>();
|
||||
|
||||
|
@ -42,12 +66,13 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
|||
"NotificationReplied",
|
||||
this,
|
||||
SLOT(notificationReplied(uint, QString)));
|
||||
}
|
||||
|
||||
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name>
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
connect(this,
|
||||
&NotificationsManager::systemPostNotificationCb,
|
||||
this,
|
||||
&NotificationsManager::systemPostNotification,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void
|
||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||
|
@ -55,25 +80,87 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
|||
{
|
||||
const auto room_id = QString::fromStdString(notification.room_id);
|
||||
const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
|
||||
const auto sender = cache::displayName(
|
||||
room_id, QString::fromStdString(mtx::accessors::sender(notification.event)));
|
||||
const auto text = utils::event_body(notification.event);
|
||||
const auto room_name =
|
||||
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
|
||||
|
||||
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;
|
||||
hints["image-data"] = icon;
|
||||
hints["sound-name"] = "message-new-instant";
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << "nheko"; // app_name
|
||||
argumentList << (uint)0; // replace_id
|
||||
argumentList << ""; // app_icon
|
||||
argumentList << QString::fromStdString(
|
||||
cache::singleRoomInfo(notification.room_id).name); // summary
|
||||
|
||||
// body
|
||||
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
|
||||
argumentList << "* " + sender + " " + text;
|
||||
else
|
||||
argumentList << sender + ": " + text;
|
||||
argumentList << "nheko"; // app_name
|
||||
argumentList << (uint)0; // replace_id
|
||||
argumentList << ""; // app_icon
|
||||
argumentList << roomName; // summary
|
||||
argumentList << text; // body
|
||||
|
||||
// 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.
|
||||
|
@ -84,10 +171,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
|||
argumentList << hints; // hints
|
||||
argumentList << (int)-1; // timeout in ms
|
||||
|
||||
static QDBusInterface notifyApp("org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
"org.freedesktop.Notifications");
|
||||
QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList);
|
||||
QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList);
|
||||
auto watcher = new QDBusPendingCallWatcher{call, this};
|
||||
connect(
|
||||
watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
|
||||
|
@ -103,10 +187,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
|||
void
|
||||
NotificationsManager::closeNotification(uint id)
|
||||
{
|
||||
static QDBusInterface closeCall("org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
"org.freedesktop.Notifications");
|
||||
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
|
||||
auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id
|
||||
auto watcher = new QDBusPendingCallWatcher{call, this};
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
|
||||
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
|
||||
|
|
66
src/notifications/ManagerMac.cpp
Normal file
66
src/notifications/ManagerMac.cpp
Normal 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 ¬ification)
|
||||
{
|
||||
return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
|
||||
}
|
||||
|
||||
void
|
||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||
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>>(
|
||||
¬ification.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());
|
||||
}
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
#include "notifications/Manager.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <QtMac>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AppKit/NSImage.h>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Utils.h"
|
||||
#include <mtx/responses/notifications.hpp>
|
||||
#include <QtMac>
|
||||
#include <QImage>
|
||||
|
||||
@interface NSUserNotification (CFIPrivate)
|
||||
- (void)set_identityImage:(NSImage *)image;
|
||||
|
@ -19,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
|
|||
}
|
||||
|
||||
void
|
||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||
const QImage &icon)
|
||||
NotificationsManager::objCxxPostNotification(const QString &title,
|
||||
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.subtitle = QString("%1 sent a message").arg(sender).toNSString();
|
||||
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
|
||||
notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString();
|
||||
else
|
||||
notif.informativeText = text.toNSString();
|
||||
notif.title = title.toNSString();
|
||||
notif.subtitle = subtitle.toNSString();
|
||||
notif.informativeText = informativeText.toNSString();
|
||||
notif.soundName = NSUserNotificationDefaultSoundName;
|
||||
|
||||
if (!bodyImage.isNull())
|
||||
notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize];
|
||||
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
|
||||
[notif autorelease];
|
||||
}
|
||||
|
@ -45,7 +40,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
|||
void
|
||||
NotificationsManager::actionInvoked(uint, QString)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NotificationsManager::notificationReplied(uint, QString)
|
||||
|
|
|
@ -5,11 +5,15 @@
|
|||
#include "notifications/Manager.h"
|
||||
#include "wintoastlib.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Utils.h"
|
||||
#include <mtx/responses/notifications.hpp>
|
||||
|
||||
using namespace WinToastLib;
|
||||
|
||||
|
@ -45,34 +49,57 @@ void
|
|||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||
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 text = utils::event_body(notification.event);
|
||||
|
||||
const auto isEncrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
¬ification.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)
|
||||
init();
|
||||
|
||||
auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
|
||||
if (room_name != sender)
|
||||
templ.setTextField(QString("%1 - %2").arg(sender).arg(room_name).toStdWString(),
|
||||
WinToastTemplate::FirstLine);
|
||||
else
|
||||
templ.setTextField(QString("%1").arg(sender).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");
|
||||
templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine);
|
||||
templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine);
|
||||
|
||||
if (!iconPath.isNull())
|
||||
templ.setImagePath(iconPath.toStdWString());
|
||||
|
||||
WinToast::instance()->showToast(templ, new CustomHandler());
|
||||
}
|
||||
|
|
|
@ -302,7 +302,9 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
|||
|
||||
// NOTE(Nico): rich replies always need a formatted_body!
|
||||
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 =
|
||||
utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg))
|
||||
.toStdString();
|
||||
|
@ -572,7 +574,7 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
|
|||
auto mimeClass = mime.split("/")[0];
|
||||
nhlog::ui()->debug("Mime: {}", mime.toStdString());
|
||||
if (mimeClass == "image") {
|
||||
QImage img = utils::readImage(&data);
|
||||
QImage img = utils::readImage(data);
|
||||
|
||||
dimensions = img.size();
|
||||
if (img.height() > 200 && img.width() > 360)
|
||||
|
|
|
@ -369,7 +369,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||
|
||||
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));
|
||||
if (formattedBody_.isEmpty()) {
|
||||
|
@ -870,30 +870,7 @@ TimelineModel::relatedInfo(QString id)
|
|||
if (!event)
|
||||
return {};
|
||||
|
||||
RelatedInfo related = {};
|
||||
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;
|
||||
return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
Loading…
Reference in a new issue