New attachment mechanism. Also add image from clipboard.

square-messages
Black Hat 2019-05-19 21:58:54 +08:00
parent ae5154fd35
commit 603cb33042
16 changed files with 279 additions and 67 deletions

View File

@ -11,3 +11,4 @@ FontFamilyDialog 2.0 FontFamilyDialog.qml
AccountDetailDialog 2.0 AccountDetailDialog.qml AccountDetailDialog 2.0 AccountDetailDialog.qml
OpenFileDialog 2.0 OpenFileDialog.qml OpenFileDialog 2.0 OpenFileDialog.qml
OpenFolderDialog 2.0 OpenFolderDialog.qml OpenFolderDialog 2.0 OpenFolderDialog.qml
ImageClipboardDialog 2.0 ImageClipboardDialog.qml

View File

@ -31,7 +31,7 @@ Item {
connection: root.connection connection: root.connection
onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) onNewMessage: if (!window.active && MSettings.showNotification) notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon)
} }
SortFilterProxyModel { SortFilterProxyModel {

View File

@ -3,10 +3,12 @@ import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material 2.12
import Qt.labs.qmlmodels 1.0 import Qt.labs.qmlmodels 1.0
import Qt.labs.platform 1.0
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0 import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 2.0 import Spectral.Component.Timeline 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0 import Spectral.Effect 2.0
import Spectral 0.1 import Spectral 0.1
@ -31,10 +33,113 @@ Item {
onDropped: { onDropped: {
if (!drop.hasUrls) return if (!drop.hasUrls) return
currentRoom.uploadFile(drop.urls[0]) roomPanelInput.attach(drop.urls[0])
} }
} }
ImageClipboard {
id: imageClipboard
}
Popup {
anchors.centerIn: parent
id: attachDialog
padding: 16
contentItem: RowLayout {
Control {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
contentItem: ColumnLayout {
spacing: 16
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
icon: "\ue2c8"
font.pixelSize: 64
color: MPalette.lighter
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Choose local file"
}
}
background: RippleEffect {
onClicked: {
attachDialog.close()
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
roomPanelInput.attach(path)
})
fileDialog.open()
}
}
}
Rectangle {
Layout.preferredWidth: 1
Layout.fillHeight: true
color: MPalette.banner
}
Control {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
contentItem: ColumnLayout {
spacing: 16
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
icon: "\ue410"
font.pixelSize: 64
color: MPalette.lighter
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Clipboard image"
color: MPalette.foreground
}
}
background: RippleEffect {
onClicked: {
var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
imageClipboard.saveImage(localPath)
roomPanelInput.attach(localPath)
attachDialog.close()
}
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -5,6 +5,7 @@ import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0 import Spectral.Component.Emoji 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0 import Spectral.Effect 2.0
import Spectral.Setting 0.1 import Spectral.Setting 0.1
@ -21,6 +22,9 @@ Control {
property int autoCompleteBeginPosition property int autoCompleteBeginPosition
property int autoCompleteEndPosition property int autoCompleteEndPosition
property bool hasAttachment: false
property url attachmentPath
id: root id: root
padding: 0 padding: 0
@ -171,13 +175,13 @@ Control {
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
id: uploadButton id: uploadButton
visible: !isReply visible: !isReply && !hasAttachment
contentItem: MaterialIcon { contentItem: MaterialIcon {
icon: "\ue226" icon: "\ue226"
} }
onClicked: currentRoom.chooseAndUploadFile() onClicked: attachDialog.open()
BusyIndicator { BusyIndicator {
anchors.fill: parent anchors.fill: parent
@ -202,6 +206,51 @@ Control {
onClicked: clearReply() onClicked: clearReply()
} }
Control {
Layout.margins: 6
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignVCenter
visible: hasAttachment
rightPadding: 8
background: Rectangle {
color: MPalette.accent
radius: height / 2
antialiasing: true
}
contentItem: RowLayout {
spacing: 0
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
id: cancelAttachmentButton
contentItem: MaterialIcon {
icon: "\ue5cd"
color: "white"
font.pixelSize: 18
}
onClicked: {
hasAttachment = false
attachmentPath = ""
}
}
Label {
Layout.alignment: Qt.AlignVCenter
text: attachmentPath != "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
color: "white"
}
}
}
TextArea { TextArea {
property real progress: 0 property real progress: 0
@ -307,6 +356,12 @@ Control {
if (text.trim().length === 0) { return } if (text.trim().length === 0) { return }
if(!currentRoom) { return } if(!currentRoom) { return }
if (hasAttachment) {
currentRoom.uploadFile(attachmentPath, text)
clearAttachment()
return
}
var PREFIX_ME = '/me ' var PREFIX_ME = '/me '
var PREFIX_NOTICE = '/notice ' var PREFIX_NOTICE = '/notice '
var PREFIX_RAINBOW = '/rainbow ' var PREFIX_RAINBOW = '/rainbow '
@ -372,6 +427,10 @@ Control {
} }
} }
ImageClipboard {
id: imageClipboard
}
function insert(str) { function insert(str) {
inputField.insert(inputField.cursorPosition, str) inputField.insert(inputField.cursorPosition, str)
} }
@ -396,4 +455,14 @@ Control {
autoCompleteListView.visible = false autoCompleteListView.visible = false
emojiPicker.visible = false emojiPicker.visible = false
} }
function attach(localPath) {
hasAttachment = true
attachmentPath = localPath
}
function clearAttachment() {
hasAttachment = false
attachmentPath = ""
}
} }

