Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
2b1b21dd07 Add spell checking hint to ChatBar again 2023-01-05 21:52:52 -05:00
119 changed files with 26158 additions and 33377 deletions

1
.gitignore vendored
View File

@@ -9,4 +9,3 @@ compile_commands.json
kate.project.ctags.* kate.project.ctags.*
*.user *.user
.flatpak-builder/ .flatpak-builder/
.idea/

View File

@@ -6,14 +6,11 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(PROJECT_VERSION "23.01") project(NeoChat)
project(NeoChat VERSION ${PROJECT_VERSION}) set(PROJECT_VERSION "22.11")
set(KF5_MIN_VERSION "5.91.0") set(KF5_MIN_VERSION "5.91.0")
set(QT_MIN_VERSION "5.15.2") set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.8")
endif()
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -125,8 +122,6 @@ set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL TYPE OPTIONAL
) )
find_package(Sqlite3)
if(NOT Quotient_VERSION_MINOR GREATER 6) if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD) cmake_policy(SET CMP0063 OLD)
endif() endif()

0
LICENSES/MIT.txt Normal file → Executable file
View File

View File

@@ -14,8 +14,7 @@
android:name="org.qtproject.qt5.android.bindings.QtActivity" android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat" android:label="NeoChat"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop" android:launchMode="singleTop">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
@@ -23,6 +22,7 @@
</intent-filter> </intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/> <meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/> <meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/> <meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/> <meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
@@ -38,6 +38,8 @@
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/> <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/> <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps --> <!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/> <meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Splash screen --> <!-- Splash screen -->

View File

@@ -12,7 +12,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.2' classpath 'com.android.tools.build:gradle:3.6.4'
} }
} }
@@ -73,10 +73,6 @@ android {
defaultConfig { defaultConfig {
minSdkVersion qtMinSdkVersion minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion targetSdkVersion qtTargetSdkVersion
applicationId "org.kde.neochat"
namespace "org.kde.neochat"
versionCode timestamp
versionName projectVersionFull
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp] manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
} }

View File

@@ -231,20 +231,6 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="23.01" date="2023-01-30">
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
<description>
<p>New features and bugfixes:</p>
<ul>
<li>Notifications will now be shown for all accounts, not just the active one</li>
<li>There is a new "compact" mode for the room list</li>
<li>You can now search in the room history</li>
<li>Emojis and Reactions have been significantly improved</li>
<li>Fixed several crashes around user invitations</li>
<li>Room permission settings can now be configured</li>
</ul>
</description>
</release>
<release version="22.11" date="2022-11-30"> <release version="22.11" date="2022-11-30">
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url> <url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
</release> </release>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,47 +6,47 @@
add_library(neochat STATIC add_library(neochat STATIC
controller.cpp controller.cpp
actionshandler.cpp actionshandler.cpp
models/emojimodel.cpp emojimodel.cpp
emojitones.cpp emojitones.cpp
models/customemojimodel.cpp customemojimodel.cpp
clipboard.cpp clipboard.cpp
matriximageprovider.cpp matriximageprovider.cpp
models/messageeventmodel.cpp messageeventmodel.cpp
models/messagefiltermodel.cpp messagefiltermodel.cpp
models/roomlistmodel.cpp roomlistmodel.cpp
models/sortfilterspacelistmodel.cpp sortfilterspacelistmodel.cpp
spacehierarchycache.cpp spacehierarchycache.cpp
roommanager.cpp roommanager.cpp
neochatroom.cpp neochatroom.cpp
neochatuser.cpp neochatuser.cpp
models/userlistmodel.cpp userlistmodel.cpp
models/userfiltermodel.cpp userfiltermodel.cpp
models/publicroomlistmodel.cpp publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp keywordnotificationrulemodel.cpp
utils.cpp utils.cpp
notificationsmanager.cpp notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp chatdocumenthandler.cpp
models/devicesmodel.cpp devicesmodel.cpp
filetypesingleton.cpp filetypesingleton.cpp
login.cpp login.cpp
stickerevent.cpp stickerevent.cpp
models/webshortcutmodel.cpp webshortcutmodel.cpp
blurhash.cpp blurhash.cpp
blurhashimageprovider.cpp blurhashimageprovider.cpp
joinrulesevent.cpp joinrulesevent.cpp
models/collapsestateproxymodel.cpp collapsestateproxymodel.cpp
urlhelper.cpp urlhelper.cpp
windowcontroller.cpp windowcontroller.cpp
linkpreviewer.cpp linkpreviewer.cpp
models/completionmodel.cpp completionmodel.cpp
models/completionproxymodel.cpp completionproxymodel.cpp
models/actionsmodel.cpp actionsmodel.cpp
models/serverlistmodel.cpp serverlistmodel.cpp
models/statemodel.cpp statemodel.cpp
filetransferpseudojob.cpp filetransferpseudojob.cpp
models/searchmodel.cpp searchmodel.cpp
) )
add_executable(neochat-app add_executable(neochat-app
@@ -103,9 +103,6 @@ endif()
if(ANDROID) if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc) target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL) target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
if(SQLite3_FOUND)
target_link_libraries(neochat-app PRIVATE SQLite::SQLite3)
endif()
target_sources(neochat-app PRIVATE notifyrc.qrc) target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL) target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS kirigami_package_breeze_icons(ICONS

View File

@@ -13,9 +13,9 @@
#include <KLocalizedString> #include <KLocalizedString>
#include <QStringBuilder> #include <QStringBuilder>
#include "actionsmodel.h"
#include "controller.h" #include "controller.h"
#include "models/actionsmodel.h" #include "customemojimodel.h"
#include "models/customemojimodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "roommanager.h" #include "roommanager.h"

View File

@@ -158,12 +158,11 @@ QVector<ActionsModel::Action> actions{
return QString(); return QString();
} }
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text); if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString(); return QString();
} }
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) { if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Ban) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString(); return QString();
} }

View File

@@ -14,9 +14,9 @@
#include <Sonnet/BackgroundChecker> #include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings> #include <Sonnet/Settings>
#include "models/actionsmodel.h" #include "actionsmodel.h"
#include "models/roomlistmodel.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "roomlistmodel.h"
class SyntaxHighlighter : public QSyntaxHighlighter class SyntaxHighlighter : public QSyntaxHighlighter
{ {
@@ -105,7 +105,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
{ {
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() { connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room); m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr; static NeoChatRoom *previousRoom = nullptr;
if (previousRoom) { if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr); disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
} }

View File

@@ -7,8 +7,8 @@
#include <QQuickTextDocument> #include <QQuickTextDocument>
#include <QTextCursor> #include <QTextCursor>
#include "models/completionmodel.h" #include "completionmodel.h"
#include "models/userlistmodel.h" #include "userlistmodel.h"
class QTextDocument; class QTextDocument;
class NeoChatRoom; class NeoChatRoom;

View File

@@ -33,14 +33,13 @@ QImage Clipboard::image() const
QString Clipboard::saveImage(QString localPath) const QString Clipboard::saveImage(QString localPath) const
{ {
QString imageDir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))); if (!QDir().exists(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)))) {
QDir().mkdir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
if (!QDir().exists(imageDir)) {
QDir().mkdir(imageDir);
} }
if (localPath.isEmpty()) { if (localPath.isEmpty()) {
localPath = QStringLiteral("file://%1/%2.png").arg(imageDir, QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss"))); localPath = QStringLiteral("file://%1/screenshots/%2.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
} }
QUrl url(localPath); QUrl url(localPath);
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
@@ -52,11 +51,14 @@ QString Clipboard::saveImage(QString localPath) const
return {}; return {};
} }
if (image.save(url.toLocalFile())) { QDir dir;
return localPath; if (!dir.exists(QFileInfo(url.fileName()).absoluteFilePath())) {
} else { dir.mkpath(QFileInfo(url.fileName()).absoluteFilePath());
return {};
} }
image.save(url.toLocalFile());
return localPath;
} }
void Clipboard::saveText(QString message) void Clipboard::saveText(QString message)