View File

@ -56,12 +56,17 @@ ApplicationWindow {
quitOnLastWindowClosed: !MSettings.showTray quitOnLastWindowClosed: !MSettings.showTray
onErrorOccured: errorControl.show(error + ": " + detail, 3000)
}
NotificationsManager {
id: notificationsManager
onNotificationClicked: { onNotificationClicked: {
roomListForm.enteredRoom = spectralController.connection.room(roomId) roomListForm.enteredRoom = spectralController.connection.room(roomId)
roomForm.goToEvent(eventId) roomForm.goToEvent(eventId)
showWindow() showWindow()
} }
onErrorOccured: errorControl.show(error + ": " + detail, 3000)
} }
Shortcut { Shortcut {

View File

@ -42,7 +42,9 @@ HEADERS += \
include/hoedown/escape.h \ include/hoedown/escape.h \
include/hoedown/html.h \ include/hoedown/html.h \
include/hoedown/stack.h \ include/hoedown/stack.h \
include/hoedown/version.h include/hoedown/version.h \
src/imageclipboard.h \
src/matriximageprovider.h
SOURCES += \ SOURCES += \
include/hoedown/autolink.c \ include/hoedown/autolink.c \
@ -53,7 +55,9 @@ SOURCES += \
include/hoedown/html_blocks.c \ include/hoedown/html_blocks.c \
include/hoedown/html_smartypants.c \ include/hoedown/html_smartypants.c \
include/hoedown/stack.c \ include/hoedown/stack.c \
include/hoedown/version.c include/hoedown/version.c \
src/imageclipboard.cpp \
src/matriximageprovider.cpp
# The following define makes your compiler emit warnings if you use # The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings # any feature of Qt which as been marked deprecated (the exact warnings
@ -117,7 +121,6 @@ mac {
HEADERS += \ HEADERS += \
src/controller.h \ src/controller.h \
src/roomlistmodel.h \ src/roomlistmodel.h \
src/imageprovider.h \
src/messageeventmodel.h \ src/messageeventmodel.h \
src/emojimodel.h \ src/emojimodel.h \
src/spectralroom.h \ src/spectralroom.h \
@ -130,7 +133,6 @@ HEADERS += \
SOURCES += src/main.cpp \ SOURCES += src/main.cpp \
src/controller.cpp \ src/controller.cpp \
src/roomlistmodel.cpp \ src/roomlistmodel.cpp \
src/imageprovider.cpp \
src/messageeventmodel.cpp \ src/messageeventmodel.cpp \
src/emojimodel.cpp \ src/emojimodel.cpp \
src/spectralroom.cpp \ src/spectralroom.cpp \

View File

@ -31,13 +31,9 @@
#include <QtNetwork/QAuthenticator> #include <QtNetwork/QAuthenticator>
#include <QtNetwork/QNetworkReply> #include <QtNetwork/QNetworkReply>
Controller::Controller(QObject* parent) Controller::Controller(QObject* parent) : QObject(parent) {
: QObject(parent), notificationsManager(this) {
QApplication::setQuitOnLastWindowClosed(false); QApplication::setQuitOnLastWindowClosed(false);
connect(&notificationsManager, &NotificationsManager::notificationClicked,
this, &Controller::notificationClicked);
Connection::setRoomType<SpectralRoom>(); Connection::setRoomType<SpectralRoom>();
Connection::setUserType<SpectralUser>(); Connection::setUserType<SpectralUser>();
@ -232,10 +228,6 @@ void Controller::createDirectChat(Connection* c, const QString& userID) {
}); });
} }
void Controller::copyToClipboard(const QString& text) {
m_clipboard->setText(text);
}
void Controller::playAudio(QUrl localFile) { void Controller::playAudio(QUrl localFile) {
QMediaPlayer* player = new QMediaPlayer; QMediaPlayer* player = new QMediaPlayer;
player->setMedia(localFile); player->setMedia(localFile);
@ -243,16 +235,6 @@ void Controller::playAudio(QUrl localFile) {
connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); });
} }
void Controller::postNotification(const QString& roomId,
const QString& eventId,
const QString& roomName,
const QString& senderName,
const QString& text,
const QImage& icon) {
notificationsManager.postNotification(roomId, eventId, roomName, senderName,
text, icon);
}
int Controller::dpi() { int Controller::dpi() {
return SettingsGroup("Interface").value("dpi", 100).toInt(); return SettingsGroup("Interface").value("dpi", 100).toInt();
} }

View File

@ -67,8 +67,6 @@ class Controller : public QObject {
} }
private: private:
QClipboard* m_clipboard = QApplication::clipboard();
NotificationsManager notificationsManager;
QVector<Connection*> m_connections; QVector<Connection*> m_connections;
QPointer<Connection> m_connection; QPointer<Connection> m_connection;
@ -99,14 +97,7 @@ class Controller : public QObject {
void joinRoom(Connection* c, const QString& alias); void joinRoom(Connection* c, const QString& alias);
void createRoom(Connection* c, const QString& name, const QString& topic); void createRoom(Connection* c, const QString& name, const QString& topic);
void createDirectChat(Connection* c, const QString& userID); void createDirectChat(Connection* c, const QString& userID);
void copyToClipboard(const QString& text);
void playAudio(QUrl localFile); void playAudio(QUrl localFile);
void postNotification(const QString& roomId,
const QString& eventId,
const QString& roomName,
const QString& senderName,
const QString& text,
const QImage& icon);
}; };
#endif // CONTROLLER_H #endif // CONTROLLER_H

26
src/imageclipboard.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "imageclipboard.h"
#include <QGuiApplication>
#include <QUrl>
ImageClipboard::ImageClipboard(QObject* parent)
: QObject(parent), m_clipboard(QGuiApplication::clipboard()) {
connect(m_clipboard, &QClipboard::changed, this,
&ImageClipboard::imageChanged);
}
bool ImageClipboard::hasImage() {
return !image().isNull();
}
QImage ImageClipboard::image() {
return m_clipboard->image();
}
void ImageClipboard::saveImage(const QUrl& localPath) {
auto i = image();
if (i.isNull()) return;
i.save(localPath.toString());
}