View File

@@ -128,7 +128,16 @@ Controller::Controller(QObject *parent)
if (AccountRegistry::instance().size() > oldAccountCount) { if (AccountRegistry::instance().size() > oldAccountCount) {
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1]; auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
connect(connection, &Connection::syncDone, this, [=]() { connect(connection, &Connection::syncDone, this, [=]() {
handleNotifications(connection); bool changes = false;
for (const auto &room : connection->allRooms()) {
if (m_notificationCounts[room] != room->unreadStats().notableCount) {
m_notificationCounts[room] = room->unreadStats().notableCount;
changes = true;
}
}
if (changes) {
handleNotifications();
}
}); });
} }
oldAccountCount = AccountRegistry::instance().size(); oldAccountCount = AccountRegistry::instance().size();
@@ -137,16 +146,19 @@ Controller::Controller(QObject *parent)
} }
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
void Controller::handleNotifications(QPointer<Quotient::Connection> connection) void Controller::handleNotifications()
{ {
static QStringList initial; static bool initial = true;
static QStringList oldNotifications; static QStringList oldNotifications;
auto job = connection->callApi<GetNotificationsJob>(); if (!m_connection) {
return;
}
auto job = m_connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [job, connection]() { connect(job, &BaseJob::success, this, [this, job]() {
const auto notifications = job->jsonData()["notifications"].toArray(); const auto notifications = job->jsonData()["notifications"].toArray();
if (!initial.contains(connection->user()->id())) { if (initial) {
initial.append(connection->user()->id()); initial = false;
for (const auto &n : notifications) { for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString(); oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
} }
@@ -162,7 +174,7 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
continue; continue;
} }
oldNotifications += notification["event"].toObject()["event_id"].toString(); oldNotifications += notification["event"].toObject()["event_id"].toString();
auto room = connection->room(notification["room_id"].toString()); auto room = m_connection->room(notification["room_id"].toString());
// If room exists, room is NOT active OR the application is NOT active, show notification // If room exists, room is NOT active OR the application is NOT active, show notification
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) { if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
@@ -184,7 +196,7 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
if (notification["event"]["type"] == "m.room.encrypted") { if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED #ifdef Quotient_E2EE_ENABLED
auto decrypted = connection->decryptNotification(notification); auto decrypted = m_connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString(); body = decrypted["content"].toObject()["body"].toString();
#endif #endif
if (body.isEmpty()) { if (body.isEmpty()) {
@@ -369,11 +381,6 @@ void Controller::invokeLogin()
if (error == "Unrecognised access token") { if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked")); Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false); logout(connection, false);
} else if (error == "Connection closed") {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else { } else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error)); Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true); logout(connection, true);

View File

@@ -119,7 +119,7 @@ private:
bool hasWindowSystem() const; bool hasWindowSystem() const;
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
void handleNotifications(QPointer<Quotient::Connection> connection); void handleNotifications();
#endif #endif
private Q_SLOTS: private Q_SLOTS:

View File

@@ -4,8 +4,8 @@
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QRegularExpression>
#include <memory> #include <memory>
#include <QRegularExpression>
struct CustomEmoji { struct CustomEmoji {
QString name; // with :semicolons: QString name; // with :semicolons:

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#include "emojitones.h" #include "emojitones.h"
#include "models/emojimodel.h" #include "emojimodel.h"
QMultiHash<QString, QVariant> EmojiTones::_tones = { QMultiHash<QString, QVariant> EmojiTones::_tones = {
#include "emojitones_data.h" #include "emojitones_data.h"

View File

@@ -42,39 +42,39 @@
#include "blurhashimageprovider.h" #include "blurhashimageprovider.h"
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
#include "clipboard.h" #include "clipboard.h"
#include "collapsestateproxymodel.h"
#include "controller.h" #include "controller.h"
#include "customemojimodel.h"
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h" #include "filetypesingleton.h"
#include "joinrulesevent.h" #include "joinrulesevent.h"
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "keywordnotificationrulemodel.h"
#include "login.h" #include "login.h"
#include "matriximageprovider.h" #include "matriximageprovider.h"
#include "models/collapsestateproxymodel.h" #include "messageeventmodel.h"
#include "models/customemojimodel.h" #include "messagefiltermodel.h"
#include "models/devicesmodel.h"
#include "models/emojimodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "searchmodel.h"
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
#include "pollhandler.h" #include "pollhandler.h"
#endif #endif
#include "publicroomlistmodel.h"
#include "roomlistmodel.h"
#include "roommanager.h" #include "roommanager.h"
#include "serverlistmodel.h"
#include "sortfilterroomlistmodel.h"
#include "sortfilterspacelistmodel.h"
#include "spacehierarchycache.h" #include "spacehierarchycache.h"
#include "urlhelper.h" #include "urlhelper.h"
#include "userdirectorylistmodel.h"
#include "userfiltermodel.h"
#include "userlistmodel.h"
#include "webshortcutmodel.h"
#include "windowcontroller.h" #include "windowcontroller.h"
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
#include <keyverificationsession.h> #include <keyverificationsession.h>
@@ -82,9 +82,9 @@
#ifdef HAVE_COLORSCHEME #ifdef HAVE_COLORSCHEME
#include "colorschemer.h" #include "colorschemer.h"
#endif #endif
#include "models/completionmodel.h" #include "completionmodel.h"
#include "models/statemodel.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "statemodel.h"
#ifdef HAVE_RUNNER #ifdef HAVE_RUNNER
#include "runner.h" #include "runner.h"

View File

@@ -439,7 +439,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
case EventTypeRole: case EventTypeRole:
return DelegateType::ReadMarker; return DelegateType::ReadMarker;
case TimeRole: { case TimeRole: {
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime(); const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
const KFormat format; const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
} }

View File

@@ -25,6 +25,10 @@
<label>Background transparency value</label> <label>Background transparency value</label>
<default>0.3</default> <default>0.3</default>
</entry> </entry>
<entry name="ShowNotifications" type="bool">
<label>Show notifications</label>
<default>true</default>
</entry>
<entry name="MergeRoomList" type="bool"> <entry name="MergeRoomList" type="bool">
<label>Merge Room Lists</label> <label>Merge Room Lists</label>
<default>false</default> <default>false</default>

View File

@@ -11,12 +11,6 @@
#include <KNotification> #include <KNotification>
#include <KNotificationReplyAction> #include <KNotificationReplyAction>
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <connection.h> #include <connection.h>
#include <csapi/pushrules.h> #include <csapi/pushrules.h>
#include <jobs/basejob.h> #include <jobs/basejob.h>
@@ -55,6 +49,10 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &replyEventId, const QString &replyEventId,
bool canReply) bool canReply)
{ {
if (!NeoChatConfig::self()->showNotifications()) {
return;
}
QPixmap img; QPixmap img;
img.convertFromImage(icon); img.convertFromImage(icon);
KNotification *notification = new KNotification("message"); KNotification *notification = new KNotification("message");
@@ -70,15 +68,8 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setDefaultAction(i18n("Open NeoChat in this room")); notification->setDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [=]() { connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
#ifdef QUOTIENT_07
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
#else
Controller::instance().setActiveConnection(AccountRegistry::instance().get(room->localUser()->id()));
#endif
}
RoomManager::instance().enterRoom(room); RoomManager::instance().enterRoom(room);
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
}); });
if (canReply) { if (canReply) {
@@ -99,6 +90,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon) void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
{ {
if (!NeoChatConfig::self()->showNotifications()) {
return;
}
QPixmap img; QPixmap img;
img.convertFromImage(icon); img.convertFromImage(icon);
KNotification *notification = new KNotification("invite"); KNotification *notification = new KNotification("invite");
@@ -108,9 +102,9 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
notification->setFlags(KNotification::Persistent); notification->setFlags(KNotification::Persistent);
notification->setDefaultAction(i18n("Open this invitation in NeoChat")); notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
connect(notification, &KNotification::defaultActivated, this, [=]() { connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
notification->close(); notification->close();
RoomManager::instance().enterRoom(room); RoomManager::instance().enterRoom(room);
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
}); });
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")}); notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
connect(notification, &KNotification::action1Activated, this, [room, notification]() { connect(notification, &KNotification::action1Activated, this, [room, notification]() {
@@ -277,9 +271,7 @@ void NotificationsManager::updateNotificationRules(const QString &type)
if (overrideRule["rule_id"] == ".m.rule.master") { if (overrideRule["rule_id"] == ".m.rule.master") {
bool ruleEnabled = overrideRule["enabled"].toBool(); bool ruleEnabled = overrideRule["enabled"].toBool();
m_globalNotificationsEnabled = !ruleEnabled; m_globalNotificationsEnabled = !ruleEnabled;
if (!m_globalNotificationsSet) { NeoChatConfig::self()->setShowNotifications(m_globalNotificationsEnabled);
m_globalNotificationsSet = true;
}
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled); Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
} }

View File

@@ -33,7 +33,6 @@ class NotificationsManager : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged) Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
Q_PROPERTY(bool globalNotificationsSet MEMBER m_globalNotificationsSet NOTIFY globalNotificationsSetChanged)
Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY
oneToOneNotificationActionChanged) oneToOneNotificationActionChanged)
Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE
@@ -74,8 +73,7 @@ private:
QMultiMap<QString, KNotification *> m_notifications; QMultiMap<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations; QHash<QString, QPointer<KNotification>> m_invitations;
bool m_globalNotificationsEnabled = false; bool m_globalNotificationsEnabled;
bool m_globalNotificationsSet = false;
PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown; PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown; PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown; PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown;
@@ -109,7 +107,6 @@ private Q_SLOTS:
Q_SIGNALS: Q_SIGNALS:
void globalNotificationsEnabledChanged(bool newState); void globalNotificationsEnabledChanged(bool newState);
void globalNotificationsSetChanged(bool newState);
void oneToOneNotificationActionChanged(PushNotificationAction::Action action); void oneToOneNotificationActionChanged(PushNotificationAction::Action action);
void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action); void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action);
void groupChatNotificationActionChanged(PushNotificationAction::Action action); void groupChatNotificationActionChanged(PushNotificationAction::Action action);

View File

@@ -60,7 +60,7 @@ Comment[nl]=Rooms zoeken in NeoChat
Comment[pl]=Znajdź pokoje w NeoChat Comment[pl]=Znajdź pokoje w NeoChat
Comment[pt]=Procurar salas no NeoChat Comment[pt]=Procurar salas no NeoChat
Comment[pt_BR]=Encontrar salas no NeoChat Comment[pt_BR]=Encontrar salas no NeoChat
Comment[ru]=Поиск комнат NeoChat Comment[ru]=Поиск комнаты NeoChat
Comment[sl]=Najdi sobe v NeoChatu Comment[sl]=Najdi sobe v NeoChatu
Comment[sv]=Sök efter rum i NeoChat Comment[sv]=Sök efter rum i NeoChat
Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும் Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும்

View File

@@ -10,115 +10,188 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
ColumnLayout { Loader {
id: root id: attachmentPaneLoader
signal attachmentCancelled() readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath)
property string attachmentPath
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix) readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length) readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
RowLayout { active: visible
spacing: Kirigami.Units.smallSpacing sourceComponent: Component {
QQC2.Pane {
id: attachmentPane
Kirigami.Theme.colorSet: Kirigami.Theme.View
QQC2.Label { contentItem: Item {
Layout.fillWidth: true property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing
Layout.alignment: Qt.AlignLeft implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth)
text: i18n("Attachment:") implicitHeight: Math.max(
horizontalAlignment: Text.AlignLeft (hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0)
verticalAlignment: Text.AlignVCenter + fileInfoLayout.implicitHeight,
} toolBar.implicitHeight
QQC2.ToolButton { )
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Component { Image {
id: imageEditorPage id: image
ImageEditorPage { property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8)
imagePath: root.attachmentPath height: preferredHeight
anchors {
horizontalCenter: parent.horizontalCenter
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
width: Math.min(implicitWidth, attachmentPane.availableWidth)
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
onSourceChanged: {
// Reset source size height, which affect implicitHeight
sourceSize.height = -1
}
onSourceSizeChanged: {
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
// This can save a lot of RAM when loading large images.
// It also improves visual quality for large images.
sourceSize.height = Kirigami.Units.gridUnit * 8
}
}
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
QQC2.BusyIndicator {
id: imageBusyIndicator
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
visible: running
running: image.visible && image.progress < 1
}
RowLayout {
id: fileInfoLayout
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: undefined
anchors.bottom: parent.bottom
spacing: parent.spacing
Kirigami.Icon {
id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight)
implicitWidth: implicitHeight
source: attachmentMimetype.iconName
}
QQC2.Label {
id: fileLabel
text: baseFileName
}
states: State {
when: !hasImage
AnchorChanges {
target: fileInfoLayout
anchors.bottom: undefined
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Using a toolbar to get a button spacing consistent with what the QQC2 style normally has
// Also has some accessibility info
QQC2.ToolBar {
id: toolBar
width: parent.width
anchors.top: parent.top
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
Kirigami.Theme.inherit: true
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: RowLayout {
spacing: parent.spacing
QQC2.Label {
Layout.leftMargin: -attachmentPane.leftPadding
Layout.topMargin: -attachmentPane.topPadding
leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding
rightPadding: cancelAttachmentButton.rightPadding + 1
topPadding: cancelAttachmentButton.topPadding + attachmentPane.topPadding
bottomPadding: cancelAttachmentButton.bottomPadding
text: i18n("Attachment:")
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
background: Kirigami.ShadowedRectangle {
property real cornerRadius: cancelAttachmentButton.background.hasOwnProperty("radius") ?
Math.min(cancelAttachmentButton.background.radius, height/2) : 0
corners.bottomLeftRadius: toolBar.mirrored ? cornerRadius : 0
corners.bottomRightRadius: toolBar.mirrored ? 0 : cornerRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.75
}
}
Item {
Layout.fillWidth: true
}
QQC2.ToolButton {
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: attachmentPaneLoader.attachmentPath
}
}
onClicked: {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
attachmentPaneLoader.attachmentPath = newPath;
});
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
id: cancelAttachmentButton
icon.name: "dialog-close"
text: i18n("Cancel sending Image")
display: QQC2.AbstractButton.IconOnly
onClicked: currentRoom.chatBoxAttachmentPath = "";
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
background: null
} }
} }
onClicked: { background: Rectangle {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage); color: Kirigami.Theme.backgroundColor
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
root.attachmentPath = newPath;
});
} }
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
id: cancelAttachmentButton
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Cancel sending attachment")
icon.name: "dialog-close"
onTriggered: attachmentCancelled();
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
Image {
id: image
Layout.alignment: Qt.AlignHCenter
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
source: hasImage ? root.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
onSourceChanged: {
// Reset source size height, which affect implicitHeight
sourceSize.height = -1
}
onSourceSizeChanged: {
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
// This can save a lot of RAM when loading large images.
// It also improves visual quality for large images.
sourceSize.height = Kirigami.Units.gridUnit * 8
}
}
Behavior on height {
NumberAnimation {
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
QQC2.BusyIndicator {
id: imageBusyIndicator
visible: running
running: image.visible && image.progress < 1
}
RowLayout {
id: fileInfoLayout
Layout.alignment: Qt.AlignHCenter
spacing: parent.spacing
Kirigami.Icon {
id: mimetypeIcon
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: attachmentMimetype.iconName
}
QQC2.Label {
id: fileLabel
text: baseFileName
} }
} }
} }

View File

@@ -5,62 +5,206 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Window 2.15
import org.kde.kirigami 2.18 as Kirigami import org.kde.kirigami 2.18 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
QQC2.Control { QQC2.ToolBar {
id: root id: chatBar
property alias inputFieldText: inputField.text
property alias textField: textField property alias textField: inputField
property bool isReplying: currentRoom.chatBoxReplyId.length > 0 property alias cursorPosition: inputField.cursorPosition
property bool isEditing: currentRoom.chatBoxEditId.length > 0
property bool replyPaneVisible: isReplying || isEditing
property NeoChatUser replyUser: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0
signal inputFieldForceActiveFocusTriggered()
signal messageSent() signal messageSent()
property list<Kirigami.Action> actions : [ onInputFieldForceActiveFocusTriggered: {
Kirigami.Action { inputField.forceActiveFocus();
id: attachmentAction // set the cursor to the end of the text
inputField.cursorPosition = inputField.length;
}
property bool isBusy: currentRoom && currentRoom.hasFileUploading position: QQC2.ToolBar.Footer
// Matrix does not allow sending attachments in replies Kirigami.Theme.colorSet: Kirigami.Theme.View
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: { // Using a custom background because some styles like Material
if (Clipboard.hasImage) { // or Fusion might have ugly colors for a TextArea placed inside
attachDialog.open() // of a toolbar. ToolBar is otherwise the closest QQC2 component
} else { // to what we want because of the padding and spacing values.
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) background: Rectangle {
fileDialog.chosen.connect((path) => { color: Kirigami.Theme.backgroundColor
if (!path) { }
return;
} contentItem: RowLayout {
currentRoom.chatBoxAttachmentPath = path; spacing: chatBar.spacing
})
fileDialog.open() QQC2.ScrollView {
} Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: inputField.implicitHeight
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
+ inputField.topPadding + inputField.bottomPadding
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
FontMetrics {
id: fontMetrics
font: inputField.font
} }
tooltip: text QQC2.TextArea {
}, id: inputField
Kirigami.Action { focus: true
id: emojiAction /* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
* Make sure there is no background since we are using the ToolBar background.
*
* This could cause a problem if the QQC2 style was designed around TextArea
* background colors being very different from the QPalette::Base color.
* Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles.
*/
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
property bool isBusy: false leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
topPadding: 0
bottomPadding: 0
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
horizontalAlignment: TextEdit.AlignLeft
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.SpellChecking.enabled: true
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
hoverEnabled: !Kirigami.Settings.tabletMode
selectByMouse: !Kirigami.Settings.tabletMode
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
if (editEvent) {
currentRoom.chatBoxEditId = editEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace && inputField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
}
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
currentRoom.chatBoxText = text
}
}
}
Item {
visible: currentRoom.chatBoxReplyId.length === 0 && (currentRoom.chatBoxAttachmentPath.length === 0 || uploadingBusySpinner.running)
implicitWidth: uploadButton.implicitWidth
implicitHeight: uploadButton.implicitHeight
QQC2.ToolButton {
id: uploadButton
anchors.fill: parent
// Matrix does not allow sending attachments in replies
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (Clipboard.hasImage) {
attachDialog.open()
} else {
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) {
return;
}
currentRoom.chatBoxAttachmentPath = path;
})
fileDialog.open()
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.BusyIndicator {
id: uploadingBusySpinner
anchors.fill: parent
visible: running
running: currentRoom && currentRoom.hasFileUploading
}
}
QQC2.ToolButton {
id: emojiButton
icon.name: "smiley" icon.name: "smiley"
text: i18n("Add an Emoji") text: i18n("Add an Emoji")
displayHint: Kirigami.DisplayHint.IconOnly display: QQC2.AbstractButton.IconOnly
checkable: true checkable: true
onTriggered: { onClicked: {
if (emojiDialog.visible) { if (emojiDialog.visible) {
emojiDialog.close() emojiDialog.close()
} else { } else {
@@ -68,266 +212,29 @@ QQC2.Control {
} }
} }
tooltip: text QQC2.ToolTip.text: text
}, QQC2.ToolTip.visible: hovered
Kirigami.Action { }
id: sendAction
property bool isBusy: false
QQC2.ToolButton {
id: sendButton
icon.name: "document-send" icon.name: "document-send"
text: i18n("Send message") text: i18n("Send message")
displayHint: Kirigami.DisplayHint.IconOnly display: QQC2.AbstractButton.IconOnly
checkable: true
onTriggered: { onClicked: {
root.postMessage() chatBar.postMessage()
} }
tooltip: text QQC2.ToolTip.text: text
} QQC2.ToolTip.visible: hovered
]
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: QQC2.ScrollView {
id: chatBarScrollView
property var textFieldHeight: textField.height
property var visualLeftPadding: (root.width - chatBoxMaxWidth) / 2 - (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing : 0)
property var visualRightPadding: (root.width - chatBoxMaxWidth) / 2 + (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0)
leftPadding: LayoutMirroring.enabled ? visualRightPadding : visualLeftPadding
rightPadding: LayoutMirroring.enabled ? visualLeftPadding : visualRightPadding
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
Behavior on implicitHeight {
NumberAnimation {
id: chatBarHeightAnimation
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutCubic
}
}
QQC2.TextArea{
id: textField
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing)
rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width
placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported)
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
currentRoom.chatBoxText = text
}
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
if (editEvent) {
currentRoom.chatBoxEditId = editEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
}
Loader {
id: paneLoader
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing
anchors.right: parent.right
anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
active: visible
visible: root.replyPaneVisible || root.attachmentPaneVisible
sourceComponent: root.replyPaneVisible ? replyPane : attachmentPane
}
Component {
id: replyPane
ReplyPane {
userName: root.replyUser ? root.replyUser.displayName : ""
userColor: root.replyUser ? root.replyUser.color : ""
userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : ""
isReply: root.isReplying
text: isEditing ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: currentRoom.chatBoxAttachmentPath
onAttachmentCancelled: {
currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus()
}
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
}
/**
* Because of the paneLoader we have to manage the scroll
* position manually or it doesn't keep the cursor visible properly all the time.
*/
function ensureVisible(r) {
// Find the child that is the Flickable created by ScrollView.
let flickable = undefined;
for (var index in children) {
if (children[index] instanceof Flickable) {
flickable = children[index];
}
}
if (flickable) {
if (flickable.contentX >= r.x) {
flickable.contentX = r.x;
} else if (flickable.contentX + width <= r.x + r.width) {
flickable.contentX = r.x + r.width - width;
} if (flickable.contentY >= r.y) {
flickable.contentY = r.y;
} else if (flickable.contentY + height <= r.y + r.height) {
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
}
}
}
}
QQC2.ToolButton {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
visible: root.replyPaneVisible
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus()
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
Row {
id: actionsRow
padding: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
anchors.right: parent.right
property var requiredMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.largeSpacing - 2
Repeater {
model: root.actions
Kirigami.Icon {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
active: actionArea.containsPress
visible: modelData.visible
enabled: modelData.enabled
MouseArea {
id: actionArea
anchors.fill: parent
onClicked: modelData.trigger()
cursorShape: Qt.PointingHandCursor
}
QQC2.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered
QQC2.ToolTip.text: modelData.tooltip
HoverHandler { id: hoverHandler }
QQC2.BusyIndicator {
anchors.fill: parent
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
visible: running
running: modelData.isBusy
}
}
} }
} }
EmojiDialog { EmojiDialog {
id: emojiDialog id: emojiDialog
x: parent.width - implicitWidth x: parent.width - implicitWidth
y: -implicitHeight // - Kirigami.Units.smallSpacing y: -implicitHeight - Kirigami.Units.smallSpacing
modal: false modal: false
includeCustom: true includeCustom: true
@@ -337,10 +244,6 @@ QQC2.Control {
onClosed: if (emojiButton.checked) emojiButton.checked = false onClosed: if (emojiButton.checked) emojiButton.checked = false
} }
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
CompletionMenu { CompletionMenu {
id: completionMenu id: completionMenu
height: implicitHeight height: implicitHeight
@@ -360,34 +263,22 @@ QQC2.Control {
target: currentRoom target: currentRoom
function onChatBoxEditIdChanged() { function onChatBoxEditIdChanged() {
if (currentRoom.chatBoxEditMessage.length > 0) { if (currentRoom.chatBoxEditMessage.length > 0) {
textField.text = currentRoom.chatBoxEditMessage chatBar.inputFieldText = currentRoom.chatBoxEditMessage
} }
} }
} }
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
document: textField.textDocument document: inputField.textDocument
cursorPosition: textField.cursorPosition cursorPosition: inputField.cursorPosition
selectionStart: textField.selectionStart selectionStart: inputField.selectionStart
selectionEnd: textField.selectionEnd selectionEnd: inputField.selectionEnd
Component.onCompleted: { Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler; RoomManager.chatDocumentHandler = documentHandler;
} }
} }
function forceActiveFocus() {
textField.forceActiveFocus();
// set the cursor to the end of the text
textField.cursorPosition = textField.length;
}
function insertText(text) {
let initialCursorPosition = textField.cursorPosition;
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
textField.cursorPosition = initialCursorPosition + text.length
}
function pasteImage() { function pasteImage() {
let localPath = Clipboard.saveImage(); let localPath = Clipboard.saveImage();
@@ -401,7 +292,7 @@ QQC2.Control {
actionsHandler.handleMessage(); actionsHandler.handleMessage();
repeatTimer.stop() repeatTimer.stop()
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
textField.clear(); inputField.clear();
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
messageSent() messageSent()

View File

@@ -5,52 +5,108 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
ColumnLayout { ColumnLayout {
id: chatBox id: chatBox
property alias inputFieldText: chatBar.inputFieldText
signal messageSent() signal messageSent()
property alias chatBar: chatBar
readonly property int extraWidth: width >= Kirigami.Units.gridUnit * 47 ? Math.min((width - Kirigami.Units.gridUnit * 47), Kirigami.Units.gridUnit * 20) : 0
readonly property int chatBoxMaxWidth: Config.compactLayout ? width : Math.min(width, Kirigami.Units.gridUnit * 39 + extraWidth)
spacing: 0 spacing: 0
Kirigami.InlineMessage { Kirigami.Separator {
id: connectionPaneSeparator
visible: connectionPane.visible
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 1 // So we can see the border }
Layout.rightMargin: 1 // So we can see the border
text: i18n("NeoChat is offline. Please check your network connection.") QQC2.Pane {
id: connectionPane
padding: fontMetrics.lineSpacing * 0.25
FontMetrics {
id: fontMetrics
font: networkLabel.font
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
visible: !Controller.isOnline visible: !Controller.isOnline
Layout.fillWidth: true
QQC2.Label {
id: networkLabel
width: parent.width
wrapMode: Text.Wrap
text: i18n("NeoChat is offline. Please check your network connection.")
}
} }
Kirigami.Separator { Kirigami.Separator {
id: replySeparator
visible: replyPane.visible
Layout.fillWidth: true
}
ReplyPane {
id: replyPane
visible: currentRoom.chatBoxReplyId.length > 0 || currentRoom.chatBoxEditId.length > 0
Layout.fillWidth: true
onReplyCancelled: {
chatBox.focusInputField()
}
}
Kirigami.Separator {
id: attachmentSeparator
visible: attachmentPane.visible
Layout.fillWidth: true
}
AttachmentPane {
id: attachmentPane
visible: currentRoom.chatBoxAttachmentPath.length > 0
Layout.fillWidth: true
}
Kirigami.Separator {
id: chatBarSeparator
visible: chatBar.visible
Layout.fillWidth: true Layout.fillWidth: true
} }
ChatBar { ChatBar {
id: chatBar id: chatBar
visible: currentRoom.canSendEvent("m.room.message") visible: currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: implicitHeight + Kirigami.Units.largeSpacing
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding
FontMetrics {
id: chatBarFontMetrics
font: chatBar.textField.font
}
onMessageSent: { onMessageSent: {
chatBox.messageSent(); chatBox.messageSent();
} }
Behavior on implicitHeight {
NumberAnimation {
property: "implicitHeight"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
function insertText(str) {
let index = chatBar.cursorPosition;
chatBox.inputFieldText = inputFieldText.substr(0, chatBar.cursorPosition) + str + inputFieldText.substr(chatBar.cursorPosition);
chatBar.cursorPosition = index + str.length;
}
function focusInputField() {
chatBar.inputFieldForceActiveFocusTriggered()
} }
} }

View File

@@ -10,83 +10,108 @@ import org.kde.kirigami 2.14 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
GridLayout { Loader {
id: root id: replyPane
property string userName property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
property color userColor: Kirigami.Theme.highlightColor
property var userAvatar: ""
property bool isReply
property var text
rows: 3 signal replyCancelled()
columns: 3
rowSpacing: Kirigami.Units.smallSpacing
columnSpacing: Kirigami.Units.largeSpacing
QQC2.Label { active: visible
id: replyLabel sourceComponent: QQC2.Pane {
Layout.fillWidth: true id: replyPane
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 3
topPadding: Kirigami.Units.smallSpacing
text: isReply ? i18n("Replying to:") : i18n("Editing message:") Kirigami.Theme.colorSet: Kirigami.Theme.View
}
Rectangle {
id: verticalBorder
Layout.fillHeight: true spacing: leftPadding
Layout.rowSpan: 2
implicitWidth: Kirigami.Units.smallSpacing contentItem: RowLayout {
color: userColor Layout.fillWidth: true
} spacing: replyPane.spacing
Kirigami.Avatar {
id: replyAvatar
implicitWidth: Kirigami.Units.iconSizes.small FontMetrics {
implicitHeight: Kirigami.Units.iconSizes.small id: fontMetrics
font: textArea.font
}
source: userAvatar Kirigami.Avatar {
name: userName id: avatar
color: userColor Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
} Layout.preferredWidth: Layout.preferredHeight
QQC2.Label { Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
Layout.fillWidth: true source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : ""
Layout.alignment: Qt.AlignLeft name: user ? user.displayName : ""
color: user ? user.color : "transparent"
visible: Boolean(user)
}
color: userColor ColumnLayout {
text: userName id: textContentLayout
elide: Text.ElideRight Layout.alignment: Qt.AlignCenter
} Layout.fillWidth: true
QQC2.TextArea { spacing: fontMetrics.leading
id: textArea QQC2.Label {
Layout.fillWidth: true
textFormat: Text.StyledText
elide: Text.ElideRight
text: {
let heading = "<b>%1</b>"
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
if (currentRoom.chatBoxEditId.length > 0) {
heading = heading.arg(i18n("Editing message:")) + "<br/>"
} else {
heading = heading.arg(i18n("Replying to %1:", userName))
}
Layout.fillWidth: true return heading
Layout.columnSpan: 2 }
}
//TODO edit user mentions
QQC2.ScrollView {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
leftPadding: 0 // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
rightPadding: 0 QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
topPadding: 0
bottomPadding: 0 QQC2.TextArea {
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText id: textArea
selectByMouse: true leftPadding: 0
selectByKeyboard: true rightPadding: 0
readOnly: true topPadding: 0
wrapMode: QQC2.Label.Wrap bottomPadding: 0
textFormat: TextEdit.RichText text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage)
background: Item {} selectByMouse: true
HoverHandler { selectByKeyboard: true
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor readOnly: true
wrapMode: QQC2.Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
} }
TextMetrics { background: Rectangle {
id: replyTextMetrics color: Kirigami.Theme.backgroundColor
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
} }
} }
} }

View File

@@ -16,11 +16,4 @@ Kirigami.LoadingPlaceholder {
QQC2.Label { QQC2.Label {
text: i18n("Please wait. This might take a little while.") text: i18n("Please wait. This might take a little while.")
} }
Connections {
target: Controller
function onInitiated() {
closeDialog()
}
}
} }

View File

@@ -11,13 +11,13 @@ import org.kde.kirigami 2.15 as Kirigami
TextEdit { TextEdit {
id: contentLabel id: contentLabel
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/ readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
readonly property var hasSpoiler: /data-mx-spoiler/g readonly property var hasSpoiler: /data-mx-spoiler/g
property bool isEmote: false property bool isEmote: false
property bool isReplyLabel: false property bool isReplyLabel: false
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g
property string textMessage: model.display.includes("http") property string textMessage: model.display.includes("http")
? model.display.replace(linkRegex, function() { ? model.display.replace(linkRegex, function() {
if (arguments[0].includes("/_matrix/media/r0/download/")) { if (arguments[0].includes("/_matrix/media/r0/download/")) {

View File

@@ -87,7 +87,6 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing) Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height) implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)

View File

@@ -18,10 +18,7 @@ TimelineContainer {
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed
property bool supportStreaming: true property bool supportStreaming: true
readonly property var maxWidth: Kirigami.Units.gridUnit * 30 readonly property int maxWidth: 1000 // TODO messageListView.width
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
readonly property var info: model.content.info
onOpenContextMenu: openFileContext(model, vid) onOpenContextMenu: openFileContext(model, vid)
@@ -39,104 +36,17 @@ TimelineContainer {
innerObject: Video { innerObject: Video {
id: vid id: vid
property var videoWidth: { Layout.maximumWidth: videoDelegate.contentMaxWidth
if (videoDelegate.info && videoDelegate.info.w && videoDelegate.info.w > 0) { Layout.fillWidth: true
return videoDelegate.info.w; Layout.maximumHeight: Kirigami.Units.gridUnit * 15
} else if (metaData.resolution && metaData.resolution.width) { Layout.minimumHeight: Kirigami.Units.gridUnit * 5
return metaData.resolution.width;
} else {
return videoDelegate.contentMaxWidth;
}
}
property var videoHeight: {
if (videoDelegate.info && videoDelegate.info.w && videoDelegate.info.h > 0) {
return videoDelegate.info.h;
} else if (metaData.resolution && metaData.resolution.height) {
return metaData.resolution.height;
} else {
// Default to a 16:9 placeholder
return videoDelegate.contentMaxWidth / 16 * 9;
}
}
readonly property var aspectRatio: videoWidth / videoHeight Layout.preferredWidth: (model.content.info.w === undefined || model.content.info.w > videoDelegate.maxWidth) ? videoDelegate.maxWidth : content.info.w
/** Layout.preferredHeight: model.content.info.w === undefined ? (videoDelegate.maxWidth * 3 / 4) : (model.content.info.w > videoDelegate.maxWidth ? (model.content.info.h / model.content.info.w * videoDelegate.maxWidth) : model.content.info.h)
* Whether the video should be limited by height or width.
* We need to prevent excessively tall as well as excessively wide media.
*
* @note In the case of a tie the media is width limited.
*/
readonly property bool limitWidth: videoWidth >= videoHeight
readonly property size maxSize: { loops: MediaPlayer.Infinite
if (limitWidth) {
let width = Math.min(videoDelegate.contentMaxWidth, videoDelegate.maxWidth);
let height = width / aspectRatio;
return Qt.size(width, height);
} else {
let height = Math.min(videoDelegate.maxHeight, videoDelegate.contentMaxWidth / aspectRatio);
let width = height * aspectRatio;
return Qt.size(width, height);
}
}
Layout.maximumWidth: maxSize.width
Layout.maximumHeight: maxSize.height
Layout.preferredWidth: videoWidth
Layout.preferredHeight: videoHeight
fillMode: VideoOutput.PreserveAspectFit fillMode: VideoOutput.PreserveAspectFit
flushMode: VideoOutput.FirstFrame
states: [
State {
name: "notDownloaded"
when: !model.progressInfo.completed && !model.progressInfo.active
PropertyChanges {
target: noDownloadLabel
visible: true
}
PropertyChanges {
target: mediaThumbnail
visible: true
}
},
State {
name: "downloading"
when: model.progressInfo.active && !model.progressInfo.completed
PropertyChanges {
target: downloadBar
visible: true
}
},
State {
name: "paused"
when: model.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
PropertyChanges {
target: videoControls
stateVisible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: vid.play()
}
},
State {
name: "playing"
when: model.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
PropertyChanges {
target: videoControls
stateVisible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-pause"
onClicked: vid.pause()
}
}
]
onDurationChanged: { onDurationChanged: {
if (!duration) { if (!duration) {
@@ -151,10 +61,9 @@ TimelineContainer {
} }
Image { Image {
id: mediaThumbnail
anchors.fill: parent anchors.fill: parent
visible: false visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
source: model.content.thumbnailMediaId ? "image://mxc/" + model.content.thumbnailMediaId : "" source: model.content.thumbnailMediaId ? "image://mxc/" + model.content.thumbnailMediaId : ""
@@ -162,10 +71,9 @@ TimelineContainer {
} }
QQC2.Label { QQC2.Label {
id: noDownloadLabel
anchors.centerIn: parent anchors.centerIn: parent
visible: false visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white" color: "white"
text: i18n("Video") text: i18n("Video")
font.pixelSize: 16 font.pixelSize: 16
@@ -180,12 +88,11 @@ TimelineContainer {
} }
Rectangle { Rectangle {
id: downloadBar
anchors.fill: parent anchors.fill: parent
visible: false
color: Kirigami.Theme.backgroundColor visible: progressInfo.active && !videoDelegate.downloaded
radius: Kirigami.Units.smallSpacing
color: "#BB000000"
QQC2.ProgressBar { QQC2.ProgressBar {
anchors.centerIn: parent anchors.centerIn: parent
@@ -198,148 +105,6 @@ TimelineContainer {
} }
} }
QQC2.Control {
id: videoControls
property bool stateVisible: false
anchors.bottom: vid.bottom
anchors.left: vid.left
anchors.right: vid.right
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
contentItem: RowLayout {
id: controlRow
QQC2.ToolButton {
id: playButton
}
QQC2.Slider {
Layout.fillWidth: true
from: 0
to: vid.duration
value: vid.position
onMoved: vid.seek(value)
}
QQC2.Label {
text: Controller.formatDuration(vid.position) + "/" + Controller.formatDuration(vid.duration)
}
QQC2.ToolButton {
id: volumeButton
property var unmuteVolume: vid.volume
icon.name: vid.volume <= 0 ? "player-volume-muted" : "player-volume"
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
onClicked: {
if (vid.volume > 0) {
vid.volume = 0
} else {
if (unmuteVolume === 0) {
vid.volume = 1
} else {
vid.volume = unmuteVolume
}
}
}
onHoveredChanged: {
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
videoControlTimer.restart()
volumePopupTimer.restart()
}
}
QQC2.Popup {
id: volumePopup
y: -height
width: volumeButton.width
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
focus: true
padding: Kirigami.Units.smallSpacing
closePolicy: QQC2.Popup.NoAutoClose
QQC2.Slider {
id: volumeSlider
anchors.centerIn: parent
implicitHeight: Kirigami.Units.gridUnit * 7
orientation: Qt.Vertical
padding: 0
from: 0
to: 1
value: vid.volume
onMoved: {
vid.volume = value
volumeButton.unmuteVolume = value
}
onHoveredChanged: {
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
videoControlTimer.restart()
volumePopupTimer.restart()
}
}
}
Timer {
id: volumePopupTimer
interval: 500
}
HoverHandler {
id: volumePopupHoverHandler
onHoveredChanged: {
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
videoControlTimer.restart()
volumePopupTimer.restart()
}
}
}
background: Kirigami.ShadowedRectangle {
radius: 4
color: Kirigami.Theme.backgroundColor
opacity: 0.8
property color borderColor: Kirigami.Theme.textColor
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
border.width: 1
shadow.xOffset: 0
shadow.yOffset: 4
shadow.color: Qt.rgba(0, 0, 0, 0.3)
shadow.size: 8
}
}
}
}
background: Kirigami.ShadowedRectangle {
radius: 4
color: Kirigami.Theme.backgroundColor
opacity: 0.8
property color borderColor: Kirigami.Theme.textColor
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
border.width: 1
shadow.xOffset: 0
shadow.yOffset: 4
shadow.color: Qt.rgba(0, 0, 0, 0.3)
shadow.size: 8
}
}
Timer {
id: videoControlTimer
interval: 1000
}
HoverHandler {
id: videoHoverHandler
onHoveredChanged: {
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
videoControlTimer.restart()
}
}
}
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onTapped: if (vid.supportStreaming || progressInfo.completed) { onTapped: if (vid.supportStreaming || progressInfo.completed) {

View File

@@ -138,7 +138,7 @@ QQC2.ToolBar {
name: Controller.activeConnection.localUser.displayName ?? Controller.activeConnection.localUser.id name: Controller.activeConnection.localUser.displayName ?? Controller.activeConnection.localUser.id
actions.main: Kirigami.Action { actions.main: Kirigami.Action {
text: i18n("Edit this account") text: i18n("Edit this account")
icon.name: "document-edit" iconName: "document-edit"
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), { onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
connection: Controller.activeConnection connection: Controller.activeConnection
}, { }, {

View File

@@ -15,7 +15,7 @@ import org.kde.kirigami 2.14 as Kirigami
*/ */
Kirigami.Action { Kirigami.Action {
id: shareAction id: shareAction
icon.name: "emblem-shared-symbolic" iconName: "emblem-shared-symbolic"
text: i18n("Share") text: i18n("Share")
tooltip: i18n("Share the selected media") tooltip: i18n("Share the selected media")

View File

@@ -34,14 +34,14 @@ Kirigami.Page {
left: Kirigami.Action { left: Kirigami.Action {
id: undoAction id: undoAction
text: i18nc("@action:button Undo modification", "Undo") text: i18nc("@action:button Undo modification", "Undo")
icon.name: "edit-undo" iconName: "edit-undo"
onTriggered: imageDoc.undo(); onTriggered: imageDoc.undo();
visible: imageDoc.edited visible: imageDoc.edited
} }
main: Kirigami.Action { main: Kirigami.Action {
id: okAction id: okAction
text: i18nc("@action:button Accept image modification", "Accept") text: i18nc("@action:button Accept image modification", "Accept")
icon.name: "dialog-ok" iconName: "dialog-ok"
onTriggered: { onTriggered: {
let newPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop(); let newPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop();
if (imageDoc.saveAs(newPath)) {; if (imageDoc.saveAs(newPath)) {;
@@ -121,7 +121,7 @@ Kirigami.Page {
display: QQC2.Button.TextBesideIcon display: QQC2.Button.TextBesideIcon
actions: [ actions: [
Kirigami.Action { Kirigami.Action {
icon.name: rootEditorView.resizing ? "dialog-cancel" : "transform-crop" iconName: rootEditorView.resizing ? "dialog-cancel" : "transform-crop"
text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop"); text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop");
onTriggered: { onTriggered: {
resizeRectangle.width = editImage.paintedWidth resizeRectangle.width = editImage.paintedWidth
@@ -137,31 +137,31 @@ Kirigami.Page {
} }
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "dialog-ok" iconName: "dialog-ok"
visible: rootEditorView.resizing visible: rootEditorView.resizing
text: i18nc("@action:button Crop an image", "Crop"); text: i18nc("@action:button Crop an image", "Crop");
onTriggered: rootEditorView.crop(); onTriggered: rootEditorView.crop();
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "object-rotate-left" iconName: "object-rotate-left"
text: i18nc("@action:button Rotate an image to the left", "Rotate left"); text: i18nc("@action:button Rotate an image to the left", "Rotate left");
onTriggered: imageDoc.rotate(-90); onTriggered: imageDoc.rotate(-90);
visible: !rootEditorView.resizing visible: !rootEditorView.resizing
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "object-rotate-right" iconName: "object-rotate-right"
text: i18nc("@action:button Rotate an image to the right", "Rotate right"); text: i18nc("@action:button Rotate an image to the right", "Rotate right");
onTriggered: imageDoc.rotate(90); onTriggered: imageDoc.rotate(90);
visible: !rootEditorView.resizing visible: !rootEditorView.resizing
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "object-flip-vertical" iconName: "object-flip-vertical"
text: i18nc("@action:button Mirror an image vertically", "Flip"); text: i18nc("@action:button Mirror an image vertically", "Flip");
onTriggered: imageDoc.mirror(false, true); onTriggered: imageDoc.mirror(false, true);
visible: !rootEditorView.resizing visible: !rootEditorView.resizing
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "object-flip-horizontal" iconName: "object-flip-horizontal"
text: i18nc("@action:button Mirror an image horizontally", "Mirror"); text: i18nc("@action:button Mirror an image horizontally", "Mirror");
onTriggered: imageDoc.mirror(true, false); onTriggered: imageDoc.mirror(true, false);
visible: !rootEditorView.resizing visible: !rootEditorView.resizing

View File

@@ -50,7 +50,6 @@ Kirigami.ScrollablePage {
applicationWindow().hoverLinkIndicator.text = ""; applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning(); messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false; hasScrolledUpBefore = false;
chatBox.chatBar.forceActiveFocus();
} }
Connections { Connections {
@@ -138,8 +137,8 @@ Kirigami.ScrollablePage {
switchRoomUp(); switchRoomUp();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { } else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true; event.accepted = true;
chatBox.chatBar.insertText(event.text); chatBox.addText(event.text);
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
return; return;
} }
} }
@@ -262,7 +261,7 @@ Kirigami.ScrollablePage {
maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0 maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0
z: 3 z: 3
visible: messageListView.sectionBannerItem != undefined && messageListView.sectionBannerItem.ListView.section != "" && !Config.blur visible: messageListView.sectionBannerItem != undefined && messageListView.sectionBannerItem.ListView.section != ""
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : "" labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
} }
footerPositioning: ListView.OverlayHeader footerPositioning: ListView.OverlayHeader
@@ -350,7 +349,7 @@ Kirigami.ScrollablePage {
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
messageListView.goToEvent(currentRoom.readMarkerEventId) messageListView.goToEvent(currentRoom.readMarkerEventId)
} }
icon.name: "go-up" icon.name: "go-up"
@@ -374,7 +373,7 @@ Kirigami.ScrollablePage {
visible: !messageListView.atYEnd visible: !messageListView.atYEnd
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
goToLastMessage(); goToLastMessage();
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
} }
@@ -520,14 +519,13 @@ Kirigami.ScrollablePage {
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "preferences-desktop-emoticons" icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open(); onClicked: emojiDialog.open();
EmojiDialog { EmojiDialog {
id: emojiDialog id: emojiDialog
showQuickReaction: true showQuickReaction: true
onChosen: { onChosen: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
} }
} }
} }
@@ -540,7 +538,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxEditId = hoverActions.event.eventId;
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
} }
} }
QQC2.Button { QQC2.Button {
@@ -551,7 +549,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxReplyId = hoverActions.event.eventId; currentRoom.chatBoxReplyId = hoverActions.event.eventId;
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
chatBox.chatBar.forceActiveFocus(); chatBox.focusInputField();
} }
} }
} }

View File

@@ -291,8 +291,7 @@ Kirigami.OverlayDrawer {
room: roomDrawer.room room: roomDrawer.room
} }
sortRole: "powerLevel" sortRole: "perm"
sortOrder: Qt.DescendingOrder
filterRole: "name" filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive filterCaseSensitivity: Qt.CaseInsensitive
} }
@@ -326,9 +325,22 @@ Kirigami.OverlayDrawer {
} }
trailing: QQC2.Label { trailing: QQC2.Label {
visible: powerLevel > 0 visible: perm != UserType.Member
text: powerLevelString text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
default:
return "";
}
}
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText textFormat: Text.PlainText
wrapMode: Text.NoWrap wrapMode: Text.NoWrap

View File

@@ -44,11 +44,10 @@ Kirigami.ScrollablePage {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: userListModel sourceModel: userListModel
sortRole: "powerLevel" sortRole: "perm"
sortOrder: Qt.DescendingOrder
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.UserRole + 5) let permRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.UserRole + 5)
return powerLevelRole > 0; return permRole != UserType.Muted && permRole != UserType.Member;
} }
} }
delegate: MobileForm.FormTextDelegate { delegate: MobileForm.FormTextDelegate {
@@ -58,7 +57,22 @@ Kirigami.ScrollablePage {
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
QQC2.Label { QQC2.Label {
visible: !room.canSendState("m.room.power_levels") visible: !room.canSendState("m.room.power_levels")
text: powerLevelString text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
case UserType.Member:
return i18n("Member");
default:
return ""
}
}
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
} }
QQC2.ComboBox { QQC2.ComboBox {
@@ -177,9 +191,22 @@ Kirigami.ScrollablePage {
} }
trailing: QQC2.Label { trailing: QQC2.Label {
visible: powerLevel > 0 visible: perm != UserType.Member
text: powerLevelString text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
default:
return "";
}
}
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText textFormat: Text.PlainText
wrapMode: Text.NoWrap wrapMode: Text.NoWrap

View File

@@ -115,7 +115,7 @@ Kirigami.ScrollablePage {
action: Kirigami.Action { action: Kirigami.Action {
id: editDeviceAction id: editDeviceAction
text: i18n("Edit device name") text: i18n("Edit device name")
icon.name: "document-edit" iconName: "document-edit"
onTriggered: deviceDelegate.editDeviceName = true onTriggered: deviceDelegate.editDeviceName = true
} }
QQC2.ToolTip { QQC2.ToolTip {
@@ -129,7 +129,7 @@ Kirigami.ScrollablePage {
action: Kirigami.Action { action: Kirigami.Action {
id: verifyDeviceAction id: verifyDeviceAction
text: i18n("Verify device") text: i18n("Verify device")
icon.name: "security-low-symbolic" iconName: "security-low-symbolic"
onTriggered: { onTriggered: {
devices.connection.startKeyVerificationSession(devices.connection.localUserId, model.id) devices.connection.startKeyVerificationSession(devices.connection.localUserId, model.id)
} }
@@ -144,7 +144,7 @@ Kirigami.ScrollablePage {
action: Kirigami.Action { action: Kirigami.Action {
id: logoutDeviceAction id: logoutDeviceAction
text: i18n("Logout device") text: i18n("Logout device")
icon.name: "edit-delete-remove" iconName: "edit-delete-remove"
onTriggered: { onTriggered: {
passwordSheet.index = index passwordSheet.index = index
passwordSheet.open() passwordSheet.open()

View File

@@ -19,12 +19,14 @@ Kirigami.ScrollablePage {
MobileForm.FormCard { MobileForm.FormCard {
Layout.fillWidth: true Layout.fillWidth: true
contentItem: MobileForm.FormCheckDelegate { contentItem: MobileForm.FormCheckDelegate {
text: i18n("Enable notifications for this account") text: i18n("Enable notifications for this account")
description: i18n("Whether push notifications are generated by your Matrix server") checked: Config.showNotifications
checked: NotificationsManager.globalNotificationsEnabled enabled: !Config.isShowNotificationsImmutable && Controller.activeConnection
enabled: NotificationsManager.globalNotificationsSet
onToggled: { onToggled: {
Config.showNotifications = checked
Config.save()
NotificationsManager.globalNotificationsEnabled = checked NotificationsManager.globalNotificationsEnabled = checked
} }
} }

View File

@@ -35,18 +35,18 @@ Kirigami.CategorizedSettings {
}, },
Kirigami.SettingAction { Kirigami.SettingAction {
text: i18n("Spell Checking") text: i18n("Spell Checking")
icon.name: "tools-check-spelling" iconName: "tools-check-spelling"
page: Qt.resolvedUrl("SonnetConfigPage.qml") page: Qt.resolvedUrl("SonnetConfigPage.qml")
visible: Qt.platform.os !== "android" visible: Qt.platform.os !== "android"
}, },
Kirigami.SettingAction { Kirigami.SettingAction {
text: i18n("Network Proxy") text: i18n("Network Proxy")
icon.name: "network-connect" iconName: "network-connect"
page: Qt.resolvedUrl("NetworkProxyPage.qml") page: Qt.resolvedUrl("NetworkProxyPage.qml")
}, },
Kirigami.SettingAction { Kirigami.SettingAction {
text: i18n("Devices") text: i18n("Devices")
icon.name: "computer" iconName: "computer"
page: Qt.resolvedUrl("DevicesPage.qml") page: Qt.resolvedUrl("DevicesPage.qml")
}, },
Kirigami.SettingAction { Kirigami.SettingAction {

View File

@@ -11,8 +11,8 @@
#include <QString> #include <QString>
#include <QVariantMap> #include <QVariantMap>
#include "models/roomlistmodel.h" #include "roomlistmodel.h"
#include "models/sortfilterroomlistmodel.h" #include "sortfilterroomlistmodel.h"
// Copied from KRunner/QueryMatch // Copied from KRunner/QueryMatch
enum MatchType { enum MatchType {

Some files were not shown because too many files have changed in this diff Show More