29
src/imageclipboard.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef IMAGECLIPBOARD_H
#define IMAGECLIPBOARD_H
#include <QClipboard>
#include <QImage>
#include <QObject>
class ImageClipboard : public QObject {
Q_OBJECT
Q_PROPERTY(bool hasImage READ hasImage NOTIFY imageChanged)
Q_PROPERTY(QImage image READ image NOTIFY imageChanged)
public:
explicit ImageClipboard(QObject* parent = nullptr);
bool hasImage();
QImage image();
private:
QClipboard* m_clipboard;
signals:
void imageChanged();
public slots:
void saveImage(const QUrl& localPath);
};
#endif // IMAGECLIPBOARD_H

View File

@ -8,8 +8,10 @@
#include "accountlistmodel.h" #include "accountlistmodel.h"
#include "controller.h" #include "controller.h"
#include "emojimodel.h" #include "emojimodel.h"
#include "imageprovider.h" #include "imageclipboard.h"
#include "matriximageprovider.h"
#include "messageeventmodel.h" #include "messageeventmodel.h"
#include "notifications/manager.h"
#include "room.h" #include "room.h"
#include "roomlistmodel.h" #include "roomlistmodel.h"
#include "spectralroom.h" #include "spectralroom.h"
@ -55,6 +57,9 @@ int main(int argc, char* argv[]) {
qmlRegisterType<UserListModel>("Spectral", 0, 1, "UserListModel"); qmlRegisterType<UserListModel>("Spectral", 0, 1, "UserListModel");
qmlRegisterType<MessageEventModel>("Spectral", 0, 1, "MessageEventModel"); qmlRegisterType<MessageEventModel>("Spectral", 0, 1, "MessageEventModel");
qmlRegisterType<EmojiModel>("Spectral", 0, 1, "EmojiModel"); qmlRegisterType<EmojiModel>("Spectral", 0, 1, "EmojiModel");
qmlRegisterType<NotificationsManager>("Spectral", 0, 1,
"NotificationsManager");
qmlRegisterType<ImageClipboard>("Spectral", 0, 1, "ImageClipboard");
qmlRegisterUncreatableType<RoomMessageEvent>("Spectral", 0, 1, qmlRegisterUncreatableType<RoomMessageEvent>("Spectral", 0, 1,
"RoomMessageEvent", "ENUM"); "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<RoomType>("Spectral", 0, 1, "RoomType", "ENUM"); qmlRegisterUncreatableType<RoomType>("Spectral", 0, 1, "RoomType", "ENUM");
@ -72,9 +77,10 @@ int main(int argc, char* argv[]) {
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.addImportPath("qrc:/imports"); engine.addImportPath("qrc:/imports");
ImageProvider* m_provider = new ImageProvider(); MatrixImageProvider* matrixImageProvider = new MatrixImageProvider();
engine.rootContext()->setContextProperty("imageProvider", m_provider); engine.rootContext()->setContextProperty("imageProvider",
engine.addImageProvider(QLatin1String("mxc"), m_provider); matrixImageProvider);
engine.addImageProvider(QLatin1String("mxc"), matrixImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
if (engine.rootObjects().isEmpty()) if (engine.rootObjects().isEmpty())

View File

@ -1,4 +1,4 @@
#include "imageprovider.h" #include "matriximageprovider.h"
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
@ -108,7 +108,7 @@ void ThumbnailResponse::cancel() {
Qt::QueuedConnection); Qt::QueuedConnection);
} }
QQuickImageResponse* ImageProvider::requestImageResponse( QQuickImageResponse* MatrixImageProvider::requestImageResponse(
const QString& id, const QString& id,
const QSize& requestedSize) { const QSize& requestedSize) {
return new ThumbnailResponse(m_connection.load(), id, requestedSize); return new ThumbnailResponse(m_connection.load(), id, requestedSize);

View File

@ -1,5 +1,5 @@
#ifndef IMAGEPROVIDER_H #ifndef MatrixImageProvider_H
#define IMAGEPROVIDER_H #define MatrixImageProvider_H
#pragma once #pragma once
#include <QtQuick/QQuickAsyncImageProvider> #include <QtQuick/QQuickAsyncImageProvider>
@ -43,12 +43,12 @@ class ThumbnailResponse : public QQuickImageResponse {
void cancel() override; void cancel() override;
}; };
class ImageProvider : public QObject, public QQuickAsyncImageProvider { class MatrixImageProvider : public QObject, public QQuickAsyncImageProvider {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE
setConnection NOTIFY connectionChanged) setConnection NOTIFY connectionChanged)
public: public:
explicit ImageProvider() = default; explicit MatrixImageProvider() = default;
QQuickImageResponse* requestImageResponse( QQuickImageResponse* requestImageResponse(
const QString& id, const QString& id,
@ -67,4 +67,4 @@ class ImageProvider : public QObject, public QQuickAsyncImageProvider {
QAtomicPointer<QMatrixClient::Connection> m_connection; QAtomicPointer<QMatrixClient::Connection> m_connection;
}; };
#endif // IMAGEPROVIDER_H #endif // MatrixImageProvider_H

View File

@ -19,11 +19,7 @@ struct roomEventId {
class NotificationsManager : public QObject { class NotificationsManager : public QObject {
Q_OBJECT Q_OBJECT
public: public:
NotificationsManager(QObject *parent = nullptr); NotificationsManager(QObject* parent = nullptr);
void postNotification(const QString &roomId, const QString &eventId,
const QString &roomName, const QString &senderName,
const QString &text, const QImage &icon);
signals: signals:
void notificationClicked(const QString roomId, const QString eventId); void notificationClicked(const QString roomId, const QString eventId);
@ -31,7 +27,8 @@ class NotificationsManager : public QObject {
private: private:
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
QDBusInterface dbus; QDBusInterface dbus;
uint showNotification(const QString summary, const QString text, uint showNotification(const QString summary,
const QString text,
const QImage image); const QImage image);
#endif #endif
@ -43,9 +40,16 @@ class NotificationsManager : public QObject {
public slots: public 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 postNotification(const QString& roomId,
const QString& eventId,
const QString& roomName,
const QString& senderName,
const QString& text,
const QImage& icon);
}; };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image); QDBusArgument& operator<<(QDBusArgument& arg, const QImage& image);
const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &); const QDBusArgument& operator>>(const QDBusArgument& arg, QImage&);
#endif #endif

View File

@ -44,18 +44,11 @@ inline QSize getImageSize(const QUrl& imageUrl) {
return reader.size(); return reader.size();
} }
void SpectralRoom::chooseAndUploadFile() { void SpectralRoom::uploadFile(const QUrl& url, const QString& body) {
auto localFile = QFileDialog::getOpenFileUrl(Q_NULLPTR, tr("Save File as"));
if (!localFile.isEmpty()) {
uploadFile(localFile);
}
}
void SpectralRoom::uploadFile(const QUrl& url) {
if (url.isEmpty()) if (url.isEmpty())
return; return;
QString txnID = postFile(url.fileName(), url, false); QString txnID = postFile(body.isEmpty() ? url.fileName() : body, url, false);
setHasFileUploading(true); setHasFileUploading(true);
connect(this, &Room::fileTransferCompleted, connect(this, &Room::fileTransferCompleted,
[=](QString id, QUrl localFile, QUrl mxcUrl) { [=](QString id, QUrl localFile, QUrl mxcUrl) {

View File

@ -254,8 +254,7 @@ class SpectralRoom : public Room {
void fileUploadingProgressChanged(); void fileUploadingProgressChanged();
public slots: public slots:
void chooseAndUploadFile(); void uploadFile(const QUrl& url, const QString& body = "");
void uploadFile(const QUrl& url);
void acceptInvitation(); void acceptInvitation();
void forget(); void forget();
void sendTypingNotification(bool isTyping); void sendTypingNotification(bool isTyping);