Compare commits
3 Commits
work/carl/
...
work/yeetk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3915e22a | ||
|
|
741f6ea659 | ||
|
|
310e9b7ba3 |
@@ -59,4 +59,4 @@ Dependencies:
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ]
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
|
||||
|
||||
@@ -68,12 +68,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION}Keychain)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Secure storage of account secrets"
|
||||
)
|
||||
|
||||
if(ANDROID)
|
||||
find_package(OpenSSL)
|
||||
set_package_properties(OpenSSL PROPERTIES
|
||||
@@ -93,7 +87,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(Quotient 0.6)
|
||||
find_package(Quotient 0.7)
|
||||
set_package_properties(Quotient PROPERTIES
|
||||
TYPE REQUIRED
|
||||
DESCRIPTION "Qt wrapper around Matrix API"
|
||||
@@ -131,9 +125,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
|
||||
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
find_package(Sqlite3)
|
||||
@@ -150,7 +141,7 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
|
||||
if (BUILD_TESTING)
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
|
||||
add_subdirectory(autotests)
|
||||
endif()
|
||||
|
||||
@@ -65,7 +65,6 @@ private Q_SLOTS:
|
||||
void receiveRichEdited();
|
||||
};
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
@@ -198,7 +197,6 @@ void TextHandlerTest::initTestCase()
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
#endif
|
||||
|
||||
void TextHandlerTest::allowedAttributes()
|
||||
{
|
||||
@@ -473,7 +471,6 @@ void TextHandlerTest::receiveRichtextIn()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::receiveRichMxcUrl()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
@@ -491,7 +488,6 @@ void TextHandlerTest::receiveRichMxcUrl()
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* For when your rich input string has a plain text url left in.
|
||||
|
||||
@@ -231,7 +231,6 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.04.0" date="2023-04-20"/>
|
||||
<release version="23.01" date="2023-01-30">
|
||||
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
|
||||
<description>
|
||||
|
||||
444
po/ar/neochat.po
444
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/az/neochat.po
457
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
446
po/ca/neochat.po
446
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
452
po/cs/neochat.po
452
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
456
po/da/neochat.po
456
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
789
po/de/neochat.po
789
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
444
po/el/neochat.po
444
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
445
po/es/neochat.po
445
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/eu/neochat.po
557
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
473
po/fi/neochat.po
473
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
443
po/fr/neochat.po
443
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/hu/neochat.po
457
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
460
po/ia/neochat.po
460
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
444
po/id/neochat.po
444
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
444
po/ie/neochat.po
444
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
455
po/it/neochat.po
455
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
423
po/ja/neochat.po
423
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
443
po/ka/neochat.po
443
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/ko/neochat.po
457
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
423
po/lt/neochat.po
423
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
443
po/nl/neochat.po
443
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1028
po/nn/neochat.po
1028
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/pa/neochat.po
457
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
444
po/pl/neochat.po
444
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
448
po/pt/neochat.po
448
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
444
po/ru/neochat.po
444
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/sk/neochat.po
457
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
447
po/sl/neochat.po
447
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
457
po/sv/neochat.po
457
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
445
po/ta/neochat.po
445
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
445
po/tr/neochat.po
445
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
443
po/uk/neochat.po
443
po/uk/neochat.po
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
@@ -30,7 +30,6 @@ add_library(neochat STATIC
|
||||
models/devicesmodel.cpp
|
||||
filetypesingleton.cpp
|
||||
login.cpp
|
||||
stickerevent.cpp
|
||||
models/webshortcutmodel.cpp
|
||||
blurhash.cpp
|
||||
blurhashimageprovider.cpp
|
||||
@@ -47,8 +46,12 @@ add_library(neochat STATIC
|
||||
filetransferpseudojob.cpp
|
||||
models/searchmodel.cpp
|
||||
texthandler.cpp
|
||||
pollevent.cpp
|
||||
pollhandler.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(neochat PUBLIC "-DQT_NO_KEYWORDS")
|
||||
|
||||
add_executable(neochat-app
|
||||
main.cpp
|
||||
res.qrc
|
||||
@@ -60,14 +63,6 @@ target_link_libraries(neochat-app PRIVATE
|
||||
neochat
|
||||
)
|
||||
|
||||
if(Quotient_VERSION_MINOR GREATER 6)
|
||||
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
|
||||
target_sources(neochat PRIVATE pollevent.cpp pollhandler.cpp)
|
||||
else()
|
||||
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient_VERSION}\")
|
||||
target_sources(neochat PRIVATE neochataccountregistry.cpp)
|
||||
endif()
|
||||
|
||||
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
||||
|
||||
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
|
||||
@@ -95,6 +90,7 @@ endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
|
||||
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
|
||||
@@ -31,11 +31,7 @@
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include "accountregistry.h"
|
||||
#else
|
||||
#include "neochataccountregistry.h"
|
||||
#endif
|
||||
#include <accountregistry.h>
|
||||
|
||||
#include <connection.h>
|
||||
#include <csapi/content-repo.h>
|
||||
@@ -43,11 +39,8 @@
|
||||
#include <csapi/profile.h>
|
||||
#include <jobs/downloadfilejob.h>
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <csapi/notifications.h>
|
||||
#include <eventstats.h>
|
||||
#endif
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
@@ -56,6 +49,8 @@
|
||||
#include "roommanager.h"
|
||||
#include "windowcontroller.h"
|
||||
|
||||
#include <accountregistry.h>
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||
#include "trayicon.h"
|
||||
#elif !defined(Q_OS_ANDROID)
|
||||
@@ -93,10 +88,28 @@ Controller::Controller(QObject *parent)
|
||||
});
|
||||
#endif
|
||||
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
invokeLogin();
|
||||
connectUntil(&Accounts, &AccountRegistry::rowsInserted, this, [this]() {
|
||||
if (auto *connection = Accounts.get(NeoChatConfig::self()->activeConnection())) {
|
||||
connectSingleShot(connection, &Connection::loadedRoomState, this, [this, connection]() {
|
||||
setActiveConnection(connection);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
connect(&Accounts, &AccountRegistry::rowsRemoved, this, [this]() {
|
||||
if (!Accounts.isLoggedIn(NeoChatConfig::self()->activeConnection())) {
|
||||
if (Accounts.size() > 0) {
|
||||
setActiveConnection(Accounts.accounts().at(0));
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QMetaObject::invokeMethod(&Accounts, &AccountRegistry::invokeLogin);
|
||||
|
||||
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] {
|
||||
NeoChatConfig::self()->save();
|
||||
});
|
||||
@@ -125,23 +138,29 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
connect(&Accounts, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
static int oldAccountCount = 0;
|
||||
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, [=]() {
|
||||
if (AccountRegistry::instance().size() > oldAccountCount) {
|
||||
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
|
||||
connect(connection, &Connection::syncDone, this, [=]() {
|
||||
handleNotifications(connection);
|
||||
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (Accounts.size() > oldAccountCount) {
|
||||
auto connection = Accounts.accounts()[Accounts.size() - 1];
|
||||
connect(connection, &Connection::syncDone, this, [this, 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(connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
oldAccountCount = AccountRegistry::instance().size();
|
||||
oldAccountCount = Accounts.size();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
||||
{
|
||||
static QStringList initial;
|
||||
@@ -215,7 +234,6 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
Controller &Controller::instance()
|
||||
{
|
||||
@@ -228,239 +246,10 @@ void Controller::showWindow()
|
||||
WindowController::instance().showAndRaiseWindow(QString());
|
||||
}
|
||||
|
||||
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
|
||||
{
|
||||
if (user.isEmpty() || token.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl serverUrl(serverAddr);
|
||||
|
||||
auto conn = new Connection();
|
||||
if (serverUrl.isValid()) {
|
||||
conn->setHomeserver(serverUrl);
|
||||
}
|
||||
|
||||
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
|
||||
AccountSettings account(conn->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.setHomeserver(conn->homeserver());
|
||||
account.setDeviceId(conn->deviceId());
|
||||
account.setDeviceName(deviceName);
|
||||
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
addConnection(conn);
|
||||
setActiveConnection(conn);
|
||||
});
|
||||
connect(conn, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
});
|
||||
conn->assumeIdentity(user, token, deviceName);
|
||||
}
|
||||
|
||||
void Controller::logout(Connection *conn, bool serverSideLogout)
|
||||
{
|
||||
if (!conn) {
|
||||
qCritical() << "Attempt to logout null connection";
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsGroup("Accounts").remove(conn->userId());
|
||||
|
||||
QKeychain::DeletePasswordJob job(qAppName());
|
||||
job.setAutoDelete(true);
|
||||
job.setKey(conn->userId());
|
||||
QEventLoop loop;
|
||||
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (AccountRegistry::instance().count() > 1) {
|
||||
// Only set the connection if the the account being logged out is currently active
|
||||
if (conn == activeConnection()) {
|
||||
setActiveConnection(AccountRegistry::instance().accounts()[0]);
|
||||
}
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
}
|
||||
if (!serverSideLogout) {
|
||||
return;
|
||||
}
|
||||
conn->logout();
|
||||
}
|
||||
|
||||
void Controller::addConnection(Connection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
AccountRegistry::instance().add(c);
|
||||
#endif
|
||||
|
||||
c->setLazyLoading(true);
|
||||
|
||||
connect(c, &Connection::syncDone, this, [this, c] {
|
||||
setBusy(false);
|
||||
|
||||
Q_EMIT syncDone();
|
||||
|
||||
c->sync(30000);
|
||||
c->saveState();
|
||||
});
|
||||
connect(c, &Connection::loggedOut, this, [this, c] {
|
||||
dropConnection(c);
|
||||
});
|
||||
|
||||
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
|
||||
if (job->error() == BaseJob::UserConsentRequiredError) {
|
||||
Q_EMIT userConsentRequired(job->errorUrl());
|
||||
}
|
||||
});
|
||||
|
||||
setBusy(true);
|
||||
|
||||
c->sync();
|
||||
|
||||
Q_EMIT connectionAdded(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
}
|
||||
|
||||
void Controller::dropConnection(Connection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
AccountRegistry::instance().drop(c);
|
||||
#endif
|
||||
|
||||
Q_EMIT connectionDropped(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
#ifndef QUOTIENT_07
|
||||
c->deleteLater();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::invokeLogin()
|
||||
{
|
||||
const auto accounts = SettingsGroup("Accounts").childGroups();
|
||||
QString id = NeoChatConfig::self()->activeConnection();
|
||||
for (const auto &accountId : accounts) {
|
||||
AccountSettings account{accountId};
|
||||
if (id.isEmpty()) {
|
||||
// handle case where the account config is empty
|
||||
id = accountId;
|
||||
}
|
||||
if (!account.homeserver().isEmpty()) {
|
||||
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
|
||||
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, id, this, accessTokenLoadingJob](QKeychain::Job *) {
|
||||
AccountSettings account{accountId};
|
||||
QString accessToken;
|
||||
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
|
||||
accessToken = accessTokenLoadingJob->binaryData();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
auto connection = new Connection(account.homeserver());
|
||||
connect(connection, &Connection::connected, this, [this, connection, id] {
|
||||
connection->loadState();
|
||||
addConnection(connection);
|
||||
if (connection->userId() == id) {
|
||||
setActiveConnection(connection);
|
||||
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
|
||||
if (error == "Unrecognised access token") {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
|
||||
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 {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
logout(connection, true);
|
||||
}
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
});
|
||||
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
|
||||
});
|
||||
}
|
||||
}
|
||||
if (accounts.isEmpty()) {
|
||||
Q_EMIT initiated();
|
||||
}
|
||||
}
|
||||
|
||||
QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
|
||||
{
|
||||
qDebug() << "Reading access token from the keychain for" << account.userId();
|
||||
auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
|
||||
job->setKey(account.userId());
|
||||
|
||||
// Handling of errors
|
||||
connect(job, &QKeychain::Job::finished, this, [this, &account, job]() {
|
||||
if (job->error() == QKeychain::Error::NoError) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (job->error()) {
|
||||
case QKeychain::EntryNotFound:
|
||||
Q_EMIT globalErrorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
|
||||
break;
|
||||
case QKeychain::AccessDeniedByUser:
|
||||
case QKeychain::AccessDenied:
|
||||
Q_EMIT globalErrorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
|
||||
break;
|
||||
case QKeychain::NoBackendAvailable:
|
||||
Q_EMIT globalErrorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
|
||||
break;
|
||||
case QKeychain::OtherError:
|
||||
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), job->errorString());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
job->start();
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken)
|
||||
{
|
||||
qDebug() << "Save the access token to the keychain for " << account.userId();
|
||||
QKeychain::WritePasswordJob job(qAppName());
|
||||
job.setAutoDelete(false);
|
||||
job.setKey(account.userId());
|
||||
job.setBinaryData(accessToken);
|
||||
QEventLoop loop;
|
||||
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (job.error()) {
|
||||
qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
|
||||
{
|
||||
auto job = conn->uploadFile(localFile.toLocalFile());
|
||||
#ifdef QUOTIENT_07
|
||||
if (isJobPending(job)) {
|
||||
#else
|
||||
if (isJobRunning(job)) {
|
||||
#endif
|
||||
connect(job, &BaseJob::success, this, [conn, job] {
|
||||
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
|
||||
});
|
||||
@@ -487,7 +276,7 @@ bool Controller::supportSystemTray() const
|
||||
|
||||
void Controller::changePassword(Connection *connection, const QString ¤tPassword, const QString &newPassword)
|
||||
{
|
||||
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
|
||||
auto *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
|
||||
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
|
||||
if (job->error() == 103) {
|
||||
QJsonObject replyData = job->jsonData();
|
||||
@@ -498,7 +287,7 @@ void Controller::changePassword(Connection *connection, const QString ¤tPa
|
||||
authData["user"] = connection->user()->id();
|
||||
QJsonObject identifier = {{"type", "m.id.user"}, {"user", connection->user()->id()}};
|
||||
authData["identifier"] = identifier;
|
||||
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
|
||||
auto *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
|
||||
connect(innerJob, &BaseJob::success, this, [this]() {
|
||||
Q_EMIT passwordStatus(PasswordStatus::Success);
|
||||
});
|
||||
@@ -518,11 +307,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
|
||||
User *localUser = connection->user();
|
||||
QString decoded = avatarSource.path();
|
||||
if (decoded.isEmpty()) {
|
||||
#ifdef QUOTIENT_07
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
|
||||
#else
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), QString());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
if (QImageReader(decoded).read().isNull()) {
|
||||
@@ -533,11 +318,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
|
||||
}
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
#ifdef QUOTIENT_07
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
#else
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), QStringLiteral("/_matrix/client/r0/account/password"))
|
||||
#endif
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
@@ -546,9 +327,12 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
|
||||
setRequestData(_data);
|
||||
}
|
||||
|
||||
int Controller::accountCount() const
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
return AccountRegistry::instance().count();
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
|
||||
bool Controller::quitOnLastWindowClosed()
|
||||
@@ -593,7 +377,6 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
}
|
||||
if (m_connection != nullptr) {
|
||||
disconnect(m_connection, &Connection::syncError, this, nullptr);
|
||||
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
|
||||
}
|
||||
m_connection = connection;
|
||||
if (connection != nullptr) {
|
||||
@@ -617,18 +400,12 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == QLatin1String("org.kde.neochat.account_label")) {
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NeoChatConfig::self()->setActiveConnection(QString());
|
||||
}
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT activeConnectionChanged();
|
||||
Q_EMIT activeConnectionIndexChanged();
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
|
||||
void Controller::saveWindowGeometry()
|
||||
@@ -636,18 +413,6 @@ void Controller::saveWindowGeometry()
|
||||
WindowController::instance().saveGeometry();
|
||||
}
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
#ifdef QUOTIENT_07
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
#else
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
|
||||
#endif
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
|
||||
void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
@@ -658,7 +423,7 @@ void Controller::createRoom(const QString &name, const QString &topic)
|
||||
this,
|
||||
&Controller::roomAdded,
|
||||
this,
|
||||
[this](NeoChatRoom *room) {
|
||||
[](NeoChatRoom *room) {
|
||||
RoomManager::instance().enterRoom(room);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -738,11 +503,7 @@ QString Controller::plainText(QQuickTextDocument *document) const
|
||||
|
||||
bool Controller::encryptionSupported() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return Quotient::encryptionSupported();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
|
||||
@@ -783,29 +544,10 @@ void Controller::setApplicationProxy()
|
||||
|
||||
int Controller::activeConnectionIndex() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto result = std::find_if(Accounts.accounts().begin(), Accounts.accounts().end(), [this](const auto &it) {
|
||||
return it == m_connection;
|
||||
});
|
||||
return result - Accounts.accounts().begin();
|
||||
#else
|
||||
for (int i = 0; i < AccountRegistry::instance().rowCount(); i++) {
|
||||
if (AccountRegistry::instance().data(AccountRegistry::instance().index(i, 0), AccountRegistry::UserIdRole).toString() == m_connection->userId()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int Controller::quotientMinorVersion() const
|
||||
{
|
||||
// TODO libQuotient 0.7: Replace with version function from libQuotient
|
||||
#ifdef QUOTIENT_07
|
||||
return 7;
|
||||
#else
|
||||
return 6;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Controller::isFlatpak() const
|
||||
@@ -816,22 +558,3 @@ bool Controller::isFlatpak() const
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::setActiveAccountLabel(const QString &label)
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
QJsonObject json{
|
||||
{"account_label", label},
|
||||
};
|
||||
m_connection->setAccountData("org.kde.neochat.account_label", json);
|
||||
}
|
||||
|
||||
QString Controller::activeAccountLabel() const
|
||||
{
|
||||
if (!m_connection) {
|
||||
return {};
|
||||
}
|
||||
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
|
||||
}
|
||||
@@ -23,15 +23,9 @@ class Connection;
|
||||
class Room;
|
||||
}
|
||||
|
||||
namespace QKeychain
|
||||
{
|
||||
class ReadPasswordJob;
|
||||
}
|
||||
|
||||
class Controller : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
|
||||
Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged)
|
||||
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
|
||||
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
|
||||
@@ -40,9 +34,7 @@ class Controller : public QObject
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
|
||||
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
|
||||
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
|
||||
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
|
||||
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
|
||||
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
|
||||
|
||||
public:
|
||||
static Controller &instance();
|
||||
@@ -50,17 +42,10 @@ public:
|
||||
void setActiveConnection(Quotient::Connection *connection);
|
||||
[[nodiscard]] Quotient::Connection *activeConnection() const;
|
||||
|
||||
void addConnection(Quotient::Connection *c);
|
||||
void dropConnection(Quotient::Connection *c);
|
||||
|
||||
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
|
||||
|
||||
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString ¤tPassword, const QString &newPassword);
|
||||
|
||||
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
|
||||
|
||||
[[nodiscard]] int accountCount() const;
|
||||
|
||||
[[nodiscard]] static bool quitOnLastWindowClosed();
|
||||
void setQuitOnLastWindowClosed(bool value);
|
||||
|
||||
@@ -69,8 +54,6 @@ public:
|
||||
|
||||
[[nodiscard]] bool supportSystemTray() const;
|
||||
|
||||
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
int activeConnectionIndex() const;
|
||||
|
||||
enum PasswordStatus {
|
||||
@@ -101,26 +84,8 @@ public:
|
||||
|
||||
Q_INVOKABLE void setApplicationProxy();
|
||||
|
||||
int quotientMinorVersion() const;
|
||||
bool isFlatpak() const;
|
||||
|
||||
/**
|
||||
* @brief The label for this account.
|
||||
*
|
||||
* Account labels are a concept Specific to NeoChat, allowing accounts to be labelled, e.g. for "Work", "Private", etc.
|
||||
* @return The label, if it exists, otherwise an empty string
|
||||
*/
|
||||
[[nodiscard]] QString activeAccountLabel() const;
|
||||
|
||||
/**
|
||||
* @brief Set the label for this account.
|
||||
*
|
||||
* Set to an empty string to remove the label
|
||||
* @sa Controller::activeAccountLabel
|
||||
* @param label The label to use, or an empty string
|
||||
*/
|
||||
void setActiveAccountLabel(const QString &label);
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -128,20 +93,15 @@ private:
|
||||
bool m_busy = false;
|
||||
TrayIcon *m_trayIcon = nullptr;
|
||||
|
||||
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
|
||||
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
bool m_isOnline = true;
|
||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||
|
||||
bool hasWindowSystem() const;
|
||||
#ifdef QUOTIENT_07
|
||||
void handleNotifications(QPointer<Quotient::Connection> connection);
|
||||
#endif
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
void showWindow();
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -154,7 +114,6 @@ Q_SIGNALS:
|
||||
void syncDone();
|
||||
void connectionAdded(Quotient::Connection *_t1);
|
||||
void connectionDropped(Quotient::Connection *_t1);
|
||||
void accountCountChanged();
|
||||
void initiated();
|
||||
void notificationClicked(const QString &_t1, const QString &_t2);
|
||||
void quitOnLastWindowClosedChanged();
|
||||
@@ -170,16 +129,14 @@ Q_SIGNALS:
|
||||
void keyVerificationKey(const QString &sas);
|
||||
void activeConnectionIndexChanged();
|
||||
void roomAdded(NeoChatRoom *room);
|
||||
void activeAccountLabelChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void logout(Quotient::Connection *conn, bool serverSideLogout);
|
||||
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
|
||||
static void markAllMessagesAsRead(Quotient::Connection *conn);
|
||||
void saveWindowGeometry();
|
||||
};
|
||||
|
||||
// TODO libQuotient 0.7: Drop
|
||||
// TODO libQuotient 0.?: Drop
|
||||
class NeochatChangePasswordJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -7,30 +7,17 @@
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
class JoinRulesEvent : public StateEvent
|
||||
#else
|
||||
class JoinRulesEvent : public StateEventBase
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
#ifdef QUOTIENT_07
|
||||
QUO_EVENT(JoinRulesEvent, "m.room.join_rules")
|
||||
#else
|
||||
DEFINE_EVENT_TYPEID("m.room.join_rules", JoinRulesEvent)
|
||||
#endif
|
||||
|
||||
explicit JoinRulesEvent(const QJsonObject &obj)
|
||||
#ifdef QUOTIENT_07
|
||||
: StateEvent(obj)
|
||||
#else
|
||||
: StateEventBase(typeId(), obj)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
QString joinRule() const;
|
||||
QJsonArray allow() const;
|
||||
};
|
||||
REGISTER_EVENT_TYPE(JoinRulesEvent)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
|
||||
#include "login.h"
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <accountregistry.h>
|
||||
#else
|
||||
#include "neochataccountregistry.h"
|
||||
#endif
|
||||
|
||||
#include <connection.h>
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
@@ -43,7 +40,7 @@ void Login::init()
|
||||
return;
|
||||
}
|
||||
|
||||
m_isLoggedIn = AccountRegistry::instance().isLoggedIn(m_matrixId);
|
||||
m_isLoggedIn = Accounts.isLoggedIn(m_matrixId);
|
||||
Q_EMIT isLoggedInChanged();
|
||||
if (m_isLoggedIn) {
|
||||
return;
|
||||
@@ -74,11 +71,7 @@ void Login::init()
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
m_connection = nullptr;
|
||||
});
|
||||
@@ -97,8 +90,9 @@ void Login::init()
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
|
||||
Q_EMIT Controller::instance().initiated();
|
||||
connectSingleShot(m_connection, &Connection::loadedRoomState, this, [this]() {
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
// TODO close settings window
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
30
src/main.cpp
30
src/main.cpp
@@ -28,15 +28,14 @@
|
||||
|
||||
#include "neochat-version.h"
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <accountregistry.h>
|
||||
#else
|
||||
#include "neochataccountregistry.h"
|
||||
#endif
|
||||
|
||||
#include <networkaccessmanager.h>
|
||||
#include <room.h>
|
||||
#include <util.h>
|
||||
#include <keyverificationsession.h>
|
||||
#include <accountregistry.h>
|
||||
#include <room.h>
|
||||
#include <networkaccessmanager.h>
|
||||
|
||||
#include "actionshandler.h"
|
||||
#include "blurhashimageprovider.h"
|
||||
@@ -68,16 +67,11 @@
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "notificationsmanager.h"
|
||||
#ifdef QUOTIENT_07
|
||||
#include "pollhandler.h"
|
||||
#endif
|
||||
#include "roommanager.h"
|
||||
#include "spacehierarchycache.h"
|
||||
#include "urlhelper.h"
|
||||
#include "windowcontroller.h"
|
||||
#ifdef QUOTIENT_07
|
||||
#include <keyverificationsession.h>
|
||||
#endif
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
@@ -85,6 +79,7 @@
|
||||
#include "models/statemodel.h"
|
||||
#include "neochatuser.h"
|
||||
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
@@ -165,14 +160,10 @@ int main(int argc, char *argv[])
|
||||
|
||||
about.addComponent(QStringLiteral("libQuotient"),
|
||||
i18n("A Qt5 library to write cross-platform clients for Matrix"),
|
||||
#ifdef QUOTIENT_07
|
||||
i18nc("<version number> (built against <possibly different version number>)",
|
||||
"%1 (built against %2)",
|
||||
Quotient::versionString(),
|
||||
QStringLiteral(Quotient_VERSION_STRING)),
|
||||
#else
|
||||
QStringLiteral(QUOTIENT_VERSION),
|
||||
#endif
|
||||
QStringLiteral("https://github.com/quotient-im/libquotient"),
|
||||
KAboutLicense::LGPL_V2_1);
|
||||
|
||||
@@ -209,11 +200,8 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", &EmojiModel::instance());
|
||||
#ifdef QUOTIENT_07
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::Accounts);
|
||||
#else
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
|
||||
#endif
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Accounts", &Quotient::Accounts);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Accounts", &Quotient::Accounts);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
@@ -235,9 +223,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
||||
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
|
||||
#ifdef QUOTIENT_07
|
||||
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
||||
#endif
|
||||
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
||||
@@ -256,12 +242,10 @@ int main(int argc, char *argv[])
|
||||
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
|
||||
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
|
||||
qRegisterMetaType<QMimeType>("QMimeType");
|
||||
#ifdef QUOTIENT_07
|
||||
#ifdef Quotient_E2EE_ENABLED
|
||||
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
|
||||
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
|
||||
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
|
||||
#endif
|
||||
#endif
|
||||
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
|
||||
return engine->toScriptValue(KAboutData::applicationData());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "roommanager.h"
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/roompowerlevelsevent.h>
|
||||
@@ -191,7 +192,6 @@ QVector<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
|
||||
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));
|
||||
@@ -201,7 +201,6 @@ QVector<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
#endif
|
||||
if (room->localUser()->id() == text) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18n("You are already in this room."));
|
||||
return QString();
|
||||
@@ -243,7 +242,6 @@ QVector<ActionsModel::Action> actions{
|
||||
kli18n("<room alias or id>"),
|
||||
kli18n("Joins the given room"),
|
||||
},
|
||||
#ifdef QUOTIENT_07
|
||||
Action{
|
||||
QStringLiteral("knock"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
@@ -276,7 +274,6 @@ QVector<ActionsModel::Action> actions{
|
||||
kli18n("<room alias or id> [<reason>]"),
|
||||
kli18n("Requests to join the given room"),
|
||||
},
|
||||
#endif
|
||||
Action{
|
||||
QStringLiteral("j"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
@@ -435,14 +432,12 @@ QVector<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
|
||||
if (state && state->membership() == Membership::Ban) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
#endif
|
||||
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to ban users from this room."));
|
||||
return QString();
|
||||
@@ -472,18 +467,16 @@ QVector<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to unban users from this room."));
|
||||
return QString();
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
auto state = room->currentState().get<RoomMemberEvent>(text);
|
||||
if (state && state->membership() != Membership::Ban) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
#endif
|
||||
room->unban(text);
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
|
||||
|
||||
@@ -510,13 +503,11 @@ QVector<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You cannot kick yourself from the room."));
|
||||
return QString();
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
if (!room->isMember(parts[0])) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
#endif
|
||||
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
auto kick = plEvent->kick();
|
||||
if (plEvent->powerLevelForUser(room->localUser()->id()) < kick) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to kick users from this room."));
|
||||
|
||||
@@ -9,42 +9,20 @@
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class ActionsModel
|
||||
*
|
||||
* This class defines a model for chat actions.
|
||||
*
|
||||
* @note A chat action is a message starting with /, resulting in something other
|
||||
* than a normal message being sent (e.g. /me, /join).
|
||||
*/
|
||||
class ActionsModel : public QAbstractListModel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Definition of an action.
|
||||
*/
|
||||
struct Action {
|
||||
QString prefix; /**< The prefix, without '/' and space after the word. */
|
||||
/**
|
||||
* @brief The function to execute when the action is triggered.
|
||||
*/
|
||||
// The prefix, without '/' and space after the word
|
||||
QString prefix;
|
||||
std::function<QString(const QString &, NeoChatRoom *)> handle;
|
||||
/**
|
||||
* @brief Whether the action is a message type action.
|
||||
*
|
||||
* If this is true, a message action will be sent. If this is false, this
|
||||
* message does some action on the client and should not be sent as a message.
|
||||
*/
|
||||
// If this is true, this action transforms a message to a different message and it will be sent.
|
||||
// If this is false, this message does some action on the client and should not be sent as a message.
|
||||
bool messageAction;
|
||||
/**
|
||||
* @brief The new message type of a message being sent.
|
||||
*
|
||||
* For a non-message action or a message action that outputs the same type
|
||||
* as its input, it's nullopt.
|
||||
*/
|
||||
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
|
||||
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
|
||||
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
|
||||
KLazyLocalizedString description; /**< The description of the action. */
|
||||
KLazyLocalizedString parameters;
|
||||
KLazyLocalizedString description;
|
||||
};
|
||||
static ActionsModel &instance()
|
||||
{
|
||||
@@ -52,41 +30,18 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Prefix = Qt::DisplayRole, /**< The prefix, without '/' and space after the word. */
|
||||
Description, /**< The description of the action. */
|
||||
CompletionType, /**< The completion type (always "action" for this model). */
|
||||
Parameters, /**< The input parameters expected by the action. */
|
||||
Prefix = Qt::DisplayRole,
|
||||
Description,
|
||||
CompletionType,
|
||||
Parameters,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Return a vector with all supported actions.
|
||||
*/
|
||||
QVector<Action> &allActions() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -6,58 +6,26 @@
|
||||
#include "messageeventmodel.h"
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class CollapseStateProxyModel
|
||||
*
|
||||
* This model aggregates multiple sequential state events into a single entry.
|
||||
*
|
||||
* Events are only aggregated if they happened on the same day.
|
||||
*
|
||||
* @sa MessageEventModel
|
||||
*/
|
||||
class CollapseStateProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
|
||||
StateEventsRole, /**< List of state events in the aggregated state. */
|
||||
AuthorListRole, /**< List of unique authors of the aggregated state event. */
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
||||
StateEventsRole,
|
||||
AuthorListRole,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::data
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractProxyModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Aggregation of the text of consecutive state events starting at row.
|
||||
* @brief QString aggregating the text of consecutive state events starting at row.
|
||||
*
|
||||
* If state events happen on different days they will be split into two aggregate
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||
|
||||
/**
|
||||
* @brief Return a list of consecutive state events starting at row.
|
||||
*
|
||||
@@ -65,7 +33,6 @@ private:
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QVariantList stateEventsList(int row) const;
|
||||
|
||||
/**
|
||||
* @brief List of unique authors for the aggregate state events starting at row.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QDebug>
|
||||
|
||||
#include "actionsmodel.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "completionproxymodel.h"
|
||||
#include "customemojimodel.h"
|
||||
#include "emojimodel.h"
|
||||
|
||||
@@ -13,89 +13,41 @@ class UserListModel;
|
||||
class NeoChatRoom;
|
||||
class RoomListModel;
|
||||
|
||||
/**
|
||||
* @class CompletionModel
|
||||
*
|
||||
* This class defines the model for suggesting completions in chat text.
|
||||
*
|
||||
* This model is able to select the appropriate completion type for the input text
|
||||
* and present a list of options that can be presented to the user.
|
||||
*/
|
||||
class CompletionModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current text to search for completions.
|
||||
*/
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged)
|
||||
|
||||
/**
|
||||
* @brief The current room that the model is getting completions for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The current type of completion being done on the entered text.
|
||||
*
|
||||
* @sa AutoCompletionType
|
||||
*/
|
||||
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
|
||||
|
||||
/**
|
||||
* @brief The RoomListModel to be used for room completions.
|
||||
*/
|
||||
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the different types of completion available.
|
||||
*/
|
||||
enum AutoCompletionType {
|
||||
User, /**< A user in the current room. */
|
||||
Room, /**< A matrix room. */
|
||||
Emoji, /**< An emoji. */
|
||||
Command, /**< A / command. */
|
||||
None, /**< No available completion for the current text. */
|
||||
User,
|
||||
Room,
|
||||
Emoji,
|
||||
Command,
|
||||
None,
|
||||
};
|
||||
Q_ENUM(AutoCompletionType)
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Text = Qt::DisplayRole, /**< The main text to show. */
|
||||
Subtitle, /**< The subtitle text to show. */
|
||||
Icon, /**< The icon to show. */
|
||||
ReplacedText, /**< The text to replace the input text with for the completion. */
|
||||
Text = Qt::DisplayRole,
|
||||
Subtitle,
|
||||
Icon,
|
||||
ReplacedText,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
CompletionModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
QString text() const;
|
||||
void setText(const QString &text, const QString &fullText);
|
||||
void updateCompletion();
|
||||
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
@@ -104,7 +56,6 @@ public:
|
||||
void setRoomListModel(RoomListModel *roomListModel);
|
||||
|
||||
AutoCompletionType autoCompletionType() const;
|
||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||
|
||||
Q_SIGNALS:
|
||||
void textChanged();
|
||||
@@ -119,7 +70,7 @@ private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
AutoCompletionType m_autoCompletionType = None;
|
||||
|
||||
void updateCompletion();
|
||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||
|
||||
UserListModel *m_userListModel;
|
||||
RoomListModel *m_roomListModel;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "completionproxymodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
Q_UNUSED(sourceParent);
|
||||
@@ -43,6 +44,7 @@ int CompletionProxyModel::secondaryFilterRole() const
|
||||
void CompletionProxyModel::setSecondaryFilterRole(int role)
|
||||
{
|
||||
m_secondaryFilterRole = role;
|
||||
Q_EMIT secondaryFilterRoleChanged();
|
||||
}
|
||||
|
||||
QString CompletionProxyModel::filterText() const
|
||||
@@ -53,6 +55,7 @@ QString CompletionProxyModel::filterText() const
|
||||
void CompletionProxyModel::setFilterText(const QString &filterText)
|
||||
{
|
||||
m_filterText = filterText;
|
||||
Q_EMIT filterTextChanged();
|
||||
}
|
||||
|
||||
void CompletionProxyModel::setFullText(const QString &fullText)
|
||||
|
||||
@@ -5,71 +5,28 @@
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class CompletionProxyModel
|
||||
*
|
||||
* A filter model to sort and filter completion results.
|
||||
*
|
||||
* This model is designed to work with multiple source models depending upon the
|
||||
* completion type.
|
||||
*
|
||||
* A model value will be shown if its primary or secondary role values start with
|
||||
* the filter text. The exception is if the full text perfectly matches
|
||||
* the primary filter role value in which case the completion ends (i.e. the filter
|
||||
* will return no results).
|
||||
*
|
||||
* @note The filter is primarily design to work with strings, therefore make sure
|
||||
* that the source model roles that are to be filtered are strings.
|
||||
*/
|
||||
class CompletionProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
|
||||
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Wether a row should be shown or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns true if the value of source_left is less than source_right.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::lessThan
|
||||
*/
|
||||
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||
|
||||
/**
|
||||
* @brief Get the current secondary filter role.
|
||||
*/
|
||||
int secondaryFilterRole() const;
|
||||
|
||||
/**
|
||||
* @brief Set the secondary filter role.
|
||||
*
|
||||
* Refer to the source model for what value corresponds to what role.
|
||||
*/
|
||||
void setSecondaryFilterRole(int role);
|
||||
|
||||
/**
|
||||
* @brief Get the current text being used to filter the source model.
|
||||
*/
|
||||
QString filterText() const;
|
||||
|
||||
/**
|
||||
* @brief Set the text to be used to filter the source model.
|
||||
*/
|
||||
void setFilterText(const QString &filterText);
|
||||
|
||||
/**
|
||||
* @brief Set the full text in the chatbar after the completion start.
|
||||
*
|
||||
* This is used to automatically end the completion if the user replicated the
|
||||
* primary filter role value perfectly.
|
||||
*/
|
||||
void setFullText(const QString &fullText);
|
||||
|
||||
Q_SIGNALS:
|
||||
void secondaryFilterRoleChanged();
|
||||
void filterTextChanged();
|
||||
|
||||
private:
|
||||
int m_secondaryFilterRole = -1;
|
||||
QString m_filterText;
|
||||
|
||||
@@ -11,12 +11,6 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#define running isJobPending
|
||||
#else
|
||||
#define running isJobRunning
|
||||
#endif
|
||||
|
||||
void CustomEmojiModel::fetchEmojis()
|
||||
{
|
||||
if (!Controller::instance().activeConnection()) {
|
||||
@@ -57,18 +51,12 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
|
||||
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
|
||||
|
||||
if (running(job)) {
|
||||
if (isJobPending(job)) {
|
||||
connect(job, &BaseJob::success, this, [name, job] {
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||
#ifdef QUOTIENT_07
|
||||
{QStringLiteral("url"), job->contentUri().toString()}
|
||||
#else
|
||||
{QStringLiteral("url"), job->contentUri()}
|
||||
#endif
|
||||
});
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({{QStringLiteral("url"), job->contentUri().toString()}});
|
||||
json["images"] = emojiData;
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||
});
|
||||
@@ -144,7 +132,6 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,29 +17,19 @@ struct CustomEmoji {
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class CustomEmojiModel
|
||||
*
|
||||
* This class defines the model for custom user emojis.
|
||||
*
|
||||
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
||||
*/
|
||||
class CustomEmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Name = Qt::DisplayRole, /**< The name of the emoji. */
|
||||
ImageURL, /**< The URL for the custom emoji. */
|
||||
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
|
||||
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
|
||||
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
|
||||
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
|
||||
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
|
||||
Name = Qt::DisplayRole,
|
||||
ImageURL,
|
||||
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
||||
MxcUrl = 50,
|
||||
DisplayRole = 51,
|
||||
ReplacedTextRole = 52,
|
||||
DescriptionRole = 53, // also invalid, reserved
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
@@ -49,45 +39,14 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Substitute any custom emojis for an image in the input text.
|
||||
*/
|
||||
Q_INVOKABLE QString preprocessText(const QString &it);
|
||||
|
||||
/**
|
||||
* @brief Return a list of custom emojis where the name contains the filter text.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
||||
|
||||
/**
|
||||
* @brief Add a new emoji to the model.
|
||||
*/
|
||||
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
||||
|
||||
/**
|
||||
* @brief Remove an emoji from the model.
|
||||
*/
|
||||
Q_INVOKABLE void removeEmoji(const QString &name);
|
||||
|
||||
private:
|
||||
|
||||
@@ -67,7 +67,7 @@ QHash<int, QByteArray> DevicesModel::roleNames() const
|
||||
|
||||
void DevicesModel::logout(int index, const QString &password)
|
||||
{
|
||||
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
|
||||
auto job = Controller::instance().activeConnection()->callApi<DeleteDeviceJob>(m_devices[index].deviceId);
|
||||
|
||||
connect(job, &BaseJob::result, this, [this, job, password, index] {
|
||||
auto onSuccess = [this, index]() {
|
||||
|
||||
@@ -12,67 +12,28 @@ namespace Quotient
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class DevicesModel
|
||||
*
|
||||
* This class defines the model for managing the devices of the local user.
|
||||
*
|
||||
* A device is any session where the local user is logged into a client. This means
|
||||
* the same physical device can have multiple sessions for example if the user uses
|
||||
* multiple clients on the same machine.
|
||||
*/
|
||||
class DevicesModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its devices from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Id, /**< The device ID. */
|
||||
DisplayName, /**< Display name set by the user for this device. */
|
||||
LastIp, /**< The IP address where this device was last seen. */
|
||||
LastTimestamp, /**< The timestamp when this devices was last seen. */
|
||||
Id,
|
||||
DisplayName,
|
||||
LastIp,
|
||||
LastTimestamp,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
DevicesModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Logout the device at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void logout(int index, const QString &password);
|
||||
|
||||
/**
|
||||
* @brief Set the display name of the device at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void setName(int index, const QString &name);
|
||||
|
||||
Quotient::Connection *connection() const;
|
||||
|
||||
@@ -51,30 +51,12 @@ struct Emoji {
|
||||
|
||||
Q_DECLARE_METATYPE(Emoji)
|
||||
|
||||
/**
|
||||
* @class EmojiModel
|
||||
*
|
||||
* This class defines the model for visualising a list of emojis.
|
||||
*/
|
||||
class EmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief Return a list of recently used emojis.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji categories.
|
||||
*
|
||||
* @note No custom emoji categories will be included.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji categories with custom emojis.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
|
||||
|
||||
public:
|
||||
@@ -84,92 +66,47 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum RoleNames {
|
||||
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
|
||||
UnicodeRole, /**< The unicode character of the emoji. */
|
||||
InvalidRole = 50, /**< Invalid, reserved. */
|
||||
DisplayRole = 51, /**< The display text for an emoji. */
|
||||
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
|
||||
DescriptionRole = 53, /**< The long description of an emoji. */
|
||||
ShortNameRole = Qt::DisplayRole,
|
||||
UnicodeRole,
|
||||
InvalidRole = 50,
|
||||
DisplayRole = 51,
|
||||
ReplacedTextRole = 52,
|
||||
DescriptionRole = 53,
|
||||
};
|
||||
Q_ENUM(RoleNames);
|
||||
|
||||
/**
|
||||
* @brief Defines the potential categories an emoji can be placed in.
|
||||
*/
|
||||
enum Category {
|
||||
Custom, /**< A custom user emoji. */
|
||||
Search, /**< The results of a filter. */
|
||||
SearchNoCustom, /**< The results of a filter with no custom emojis. */
|
||||
History, /**< Recently used emojis. */
|
||||
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
|
||||
Smileys, /**< Smileys & emotion emojis. */
|
||||
People, /**< People & Body emojis. */
|
||||
Nature, /**< Animals & Nature emojis. */
|
||||
Food, /**< Food & Drink emojis. */
|
||||
Activities, /**< Activities emojis. */
|
||||
Travel, /**< Travel & Places emojis. */
|
||||
Objects, /**< Objects emojis. */
|
||||
Symbols, /**< Symbols emojis. */
|
||||
Flags, /**< Flags emojis. */
|
||||
Component, /**< ??? */
|
||||
Custom,
|
||||
Search,
|
||||
SearchNoCustom,
|
||||
History,
|
||||
HistoryNoCustom,
|
||||
Smileys,
|
||||
People,
|
||||
Nature,
|
||||
Food,
|
||||
Activities,
|
||||
Travel,
|
||||
Objects,
|
||||
Symbols,
|
||||
Flags,
|
||||
Component,
|
||||
};
|
||||
Q_ENUM(Category)
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa RoleNames, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Return a filtered list of emojis.
|
||||
*
|
||||
* @note This includes custom emojis, use filterModelNoCustom to return a result
|
||||
* without custom emojis.
|
||||
*
|
||||
* @sa filterModelNoCustom
|
||||
*/
|
||||
Q_INVOKABLE QVariantList history() const;
|
||||
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
|
||||
|
||||
/**
|
||||
* @brief Return a filtered list of emojis without custom emojis.
|
||||
*
|
||||
* @note Use filterModel to return a result with custom emojis.
|
||||
*
|
||||
* @sa filterModel
|
||||
*/
|
||||
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
|
||||
|
||||
/**
|
||||
* @brief Return a list of emojis for the given category.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList emojis(Category category) const;
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji tones for the given base emoji.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
|
||||
|
||||
Q_INVOKABLE QVariantList history() const;
|
||||
QVariantList categories() const;
|
||||
QVariantList categoriesWithCustom() const;
|
||||
|
||||
|
||||
@@ -7,54 +7,23 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
/**
|
||||
* @class KeywordNotificationRuleModel
|
||||
*
|
||||
* This class defines the model for managing notification push rule keywords.
|
||||
*/
|
||||
class KeywordNotificationRuleModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
|
||||
NameRole = Qt::DisplayRole,
|
||||
};
|
||||
|
||||
KeywordNotificationRuleModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Add a new keyword to the model.
|
||||
*/
|
||||
Q_INVOKABLE void addKeyword(const QString &keyword);
|
||||
|
||||
/**
|
||||
* @brief Remove a keyword from the model.
|
||||
*/
|
||||
Q_INVOKABLE void removeKeywordAtIndex(int index);
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
@@ -11,12 +11,10 @@
|
||||
#include <events/roomavatarevent.h>
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/simplestateevents.h>
|
||||
#include <events/stickerevent.h>
|
||||
#include <user.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include "pollevent.h"
|
||||
#endif
|
||||
#include "stickerevent.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
@@ -109,11 +107,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||
room->getPreviousContent(50);
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
lastReadEventId = room->lastFullyReadEventId();
|
||||
#else
|
||||
lastReadEventId = room->readMarkerEventId();
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||
@@ -162,11 +156,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
endInsertRows();
|
||||
if (!m_lastReadEventIndex.isValid()) {
|
||||
// no read marker, so see if we need to create one.
|
||||
#ifdef QUOTIENT_07
|
||||
moveReadMarker(m_currentRoom->lastFullyReadEventId());
|
||||
#else
|
||||
moveReadMarker(m_currentRoom->readMarkerEventId());
|
||||
#endif
|
||||
}
|
||||
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
||||
@@ -207,7 +197,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
beginRemoveRows({}, i, i);
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
|
||||
connect(m_currentRoom, &Room::readMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) {
|
||||
connect(m_currentRoom, &Room::fullyReadMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) {
|
||||
Q_UNUSED(fromEventId);
|
||||
moveReadMarker(toEventId);
|
||||
});
|
||||
@@ -230,9 +220,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
|
||||
#ifndef QUOTIENT_07
|
||||
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
|
||||
#endif
|
||||
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
@@ -534,20 +521,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
if (is<const EncryptedEvent>(evt)) {
|
||||
return DelegateType::Encrypted;
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
if (is<PollStartEvent>(evt)) {
|
||||
if (evt.isRedacted()) {
|
||||
return DelegateType::Message;
|
||||
}
|
||||
return DelegateType::Poll;
|
||||
}
|
||||
#endif
|
||||
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
if (role == EventResolvedTypeRole) {
|
||||
return EventTypeRegistry::getMatrixType(evt.type());
|
||||
return evt.type();
|
||||
}
|
||||
|
||||
if (role == AuthorRole) {
|
||||
@@ -646,7 +631,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (evt.isStateEvent() && static_cast<const StateEventBase &>(evt).repeatsState()) {
|
||||
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
@@ -820,22 +805,13 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReadMarkersRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
|
||||
QVariantList users;
|
||||
users.reserve(userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||
#else
|
||||
auto user = static_cast<NeoChatUser *>(userId);
|
||||
#endif
|
||||
users += userAtEvent(user, m_currentRoom, evt);
|
||||
}
|
||||
|
||||
@@ -843,24 +819,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReadMarkersStringRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
/**
|
||||
* The string ends up in the form
|
||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||
*/
|
||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||
#else
|
||||
auto user = static_cast<NeoChatUser *>(userId);
|
||||
#endif
|
||||
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
|
||||
}
|
||||
readMarkersString.chop(2);
|
||||
@@ -868,18 +835,13 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ShowReadMarkersRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
return userIds.size() > 0;
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::AnnotationType);
|
||||
if (annotations.isEmpty()) {
|
||||
return {};
|
||||
};
|
||||
@@ -889,7 +851,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
continue;
|
||||
}
|
||||
if (auto e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
reactions[e->eventId()].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,7 +878,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == MediaUrlRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (!e->hasFileContent()) {
|
||||
return QVariant();
|
||||
@@ -933,7 +894,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
// if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
// return m_currentRoom->makeMediaUrl(e->id(), e->url());
|
||||
// }
|
||||
#endif
|
||||
|
||||
// Construct link in the same form as urlToDownload as that function doesn't work for stickers
|
||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
@@ -948,14 +908,12 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == VerifiedRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
#ifdef Quotient_E2EE_ENABLED
|
||||
if (evt.originalEvent()) {
|
||||
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
|
||||
Q_ASSERT(encrypted);
|
||||
return m_currentRoom->connection()->isVerifiedSession(encrypted->sessionId().toLatin1());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,21 +5,10 @@
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class MessageFilterModel
|
||||
*
|
||||
* This model filters out any messages that should be hidden.
|
||||
*
|
||||
* Deleted messages are only hidden if the user hasn't set them to be shown.
|
||||
*/
|
||||
class MessageFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
MessageFilterModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function to remove hidden messages.
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
};
|
||||
|
||||
@@ -12,11 +12,6 @@ PublicRoomListModel::PublicRoomListModel(QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
Quotient::Connection *PublicRoomListModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setConnection(Connection *conn)
|
||||
{
|
||||
if (m_connection == conn) {
|
||||
@@ -52,11 +47,6 @@ void PublicRoomListModel::setConnection(Connection *conn)
|
||||
Q_EMIT hasMoreChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::server() const
|
||||
{
|
||||
return m_server;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setServer(const QString &value)
|
||||
{
|
||||
if (m_server == value) {
|
||||
@@ -86,11 +76,6 @@ void PublicRoomListModel::setServer(const QString &value)
|
||||
Q_EMIT hasMoreChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::keyword() const
|
||||
{
|
||||
return m_keyword;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setKeyword(const QString &value)
|
||||
{
|
||||
if (m_keyword == value) {
|
||||
@@ -132,11 +117,7 @@ void PublicRoomListModel::next(int count)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, {}});
|
||||
#else
|
||||
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword});
|
||||
#endif
|
||||
|
||||
connect(job, &BaseJob::finished, this, [this] {
|
||||
attempted = true;
|
||||
@@ -192,11 +173,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (avatarUrl.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
return avatarUrl.url().remove(0, 6);
|
||||
#else
|
||||
return avatarUrl.remove(0, 6);
|
||||
#endif
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room.topic;
|
||||
|
||||
@@ -13,96 +13,54 @@ namespace Quotient
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PublicRoomListModel
|
||||
*
|
||||
* This class defines the model for visualising a list of public rooms.
|
||||
*
|
||||
* The model finds the public rooms visible to the given server (which doesn't have
|
||||
* to be the user's home server) and can also apply a filter if desired.
|
||||
*
|
||||
* Due to the fact that the public room list could be huge the model is lazily loaded
|
||||
* and requires that the next batch of rooms be manually called.
|
||||
*/
|
||||
class PublicRoomListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its rooms from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The server to get the public room list from.
|
||||
*/
|
||||
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
|
||||
|
||||
/**
|
||||
* @brief The filter keyword for the list of public rooms.
|
||||
*/
|
||||
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model has more items to load.
|
||||
*/
|
||||
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
TopicRole, /**< The room topic. */
|
||||
RoomIDRole, /**< The room matrix ID. */
|
||||
AliasRole, /**< The room canonical alias. */
|
||||
MemberCountRole, /**< The number of members in the room. */
|
||||
AllowGuestsRole, /**< Whether the room allows guest users. */
|
||||
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
|
||||
IsJoinedRole, /**< Whether the local user has joined the room. */
|
||||
NameRole = Qt::DisplayRole + 1,
|
||||
AvatarRole,
|
||||
TopicRole,
|
||||
RoomIDRole,
|
||||
AliasRole,
|
||||
MemberCountRole,
|
||||
AllowGuestsRole,
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
};
|
||||
|
||||
PublicRoomListModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
[[nodiscard]] Quotient::Connection *connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
void setConnection(Quotient::Connection *conn);
|
||||
|
||||
[[nodiscard]] QString server() const;
|
||||
[[nodiscard]] QString server() const
|
||||
{
|
||||
return m_server;
|
||||
}
|
||||
void setServer(const QString &value);
|
||||
|
||||
[[nodiscard]] QString keyword() const;
|
||||
[[nodiscard]] QString keyword() const
|
||||
{
|
||||
return m_keyword;
|
||||
}
|
||||
void setKeyword(const QString &value);
|
||||
|
||||
[[nodiscard]] bool hasMore() const;
|
||||
|
||||
/**
|
||||
* @brief Load the next set of rooms.
|
||||
*
|
||||
* @param count the maximum number of rooms to load.
|
||||
*/
|
||||
Q_INVOKABLE void next(int count = 50);
|
||||
|
||||
private:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "user.h"
|
||||
#include <eventstats.h>
|
||||
|
||||
#include <QDebug>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
@@ -22,11 +23,6 @@
|
||||
#include <QGuiApplication>
|
||||
#include <utility>
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
#include "notificationsmanager.h"
|
||||
#include <csapi/notifications.h>
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
Q_DECLARE_METATYPE(Quotient::JoinState)
|
||||
@@ -69,11 +65,6 @@ RoomListModel::RoomListModel(QObject *parent)
|
||||
|
||||
RoomListModel::~RoomListModel() = default;
|
||||
|
||||
Quotient::Connection *RoomListModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void RoomListModel::setConnection(Connection *connection)
|
||||
{
|
||||
if (connection == m_connection) {
|
||||
@@ -156,7 +147,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
connect(room, &Room::displaynameChanged, this, [this, room] {
|
||||
refresh(room, {DisplayNameRole, NameRole});
|
||||
});
|
||||
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
|
||||
connect(room, &Room::unreadStatsChanged, this, [this, room] {
|
||||
refresh(room, {UnreadCountRole, NotificationCountRole, HighlightCountRole});
|
||||
});
|
||||
connect(room, &Room::notificationCountChanged, this, [this, room] {
|
||||
@@ -177,74 +168,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
});
|
||||
#ifndef QUOTIENT_07
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
||||
#endif
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
#else
|
||||
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
void RoomListModel::handleNotifications()
|
||||
{
|
||||
static bool initial = true;
|
||||
static QStringList oldNotifications;
|
||||
auto job = m_connection->callApi<GetNotificationsJob>();
|
||||
|
||||
connect(job, &BaseJob::success, this, [this, job]() {
|
||||
const auto notifications = job->jsonData()["notifications"].toArray();
|
||||
if (initial) {
|
||||
initial = false;
|
||||
for (const auto &n : notifications) {
|
||||
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const auto &n : notifications) {
|
||||
const auto notification = n.toObject();
|
||||
if (notification["read"].toBool()) {
|
||||
oldNotifications.removeOne(notification["event"].toObject()["event_id"].toString());
|
||||
continue;
|
||||
}
|
||||
if (oldNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
|
||||
continue;
|
||||
}
|
||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
||||
|
||||
auto room = m_connection->room(notification["room_id"].toString());
|
||||
auto currentRoom = RoomManager::instance().currentRoom();
|
||||
bool roomIsActive = currentRoom && room->id() == currentRoom->id();
|
||||
|
||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||
if (room && !(roomIsActive && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender->avatarUrl(room).isEmpty()) {
|
||||
avatar_image = sender->avatar(128, room);
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender->displayname(room),
|
||||
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
||||
avatar_image,
|
||||
notification["event"].toObject()["event_id"].toString(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
int RoomListModel::notificationCount() const
|
||||
{
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
void RoomListModel::refreshNotificationCount()
|
||||
@@ -371,7 +295,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return NeoChatRoomType::Normal;
|
||||
}
|
||||
if (role == UnreadCountRole) {
|
||||
return room->unreadCount();
|
||||
return room->unreadStats().notableCount;
|
||||
}
|
||||
if (role == NotificationCountRole) {
|
||||
return room->notificationCount();
|
||||
@@ -450,40 +374,40 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryName(int category)
|
||||
QString RoomListModel::categoryName(int section)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
switch (section) {
|
||||
case 1:
|
||||
return i18n("Invited");
|
||||
case NeoChatRoomType::Favorite:
|
||||
case 2:
|
||||
return i18n("Favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
case 3:
|
||||
return i18n("Direct Messages");
|
||||
case NeoChatRoomType::Normal:
|
||||
case 4:
|
||||
return i18n("Normal");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
case 5:
|
||||
return i18n("Low priority");
|
||||
case NeoChatRoomType::Space:
|
||||
case 6:
|
||||
return i18n("Spaces");
|
||||
default:
|
||||
return "Deadbeef";
|
||||
}
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryIconName(int category)
|
||||
QString RoomListModel::categoryIconName(int section)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
switch (section) {
|
||||
case 1:
|
||||
return QStringLiteral("user-invisible");
|
||||
case NeoChatRoomType::Favorite:
|
||||
case 2:
|
||||
return QStringLiteral("favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
case 3:
|
||||
return QStringLiteral("dialog-messages");
|
||||
case NeoChatRoomType::Normal:
|
||||
case 4:
|
||||
return QStringLiteral("group");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
case 5:
|
||||
return QStringLiteral("object-order-lower");
|
||||
case NeoChatRoomType::Space:
|
||||
case 6:
|
||||
return QStringLiteral("group");
|
||||
default:
|
||||
return QStringLiteral("tools-report-bug");
|
||||
@@ -521,7 +445,7 @@ NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int RoomListModel::rowForRoom(NeoChatRoom *room) const
|
||||
int RoomListModel::indexForRoom(NeoChatRoom *room) const
|
||||
{
|
||||
return m_rooms.indexOf(room);
|
||||
}
|
||||
|
||||
@@ -20,137 +20,77 @@ class NeoChatRoomType : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the room list categories a room can be assigned.
|
||||
*/
|
||||
enum Types {
|
||||
Invited = 1, /**< The user has been invited to the room. */
|
||||
Favorite, /**< The room is set as a favourite. */
|
||||
Direct, /**< The room is a direct chat. */
|
||||
Normal, /**< The default category for a joined room. */
|
||||
Deprioritized, /**< The room is set as low priority. */
|
||||
Space, /**< The room is a space. */
|
||||
Invited = 1,
|
||||
Favorite,
|
||||
Direct,
|
||||
Normal,
|
||||
Deprioritized,
|
||||
Space,
|
||||
};
|
||||
Q_ENUM(Types)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RoomListModel
|
||||
*
|
||||
* This class defines the model for visualising the user's list of joined rooms.
|
||||
*/
|
||||
class RoomListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its rooms from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The total number of notifications for all the rooms.
|
||||
*/
|
||||
Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::UserRole + 1, /**< The name of the room. */
|
||||
DisplayNameRole, /**< The display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
UnreadCountRole, /**< The number of unread messages in the room. */
|
||||
NotificationCountRole, /**< The number of notifications in the room. */
|
||||
HighlightCountRole, /**< The number of highlighted messages in the room. */
|
||||
LastEventRole, /**< Text for the last event in the room. */
|
||||
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
|
||||
JoinStateRole, /**< The local user's join state in the room. */
|
||||
CurrentRoomRole, /**< The room object for the room. */
|
||||
CategoryVisibleRole, /**< If the room's category is visible. */
|
||||
SubtitleTextRole, /**< The text to show as the room subtitle. */
|
||||
AvatarImageRole, /**< The room avatar as an image. */
|
||||
IdRole, /**< The room matrix ID. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
NameRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
AvatarRole,
|
||||
CanonicalAliasRole,
|
||||
TopicRole,
|
||||
CategoryRole,
|
||||
UnreadCountRole,
|
||||
NotificationCountRole,
|
||||
HighlightCountRole,
|
||||
LastEventRole,
|
||||
LastActiveTimeRole,
|
||||
JoinStateRole,
|
||||
CurrentRoomRole,
|
||||
CategoryVisibleRole,
|
||||
SubtitleTextRole,
|
||||
AvatarImageRole,
|
||||
IdRole,
|
||||
IsSpaceRole,
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
RoomListModel(QObject *parent = nullptr);
|
||||
~RoomListModel() override;
|
||||
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
[[nodiscard]] Quotient::Connection *connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
void setConnection(Quotient::Connection *connection);
|
||||
void doResetModel();
|
||||
|
||||
[[nodiscard]] int notificationCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Return the room at the given row.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
|
||||
|
||||
/**
|
||||
* @brief Return a string to represent the given room category.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
Q_INVOKABLE [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Return a string with the name of the given room category icon.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Set whether a given category should be visible or not.
|
||||
*
|
||||
* @param category the NeoChatRoomType::Types value for the category (it's an
|
||||
* int due to the pain of Q_INVOKABLES and cpp enums).
|
||||
* @param visible true if the category should be visible, false if not.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryName(int section);
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int section);
|
||||
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
|
||||
|
||||
/**
|
||||
* @brief Return whether a room category is set to be visible.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
|
||||
Q_INVOKABLE [[nodiscard]] int indexForRoom(NeoChatRoom *room) const;
|
||||
|
||||
/**
|
||||
* @brief Return the model row for the given room.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int rowForRoom(NeoChatRoom *room) const;
|
||||
[[nodiscard]] int notificationCount() const
|
||||
{
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a room for the given room alias or room matrix ID.
|
||||
*
|
||||
* The room must be in the model.
|
||||
*/
|
||||
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
|
||||
|
||||
private Q_SLOTS:
|
||||
void doResetModel();
|
||||
void doAddRoom(Quotient::Room *room);
|
||||
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
|
||||
void deleteRoom(Quotient::Room *room);
|
||||
@@ -167,9 +107,6 @@ private:
|
||||
QString m_activeSpaceId = "";
|
||||
|
||||
void connectRoomSignals(NeoChatRoom *room);
|
||||
#ifndef QUOTIENT_07
|
||||
void handleNotifications();
|
||||
#endif
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
#include <KLocalizedString>
|
||||
#include <connection.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <csapi/search.h>
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -34,7 +32,6 @@ void SearchModel::setSearchText(const QString &searchText)
|
||||
|
||||
void SearchModel::search()
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
Q_ASSERT(m_connection);
|
||||
setSearching(true);
|
||||
if (m_job) {
|
||||
@@ -58,7 +55,6 @@ void SearchModel::search()
|
||||
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
|
||||
.includeState = false,
|
||||
.groupings = none,
|
||||
|
||||
};
|
||||
|
||||
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
|
||||
@@ -71,7 +67,6 @@ void SearchModel::search()
|
||||
m_job = nullptr;
|
||||
// TODO error handling
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
Connection *SearchModel::connection() const
|
||||
@@ -87,7 +82,6 @@ void SearchModel::setConnection(Connection *connection)
|
||||
|
||||
QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto row = index.row();
|
||||
const auto &event = *m_result->results[row].result;
|
||||
switch (role) {
|
||||
@@ -117,25 +111,21 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return event.originTimestamp();
|
||||
}
|
||||
return MessageEventModel::DelegateType::Message;
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
int SearchModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
#ifdef QUOTIENT_07
|
||||
if (m_result.has_value()) {
|
||||
return m_result->results.size();
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{DelegateTypeRole, "delegateType"},
|
||||
{EventTypeRole, "eventType"},
|
||||
{DisplayRole, "display"},
|
||||
{AuthorRole, "author"},
|
||||
{ShowSectionRole, "showSection"},
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QString>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <csapi/search.h>
|
||||
#endif
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
@@ -17,50 +15,24 @@ class Connection;
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class SearchModel
|
||||
*
|
||||
* This class defines the model for visualising the results of a room message search.
|
||||
*/
|
||||
class SearchModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The text to search messages for.
|
||||
*/
|
||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is using to search for messages.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The current room that the search is being done from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model is currently searching for messages.
|
||||
*/
|
||||
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*
|
||||
* Some of the roles exist only for compatibility with the MessageEventModel,
|
||||
* since the same delegates are used.
|
||||
*/
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole, /**< The message string. */
|
||||
DelegateTypeRole, /**< The type of the event. */
|
||||
ShowAuthorRole, /**< Whether the author should be shown (always true). */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
TimeRole, /**< The timestamp for when the event was sent. */
|
||||
DisplayRole = Qt::DisplayRole,
|
||||
EventTypeRole,
|
||||
ShowAuthorRole,
|
||||
AuthorRole,
|
||||
ShowSectionRole,
|
||||
SectionRole,
|
||||
TimeRole,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
SearchModel(QObject *parent = nullptr);
|
||||
@@ -74,33 +46,13 @@ public:
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
bool searching() const;
|
||||
Q_INVOKABLE void search();
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Start searching for messages.
|
||||
*/
|
||||
Q_INVOKABLE void search();
|
||||
bool searching() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void searchTextChanged();
|
||||
@@ -114,10 +66,8 @@ private:
|
||||
QString m_searchText;
|
||||
Quotient::Connection *m_connection = nullptr;
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
#ifdef QUOTIENT_07
|
||||
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
|
||||
Quotient::SearchJob *m_job = nullptr;
|
||||
#endif
|
||||
bool m_searching = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -95,11 +95,7 @@ void ServerListModel::checkServer(const QString &url)
|
||||
KConfigGroup serverGroup(&dataResource, "Servers");
|
||||
|
||||
if (!serverGroup.hasKey(url)) {
|
||||
#ifdef QUOTIENT_07
|
||||
if (Quotient::isJobPending(m_checkServerJob)) {
|
||||
#else
|
||||
if (Quotient::isJobRunning(m_checkServerJob)) {
|
||||
#endif
|
||||
m_checkServerJob->abandon();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,89 +9,33 @@
|
||||
#include <QPointer>
|
||||
#include <QUrl>
|
||||
|
||||
/**
|
||||
* @class ServerListModel
|
||||
*
|
||||
* This class defines the model for visualising a list of matrix servers.
|
||||
*
|
||||
* The list of servers is retrieved from the local cache. Any additions are also
|
||||
* stored locally so that they are retrieved on subsequent instantiations.
|
||||
*
|
||||
* The model also automatically adds the local user's home server and matrix.org to
|
||||
* the model. Finally the model also adds an entry to create a space in the model
|
||||
* for an "add new server" delegate.
|
||||
*/
|
||||
class ServerListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Define the data required to represent a server.
|
||||
*/
|
||||
struct Server {
|
||||
QString url; /**< Server URL. */
|
||||
bool isHomeServer; /**< Whether the server is the local user's home server. */
|
||||
bool isAddServerDelegate; /**< Wether the item is the "add new server" delegate. */
|
||||
bool isDeletable; /**< Whether the item can be deleted from the model. */
|
||||
QString url;
|
||||
bool isHomeServer;
|
||||
bool isAddServerDelegate;
|
||||
bool isDeletable;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
UrlRole = Qt::UserRole + 1, /**< Server URL. */
|
||||
IsHomeServerRole, /**< Whether the server is the local user's home server. */
|
||||
IsAddServerDelegateRole, /**< Whether the item is the add new server delegate. */
|
||||
IsDeletableRole, /**< Whether the item can be deleted from the model. */
|
||||
UrlRole = Qt::UserRole + 1,
|
||||
IsHomeServerRole,
|
||||
IsAddServerDelegateRole,
|
||||
IsDeletableRole,
|
||||
};
|
||||
|
||||
ServerListModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Start a check to see if the given URL is a valid matrix server.
|
||||
*
|
||||
* This function starts the check but due to the requests being asynchronous
|
||||
* the caller will need to watch the serverCheckComplete signal for confirmation.
|
||||
* The server URL should be treated as invalid until the signal is emitted true.
|
||||
*
|
||||
* @sa serverCheckComplete()
|
||||
*/
|
||||
Q_INVOKABLE void checkServer(const QString &url);
|
||||
|
||||
/**
|
||||
* @brief Add a new server to the model.
|
||||
*
|
||||
* The server will also be stored in local cache.
|
||||
*/
|
||||
Q_INVOKABLE void addServer(const QString &url);
|
||||
|
||||
/**
|
||||
* @brief Remove the server at the given index.
|
||||
*
|
||||
* The server will also be removed from local cache.
|
||||
*/
|
||||
Q_INVOKABLE void removeServerAtIndex(int index);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -14,7 +14,6 @@ QHash<int, QByteArray> StateModel::roleNames() const
|
||||
}
|
||||
QVariant StateModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto row = index.row();
|
||||
switch (role) {
|
||||
case TypeRole:
|
||||
@@ -24,18 +23,13 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
|
||||
case SourceRole:
|
||||
return QJsonDocument(m_room->currentState().events()[m_room->currentState().events().keys()[row]]->fullJson()).toJson();
|
||||
}
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
int StateModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
#ifdef QUOTIENT_07
|
||||
return m_room->currentState().events().size();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
NeoChatRoom *StateModel::room() const
|
||||
|
||||
@@ -131,11 +131,7 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
|
||||
if (avatarUrl.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
return avatarUrl.url().remove(0, 6);
|
||||
#else
|
||||
return avatarUrl.remove(0, 6);
|
||||
#endif
|
||||
}
|
||||
if (role == UserIDRole) {
|
||||
return user.userId;
|
||||
|
||||
@@ -44,13 +44,9 @@ void UserListModel::setRoom(NeoChatRoom *room)
|
||||
std::sort(m_users.begin(), m_users.end(), room->memberSorter());
|
||||
}
|
||||
for (User *user : std::as_const(m_users)) {
|
||||
#ifdef QUOTIENT_07
|
||||
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
|
||||
avatarChanged(user, m_currentRoom);
|
||||
});
|
||||
#else
|
||||
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
|
||||
#endif
|
||||
}
|
||||
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
|
||||
setRoom(nullptr);
|
||||
@@ -96,15 +92,14 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant::fromValue(user);
|
||||
}
|
||||
if (role == PowerLevelRole) {
|
||||
auto pl = m_currentRoom->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!pl) {
|
||||
return 0;
|
||||
}
|
||||
return pl->powerLevelForUser(user->id());
|
||||
}
|
||||
if (role == PowerLevelStringRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
|
||||
#else
|
||||
auto pl = m_currentRoom->getCurrentState<RoomPowerLevelsEvent>();
|
||||
#endif
|
||||
// User might not in the room yet, in this case pl can be nullptr.
|
||||
// e.g. When invited but user not accepted or denied the invitation.
|
||||
if (!pl) {
|
||||
@@ -143,13 +138,9 @@ void UserListModel::userAdded(Quotient::User *user)
|
||||
beginInsertRows(QModelIndex(), pos, pos);
|
||||
m_users.insert(pos, user);
|
||||
endInsertRows();
|
||||
#ifdef QUOTIENT_07
|
||||
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
|
||||
avatarChanged(user, m_currentRoom);
|
||||
});
|
||||
#else
|
||||
connect(user, &Quotient::User::avatarChanged, this, &UserListModel::avatarChanged);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserListModel::userRemoved(Quotient::User *user)
|
||||
@@ -188,13 +179,9 @@ void UserListModel::refreshAll()
|
||||
std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter());
|
||||
}
|
||||
for (User *user : std::as_const(m_users)) {
|
||||
#ifdef QUOTIENT_07
|
||||
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
|
||||
avatarChanged(user, m_currentRoom);
|
||||
});
|
||||
#else
|
||||
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
|
||||
#endif
|
||||
}
|
||||
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
|
||||
setRoom(nullptr);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "neochataccountregistry.h"
|
||||
|
||||
#include <connection.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
void AccountRegistry::add(Connection *c)
|
||||
{
|
||||
if (m_accounts.contains(c))
|
||||
return;
|
||||
beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
|
||||
m_accounts += c;
|
||||
endInsertRows();
|
||||
emit accountCountChanged();
|
||||
}
|
||||
|
||||
void AccountRegistry::drop(Connection *c)
|
||||
{
|
||||
beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
|
||||
m_accounts.removeOne(c);
|
||||
endRemoveRows();
|
||||
Q_ASSERT(!m_accounts.contains(c));
|
||||
emit accountCountChanged();
|
||||
}
|
||||
|
||||
bool AccountRegistry::isLoggedIn(const QString &userId) const
|
||||
{
|
||||
return std::any_of(m_accounts.cbegin(), m_accounts.cend(), [&userId](Connection *a) {
|
||||
return a->userId() == userId;
|
||||
});
|
||||
}
|
||||
|
||||
bool AccountRegistry::contains(Connection *c) const
|
||||
{
|
||||
return m_accounts.contains(c);
|
||||
}
|
||||
|
||||
AccountRegistry::AccountRegistry() = default;
|
||||
|
||||
QVariant AccountRegistry::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= m_accounts.count()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto account = m_accounts[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case ConnectionRole:
|
||||
return QVariant::fromValue(account);
|
||||
case UserIdRole:
|
||||
return QVariant::fromValue(account->userId());
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int AccountRegistry::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountRegistry::roleNames() const
|
||||
{
|
||||
return {{ConnectionRole, "connection"}, {UserIdRole, "userId"}};
|
||||
}
|
||||
|
||||
bool AccountRegistry::isEmpty() const
|
||||
{
|
||||
return m_accounts.isEmpty();
|
||||
}
|
||||
|
||||
int AccountRegistry::count() const
|
||||
{
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
const QVector<Connection *> AccountRegistry::accounts() const
|
||||
{
|
||||
return m_accounts;
|
||||
}
|
||||
|
||||
Connection *AccountRegistry::get(const QString &userId)
|
||||
{
|
||||
for (const auto &connection : m_accounts) {
|
||||
if (connection->userId() == userId) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
|
||||
class AccountRegistry : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int accountCount READ count NOTIFY accountCountChanged);
|
||||
|
||||
public:
|
||||
enum EventRoles {
|
||||
ConnectionRole = Qt::UserRole + 1,
|
||||
UserIdRole = Qt::DisplayRole,
|
||||
};
|
||||
|
||||
static AccountRegistry &instance()
|
||||
{
|
||||
static AccountRegistry _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
const QVector<Connection *> accounts() const;
|
||||
void add(Connection *a);
|
||||
void drop(Connection *a);
|
||||
bool isLoggedIn(const QString &userId) const;
|
||||
bool isEmpty() const;
|
||||
int count() const;
|
||||
bool contains(Connection *) const;
|
||||
Connection *get(const QString &userId);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void accountCountChanged();
|
||||
|
||||
private:
|
||||
AccountRegistry();
|
||||
|
||||
QVector<Connection *> m_accounts;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <csapi/room_state.h>
|
||||
#include <csapi/typing.h>
|
||||
#include <events/encryptionevent.h>
|
||||
#include <events/eventrelation.h>
|
||||
#include <events/reactionevent.h>
|
||||
#include <events/redactionevent.h>
|
||||
#include <events/roomavatarevent.h>
|
||||
@@ -31,23 +32,19 @@
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/roompowerlevelsevent.h>
|
||||
#include <events/simplestateevents.h>
|
||||
#include <events/stickerevent.h>
|
||||
#include <eventstats.h>
|
||||
#include <jobs/downloadfilejob.h>
|
||||
#ifndef QUOTIENT_07
|
||||
#include <joinstate.h>
|
||||
#endif
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "joinrulesevent.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatuser.h"
|
||||
#include "notificationsmanager.h"
|
||||
#ifdef QUOTIENT_07
|
||||
#include "pollevent.h"
|
||||
#include "pollhandler.h"
|
||||
#endif
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "stickerevent.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
@@ -85,7 +82,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
if (this->joinState() != JoinState::Invite) {
|
||||
return;
|
||||
}
|
||||
const QString senderId = getCurrentState<RoomMemberEvent>(localUser()->id())->senderId();
|
||||
const QString senderId = currentState().get<RoomMemberEvent>(localUser()->id())->senderId();
|
||||
QImage avatar_image;
|
||||
if (!user(senderId)->avatarUrl(this).isEmpty()) {
|
||||
avatar_image = user(senderId)->avatar(128, this);
|
||||
@@ -174,17 +171,9 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
|
||||
} else {
|
||||
content = new EventContent::FileContent(url, fileInfo.size(), mime, fileInfo.fileName());
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, content);
|
||||
#else
|
||||
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
|
||||
#endif
|
||||
setHasFileUploading(true);
|
||||
#ifdef QUOTIENT_07
|
||||
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
|
||||
#else
|
||||
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
|
||||
#endif
|
||||
if (id == txnId) {
|
||||
setFileUploadingProgress(0);
|
||||
setHasFileUploading(false);
|
||||
@@ -267,7 +256,7 @@ const RoomEvent *NeoChatRoom::lastEvent() const
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (event->isStateEvent() && static_cast<const StateEventBase &>(*event).repeatsState()) {
|
||||
if (event->isStateEvent() && static_cast<const StateEvent &>(*event).repeatsState()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -292,11 +281,9 @@ const RoomEvent *NeoChatRoom::lastEvent() const
|
||||
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -317,8 +304,7 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
||||
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
|
||||
+ eventToString(*event, format, stripNewlines);
|
||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event, format, stripNewlines);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -336,7 +322,7 @@ void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
|
||||
}
|
||||
if (auto *e = ti.viewAs<RoomMessageEvent>()) {
|
||||
const auto &text = e->plainBody();
|
||||
if (text.contains(localUserId) || text.contains(roomMembername(localUserId))) {
|
||||
if (text.contains(localUserId) || text.contains(safeMemberName(localUserId))) {
|
||||
highlights.insert(e);
|
||||
}
|
||||
}
|
||||
@@ -359,7 +345,7 @@ void NeoChatRoom::onAddHistoricalTimelineEvents(rev_iter_t from)
|
||||
void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*after*/)
|
||||
{
|
||||
if (const auto &e = eventCast<const ReactionEvent>(&prevEvent)) {
|
||||
if (auto relatedEventId = e->relation().eventId; !relatedEventId.isEmpty()) {
|
||||
if (auto relatedEventId = e->eventId(); !relatedEventId.isEmpty()) {
|
||||
Q_EMIT updatedEvent(relatedEventId);
|
||||
}
|
||||
}
|
||||
@@ -367,9 +353,8 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
|
||||
|
||||
void NeoChatRoom::countChanged()
|
||||
{
|
||||
if (displayed() && !hasUnreadMessages()) {
|
||||
resetNotificationCount();
|
||||
resetHighlightCount();
|
||||
if (displayed() && unreadStats().empty()) {
|
||||
setReadReceipt(lastEvent()->id());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,11 +427,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
using namespace Quotient;
|
||||
#ifdef QUOTIENT_07
|
||||
return switchOnType(
|
||||
#else
|
||||
return visit(
|
||||
#endif
|
||||
evt,
|
||||
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
@@ -492,14 +473,9 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == MembershipType::Leave) {
|
||||
#ifdef QUOTIENT_07
|
||||
if (e.membership() == Membership::Leave) {
|
||||
if (e.prevContent() && e.prevContent()->displayName) {
|
||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||
#else
|
||||
if (e.prevContent() && e.prevContent()->displayName.isEmpty()) {
|
||||
subjectName = sanitized(e.prevContent()->displayName).toHtmlEscaped();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +486,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case MembershipType::Invite:
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||
if (!e.reason().isEmpty()) {
|
||||
@@ -519,13 +495,13 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
return text;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case MembershipType::Join: {
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
if (!e.reason().isEmpty()) {
|
||||
@@ -535,23 +511,19 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (e.displayName().isEmpty()) {
|
||||
if (e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.displayName().toHtmlEscaped());
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (e.avatarUrl().isEmpty()) {
|
||||
if (e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
#ifdef QUOTIENT_07
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
#else
|
||||
} else if (e.prevContent()->avatarUrl.isEmpty()) {
|
||||
#endif
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
@@ -562,18 +534,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case MembershipType::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||
: i18n("left the room");
|
||||
case MembershipType::Ban:
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
if (e.reason().isEmpty()) {
|
||||
return i18n("banned %1 from the room", subjectName);
|
||||
@@ -583,7 +555,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case MembershipType::Knock: {
|
||||
case Membership::Knock: {
|
||||
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
||||
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
||||
}
|
||||
@@ -617,7 +589,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEventBase &e) {
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
@@ -633,21 +605,15 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
},
|
||||
#ifdef QUOTIENT_07
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
},
|
||||
#endif
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return switchOnType(
|
||||
#else
|
||||
return visit(
|
||||
#endif
|
||||
evt,
|
||||
[](const RoomMessageEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
@@ -659,25 +625,25 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
},
|
||||
[](const RoomMemberEvent &e) {
|
||||
switch (e.membership()) {
|
||||
case MembershipType::Invite:
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
return i18n("reinvited someone to the room");
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case MembershipType::Join: {
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == MembershipType::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (e.displayName().isEmpty()) {
|
||||
if (e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name");
|
||||
@@ -687,13 +653,9 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (e.avatarUrl().isEmpty()) {
|
||||
if (e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
#ifdef QUOTIENT_07
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
#else
|
||||
} else if (e.prevContent()->avatarUrl.isEmpty()) {
|
||||
#endif
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
@@ -704,22 +666,22 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case MembershipType::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||
case MembershipType::Ban:
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
return i18n("banned a user from the room");
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case MembershipType::Knock: {
|
||||
case Membership::Knock: {
|
||||
return i18n("requested an invite");
|
||||
}
|
||||
default:;
|
||||
@@ -747,7 +709,7 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEventBase &e) {
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
@@ -762,29 +724,19 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
}
|
||||
return i18n("updated the state");
|
||||
},
|
||||
#ifdef QUOTIENT_07
|
||||
[](const PollStartEvent &e) {
|
||||
Q_UNUSED(e);
|
||||
return i18n("started a poll");
|
||||
},
|
||||
#endif
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||
{
|
||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||
#ifdef QUOTIENT_07
|
||||
if (isJobPending(job)) {
|
||||
#else
|
||||
if (isJobRunning(job)) {
|
||||
#endif
|
||||
connect(job, &BaseJob::success, this, [this, job] {
|
||||
#ifdef QUOTIENT_07
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri().toString()}});
|
||||
#else
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri()}});
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -893,11 +845,11 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
|
||||
|
||||
QStringList redactEventIds; // What if there are multiple reaction events?
|
||||
|
||||
const auto &annotations = relatedEvents(evt, EventRelation::Annotation());
|
||||
const auto &annotations = relatedEvents(evt, EventRelation::AnnotationType);
|
||||
if (!annotations.isEmpty()) {
|
||||
for (const auto &a : annotations) {
|
||||
if (auto e = eventCast<const ReactionEvent>(a)) {
|
||||
if (e->relation().key != reaction) {
|
||||
if (e->key() != reaction) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -920,18 +872,15 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
|
||||
|
||||
bool NeoChatRoom::containsUser(const QString &userID) const
|
||||
{
|
||||
auto u = Room::user(userID);
|
||||
|
||||
if (!u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Room::memberJoinState(u) != JoinState::Leave;
|
||||
return !isMember(userID);
|
||||
}
|
||||
|
||||
bool NeoChatRoom::canSendEvent(const QString &eventType) const
|
||||
{
|
||||
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!plEvent) {
|
||||
return true;
|
||||
}
|
||||
auto pl = plEvent->powerLevelForEvent(eventType);
|
||||
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
||||
|
||||
@@ -940,28 +889,19 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
|
||||
|
||||
bool NeoChatRoom::canSendState(const QString &eventType) const
|
||||
{
|
||||
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!plEvent) {
|
||||
return false;
|
||||
}
|
||||
auto pl = plEvent->powerLevelForState(eventType);
|
||||
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
if (eventType == "m.room.history_visibility" || eventType == "org.matrix.room.preview_urls") {
|
||||
return false;
|
||||
} else {
|
||||
return currentPl >= pl;
|
||||
}
|
||||
#else
|
||||
return currentPl >= pl;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NeoChatRoom::readMarkerLoaded() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
const auto it = findInTimeline(lastFullyReadEventId());
|
||||
#else
|
||||
const auto it = findInTimeline(readMarkerEventId());
|
||||
#endif
|
||||
return it != historyEdge();
|
||||
}
|
||||
|
||||
@@ -972,7 +912,7 @@ bool NeoChatRoom::isInvite() const
|
||||
|
||||
bool NeoChatRoom::isUserBanned(const QString &user) const
|
||||
{
|
||||
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
|
||||
return currentState().get<RoomMemberEvent>(user)->membership() == Membership::Ban;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::htmlSafeDisplayName() const
|
||||
@@ -987,7 +927,7 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reaso
|
||||
|
||||
QString NeoChatRoom::joinRule() const
|
||||
{
|
||||
return getCurrentState<JoinRulesEvent>()->joinRule();
|
||||
return currentState().get<JoinRulesEvent>()->joinRule();
|
||||
}
|
||||
|
||||
void NeoChatRoom::setJoinRule(const QString &joinRule)
|
||||
@@ -996,21 +936,13 @@ void NeoChatRoom::setJoinRule(const QString &joinRule)
|
||||
qWarning() << "Power level too low to set join rules";
|
||||
return;
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
setState("m.room.join_rules", "", QJsonObject{{"join_rule", joinRule}});
|
||||
#else
|
||||
setState<JoinRulesEvent>(QJsonObject{{"type", "m.room.join_rules"}, {"state_key", ""}, {"content", QJsonObject{{"join_rule", joinRule}}}});
|
||||
#endif
|
||||
// Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
|
||||
}
|
||||
|
||||
QString NeoChatRoom::historyVisibility() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return currentState().get("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
|
||||
#else
|
||||
return getCurrentState("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
|
||||
#endif
|
||||
}
|
||||
|
||||
void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
|
||||
@@ -1020,23 +952,14 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
setState("m.room.history_visibility", "", QJsonObject{{"history_visibility", historyVisibilityRule}});
|
||||
#else
|
||||
qWarning() << "Quotient 0.7 required to set history visibility";
|
||||
return;
|
||||
#endif
|
||||
|
||||
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
|
||||
}
|
||||
|
||||
bool NeoChatRoom::defaultUrlPreviewState() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls");
|
||||
#else
|
||||
auto urlPreviewsDisabled = getCurrentState("org.matrix.room.preview_urls");
|
||||
#endif
|
||||
|
||||
// Some rooms will not have this state event set so check for a nullptr return.
|
||||
if (urlPreviewsDisabled != nullptr) {
|
||||
@@ -1082,12 +1005,7 @@ void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
|
||||
*
|
||||
* You just have to set disable to true to disable URL previews by default.
|
||||
*/
|
||||
#ifdef QUOTIENT_07
|
||||
setState("org.matrix.room.preview_urls", "", QJsonObject{{"disable", !defaultUrlPreviewState}});
|
||||
#else
|
||||
qWarning() << "Quotient 0.7 required to set room default url preview setting";
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NeoChatRoom::urlPreviewEnabled() const
|
||||
@@ -1125,32 +1043,20 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
|
||||
qWarning() << "Power level too low to set user power levels";
|
||||
return;
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
if (!isMember(userID)) {
|
||||
#else
|
||||
if (memberJoinState(user(userID)) == JoinState::Join) {
|
||||
#endif
|
||||
qWarning() << "User is not a member of this room so power level cannot be set";
|
||||
return;
|
||||
}
|
||||
int clampPowerLevel = std::clamp(powerLevel, 0, 100);
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
|
||||
#else
|
||||
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
|
||||
#endif
|
||||
auto powerLevelUserOverrides = powerLevelContent["users"].toObject();
|
||||
|
||||
if (powerLevelUserOverrides[userID] != clampPowerLevel) {
|
||||
powerLevelUserOverrides[userID] = clampPowerLevel;
|
||||
powerLevelContent["users"] = powerLevelUserOverrides;
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
setState("m.room.power_levels", "", powerLevelContent);
|
||||
#else
|
||||
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,11 +1068,7 @@ int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
||||
|
||||
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
|
||||
#else
|
||||
const auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||
#endif
|
||||
if (eventName == "ban") {
|
||||
return powerLevelEvent->ban();
|
||||
} else if (eventName == "kick") {
|
||||
@@ -1190,11 +1092,7 @@ int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent)
|
||||
|
||||
void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent)
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
|
||||
#else
|
||||
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
|
||||
#endif
|
||||
int clampPowerLevel = std::clamp(newPowerLevel, 0, 100);
|
||||
int powerLevel = 0;
|
||||
|
||||
@@ -1223,11 +1121,7 @@ void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLev
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
setState("m.room.power_levels", "", powerLevelContent);
|
||||
#else
|
||||
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
|
||||
#endif
|
||||
}
|
||||
|
||||
int NeoChatRoom::defaultUserPowerLevel() const
|
||||
@@ -1450,11 +1344,7 @@ bool NeoChatRoom::isSpace()
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
return creationEvent->roomType() == RoomType::Space;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
PushNotificationState::State NeoChatRoom::pushNotificationState() const
|
||||
@@ -1781,15 +1671,9 @@ void NeoChatRoom::setSavedText(const QString &savedText)
|
||||
|
||||
bool NeoChatRoom::canEncryptRoom() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
#ifdef Quotient_E2EE_ENABLED
|
||||
return !usesEncryption() && canSendState("m.room.encryption");
|
||||
#endif
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
PollHandler *NeoChatRoom::poll(const QString &eventId)
|
||||
{
|
||||
if (!m_polls.contains(eventId)) {
|
||||
@@ -1800,7 +1684,6 @@ PollHandler *NeoChatRoom::poll(const QString &eventId)
|
||||
}
|
||||
return m_polls[eventId];
|
||||
}
|
||||
#endif
|
||||
|
||||
bool NeoChatRoom::downloadTempFile(const QString &eventId)
|
||||
{
|
||||
@@ -1884,27 +1767,3 @@ NeoChatUser *NeoChatRoom::directChatRemoteUser() const
|
||||
{
|
||||
return dynamic_cast<NeoChatUser *>(connection()->directChatUsers(this)[0]);
|
||||
}
|
||||
|
||||
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
|
||||
{
|
||||
QJsonObject locationContent{
|
||||
{"uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
|
||||
};
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
locationContent["description"] = description;
|
||||
}
|
||||
|
||||
QJsonObject content{
|
||||
{"body", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
|
||||
{"msgtype", "m.location"},
|
||||
{"geo_uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
|
||||
{"org.matrix.msc3488.location", locationContent},
|
||||
{"org.matrix.msc3488.asset",
|
||||
QJsonObject{
|
||||
{"type", "m.pin"},
|
||||
}},
|
||||
{"org.matrix.msc1767.text", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
|
||||
};
|
||||
postJson("m.room.message", content);
|
||||
}
|
||||
|
||||
@@ -735,7 +735,6 @@ public:
|
||||
*/
|
||||
void setSavedText(const QString &savedText);
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
/**
|
||||
* @brief Get a PollHandler object for the given event Id.
|
||||
*
|
||||
@@ -747,7 +746,6 @@ public:
|
||||
* @sa PollHandler
|
||||
*/
|
||||
Q_INVOKABLE PollHandler *poll(const QString &eventId);
|
||||
#endif
|
||||
|
||||
private:
|
||||
QSet<const Quotient::RoomEvent *> highlights;
|
||||
@@ -775,9 +773,7 @@ private:
|
||||
QVector<Mention> m_mentions;
|
||||
QVector<Mention> m_editMentions;
|
||||
QString m_savedText;
|
||||
#ifdef QUOTIENT_07
|
||||
QCache<QString, PollHandler> m_polls;
|
||||
#endif
|
||||
|
||||
private Q_SLOTS:
|
||||
void countChanged();
|
||||
@@ -901,13 +897,4 @@ public Q_SLOTS:
|
||||
* This will delete all messages by that user in this room that are currently loaded.
|
||||
*/
|
||||
void deleteMessagesByUser(const QString &user, const QString &reason);
|
||||
|
||||
/**
|
||||
* @brief Sends a location to a room
|
||||
* The event is sent in the migration format as specified in MSC3488
|
||||
* @param lat latitude
|
||||
* @param lon longitude
|
||||
* @param description description for the location
|
||||
*/
|
||||
void sendLocation(float lat, float lon, const QString &description);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,13 @@ NeoChatUser::NeoChatUser(QString userId, Connection *connection)
|
||||
{
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, &NeoChatUser::polishColor);
|
||||
polishColor();
|
||||
if (connection->userId() == id()) {
|
||||
connect(connection, &Connection::accountDataChanged, this, [this](QString type) {
|
||||
if (type == QLatin1String("org.kde.neochat.account_label")) {
|
||||
Q_EMIT accountLabelChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QColor NeoChatUser::color()
|
||||
@@ -39,3 +46,17 @@ void NeoChatUser::polishColor()
|
||||
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
|
||||
setColor(QColor::fromHslF(hueF(), 1, -0.7 * lightness + 0.9, 1));
|
||||
}
|
||||
|
||||
void NeoChatUser::setAccountLabel(const QString &accountLabel)
|
||||
{
|
||||
Q_ASSERT(connection()->user()->id() == id());
|
||||
QJsonObject json;
|
||||
json["account_label"] = accountLabel;
|
||||
connection()->setAccountData("org.kde.neochat.account_label", json);
|
||||
}
|
||||
|
||||
QString NeoChatUser::accountLabel() const
|
||||
{
|
||||
Q_ASSERT(connection()->user()->id() == id());
|
||||
return connection()->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ class NeoChatUser : public Quotient::User
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
|
||||
// Only valid for the local user
|
||||
Q_PROPERTY(QString accountLabel READ accountLabel WRITE setAccountLabel NOTIFY accountLabelChanged)
|
||||
public:
|
||||
NeoChatUser(QString userId, Quotient::Connection *connection);
|
||||
|
||||
@@ -18,8 +20,13 @@ public Q_SLOTS:
|
||||
QColor color();
|
||||
void setColor(const QColor &color);
|
||||
|
||||
// Only valid for the local user
|
||||
QString accountLabel() const;
|
||||
void setAccountLabel(const QString &accountLabel);
|
||||
|
||||
Q_SIGNALS:
|
||||
void colorChanged(QColor _t1);
|
||||
void accountLabelChanged();
|
||||
|
||||
private:
|
||||
QColor m_color;
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
#include <KNotification>
|
||||
#include <KNotificationReplyAction>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <accountregistry.h>
|
||||
#else
|
||||
#include "neochataccountregistry.h"
|
||||
#endif
|
||||
|
||||
#include <connection.h>
|
||||
#include <csapi/pushrules.h>
|
||||
@@ -71,15 +67,8 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
notification->setDefaultAction(i18n("Open NeoChat in this room"));
|
||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
});
|
||||
@@ -119,23 +108,14 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
|
||||
});
|
||||
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
|
||||
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
room->acceptInvitation();
|
||||
notification->close();
|
||||
});
|
||||
connect(notification, &KNotification::action2Activated, this, [room, notification]() {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
RoomManager::instance().leaveRoom(room);
|
||||
notification->close();
|
||||
});
|
||||
connect(notification, &KNotification::closed, this, [this, room]() {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
m_invitations.remove(room->id());
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ void PollHandler::setRoom(NeoChatRoom *room)
|
||||
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
|
||||
for (const auto &event : events) {
|
||||
if (event->is<PollEndEvent>()) {
|
||||
auto pl = m_room->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto pl = m_room->currentState().get<RoomPowerLevelsEvent>();
|
||||
auto userPl = pl->powerLevelForUser(event->senderId());
|
||||
if (event->senderId() == (*m_room->findInTimeline(m_pollStartEventId))->senderId() || userPl >= pl->redact()) {
|
||||
m_hasEnded = true;
|
||||
@@ -75,7 +75,7 @@ void PollHandler::checkLoadRelations()
|
||||
connect(job, &BaseJob::success, this, [this, job]() {
|
||||
for (const auto &event : job->chunk()) {
|
||||
if (event->is<PollEndEvent>()) {
|
||||
auto pl = m_room->getCurrentState<RoomPowerLevelsEvent>();
|
||||
auto pl = m_room->currentState().get<RoomPowerLevelsEvent>();
|
||||
auto userPl = pl->powerLevelForUser(event->senderId());
|
||||
if (event->senderId() == (*m_room->findInTimeline(m_pollStartEventId))->senderId() || userPl >= pl->redact()) {
|
||||
m_hasEnded = true;
|
||||
|
||||
@@ -65,17 +65,8 @@ QQC2.Control {
|
||||
emojiDialog.open()
|
||||
}
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: mapButton
|
||||
icon.name: "globe"
|
||||
property bool isBusy: false
|
||||
text: i18n("Send a Location")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: currentRoom}).open()
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: sendAction
|
||||
@@ -482,9 +473,4 @@ QQC2.Control {
|
||||
textField.cursorPosition = index + format.start.length + format.end.length;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: locationChooserComponent
|
||||
LocationChooser {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtLocation 5.15
|
||||
import QtPositioning 5.15
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Components.AbstractMaximizeComponent {
|
||||
id: root
|
||||
|
||||
required property var room
|
||||
property var location
|
||||
|
||||
title: i18n("Choose a Location")
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send this location")
|
||||
onTriggered: {
|
||||
root.room.sendLocation(root.location.latitude, root.location.longitude, "");
|
||||
root.close();
|
||||
}
|
||||
enabled: !!root.location
|
||||
}
|
||||
]
|
||||
|
||||
content: Map {
|
||||
id: map
|
||||
plugin: Plugin {
|
||||
name: "osm"
|
||||
PluginParameter {
|
||||
name: "osm.useragent"
|
||||
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
|
||||
}
|
||||
PluginParameter {
|
||||
name: "osm.mapping.providersrepository.address"
|
||||
value: "https://autoconfig.kde.org/qtlocation/"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.location = map.toCoordinate(Qt.point(mouseX, mouseY), false)
|
||||
}
|
||||
}
|
||||
|
||||
MapQuickItem {
|
||||
id: point
|
||||
|
||||
visible: root.location
|
||||
anchorPoint.x: sourceItem.width / 2
|
||||
anchorPoint.y: sourceItem.height * 0.85
|
||||
coordinate: root.location
|
||||
autoFadeIn: false
|
||||
|
||||
sourceItem: Kirigami.Icon {
|
||||
width: height
|
||||
height: Kirigami.Units.iconSizes.huge
|
||||
source: "gps"
|
||||
isMask: true
|
||||
color: Kirigami.Theme.highlightColor
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -parent.height / 8
|
||||
width: height
|
||||
height: parent.height / 3 + 1
|
||||
source: "pin"
|
||||
isMask: true
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
312
src/qml/Component/FullScreenImage.qml
Normal file
312
src/qml/Component/FullScreenImage.qml
Normal file
@@ -0,0 +1,312 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
QQC2.Popup {
|
||||
id: root
|
||||
|
||||
property alias source: image.source
|
||||
property string filename
|
||||
property string blurhash: ""
|
||||
property int imageWidth: -1
|
||||
property int imageHeight: -1
|
||||
property var modelData
|
||||
|
||||
parent: QQC2.Overlay.overlay
|
||||
closePolicy: QQC2.Popup.CloseOnEscape
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
modal: true
|
||||
padding: 0
|
||||
background: null
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: avatar
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: modelData.author.name ?? modelData.author.displayName
|
||||
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
|
||||
color: modelData.author.color
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
id: nameLabel
|
||||
|
||||
text: modelData.author.displayName
|
||||
textFormat: Text.PlainText
|
||||
font.weight: Font.Bold
|
||||
color: author.color
|
||||
}
|
||||
QQC2.Label {
|
||||
id: timeLabel
|
||||
|
||||
text: time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: imageLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
text: modelData.display
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Zoom in")
|
||||
Accessible.name: text
|
||||
icon.name: "zoom-in"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
image.scaleFactor = image.scaleFactor + 0.25
|
||||
if (image.scaleFactor > 3) {
|
||||
image.scaleFactor = 3
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Zoom out")
|
||||
Accessible.name: text
|
||||
icon.name: "zoom-out"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
image.scaleFactor = image.scaleFactor - 0.25
|
||||
if (image.scaleFactor < 0.25) {
|
||||
image.scaleFactor = 0.25
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Rotate left")
|
||||
Accessible.name: text
|
||||
icon.name: "image-rotate-left-symbolic"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: image.rotationAngle = image.rotationAngle - 90
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Rotate right")
|
||||
Accessible.name: text
|
||||
icon.name: "image-rotate-right-symbolic"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: image.rotationAngle = image.rotationAngle + 90
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Save as")
|
||||
Accessible.name: text
|
||||
icon.name: "document-save"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Close")
|
||||
Accessible.name: text
|
||||
icon.name: "dialog-close"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
Layout.fillWidth: true
|
||||
visible: image.status !== Image.Ready && root.blurhash === ""
|
||||
running: visible
|
||||
}
|
||||
// Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area.
|
||||
Item {
|
||||
id: imageContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
clip: true
|
||||
|
||||
AnimatedImage {
|
||||
id: image
|
||||
|
||||
property var scaleFactor: 1
|
||||
property int rotationAngle: 0
|
||||
property var rotationInsensitiveWidth: Math.min(root.imageWidth > 0 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2)
|
||||
property var rotationInsensitiveHeight: Math.min(root.imageHeight > 0 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2)
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
|
||||
height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
|
||||
fillMode: Image.PreserveAspectFit
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: image.width
|
||||
height: image.height
|
||||
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
|
||||
visible: root.blurhash !== "" && parent.status !== Image.Ready
|
||||
}
|
||||
|
||||
transform: [
|
||||
Rotation {
|
||||
origin.x: image.width / 2
|
||||
origin.y: image.height / 2
|
||||
angle: image.rotationAngle
|
||||
|
||||
Behavior on angle {
|
||||
RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
},
|
||||
Scale {
|
||||
origin.x: image.width / 2
|
||||
origin.y: image.height / 2
|
||||
xScale: image.scaleFactor
|
||||
yScale: image.scaleFactor
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
Behavior on yScale {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(parent, {
|
||||
author: modelData.author,
|
||||
message: modelData.message,
|
||||
eventId: modelData.eventId,
|
||||
source: modelData.source,
|
||||
file: root.parent,
|
||||
mimeType: modelData.mimeType,
|
||||
progressInfo: modelData.progressInfo,
|
||||
plainMessage: modelData.message,
|
||||
});
|
||||
contextMenu.closeFullscreen.connect(root.close)
|
||||
contextMenu.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: saveAsDialog
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
Config.lastSaveDirectory = folder
|
||||
Config.save()
|
||||
if (!currentFile) {
|
||||
return;
|
||||
}
|
||||
currentRoom.downloadFile(eventId, currentFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
image.scaleFactor = 1
|
||||
image.rotationAngle = 0
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Components.AlbumMaximizeComponent {
|
||||
id: root
|
||||
|
||||
property var modelData
|
||||
|
||||
property list<Components.AlbumModelItem> items: [
|
||||
Components.AlbumModelItem {
|
||||
type: root.modelData.delegateType === MessageEventModel.Image ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
|
||||
source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaUrl
|
||||
tempSource: modelData.content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + modelData.content.info["xyz.amorgan.blurhash"]) : ""
|
||||
caption: modelData.display
|
||||
}
|
||||
]
|
||||
|
||||
model: items
|
||||
initialIndex: 0
|
||||
|
||||
leading: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
id: userAvatar
|
||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: modelData.author.name ?? modelData.author.displayName
|
||||
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
|
||||
color: modelData.author.color
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
id: userLabel
|
||||
text: modelData.author.name ?? modelData.author.displayName
|
||||
color: modelData.author.color
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: dateTimeLabel
|
||||
text: modelData.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
onItemRightClicked: {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(parent, {
|
||||
author: modelData.author,
|
||||
message: modelData.message,
|
||||
eventId: modelData.eventId,
|
||||
source: modelData.source,
|
||||
file: parent,
|
||||
mimeType: modelData.mimeType,
|
||||
progressInfo: modelData.progressInfo,
|
||||
plainMessage: modelData.message,
|
||||
});
|
||||
contextMenu.closeFullscreen.connect(root.close)
|
||||
contextMenu.open();
|
||||
}
|
||||
onSaveItem: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(modelData.eventId)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: saveAsDialog
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: root.saveFolder
|
||||
onAccepted: {
|
||||
Config.lastSaveDirectory = folder
|
||||
Config.save()
|
||||
if (!currentFile) {
|
||||
return;
|
||||
}
|
||||
currentRoom.downloadFile(eventId, currentFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,9 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQml.Models 2.15
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
@@ -116,27 +115,37 @@ TimelineContainer {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
currentRoom.downloadFile(eventId, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
img.QQC2.ToolTip.hide()
|
||||
img.paused = true
|
||||
imageDelegate.ListView.view.interactive = false
|
||||
var popup = maximizeImageComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
modelData: model,
|
||||
})
|
||||
popup.closed.connect(() => {
|
||||
imageDelegate.ListView.view.interactive = true
|
||||
img.paused = false
|
||||
popup.destroy()
|
||||
})
|
||||
popup.open()
|
||||
fullScreenImage.open()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: maximizeImageComponent
|
||||
NeochatMaximizeComponent {}
|
||||
FullScreenImage {
|
||||
id: fullScreenImage
|
||||
filename: eventId
|
||||
source: mediaUrl
|
||||
blurhash: model.content.info["xyz.amorgan.blurhash"]
|
||||
imageWidth: content.info.w
|
||||
imageHeight: content.info.h
|
||||
modelData: model
|
||||
|
||||
onClosed: img.paused = false
|
||||
}
|
||||
|
||||
function downloadAndOpen() {
|
||||
|
||||
@@ -8,7 +8,6 @@ import QtMultimedia 5.15
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
@@ -311,31 +310,6 @@ TimelineContainer {
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: maximizeButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Maximize")
|
||||
icon.name: "view-fullscreen"
|
||||
onTriggered: {
|
||||
videoDelegate.ListView.view.interactive = false
|
||||
vid.pause()
|
||||
var popup = maximizeVideoComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
modelData: model,
|
||||
})
|
||||
popup.closed.connect(() => {
|
||||
videoDelegate.ListView.view.interactive = true
|
||||
popup.destroy()
|
||||
})
|
||||
popup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: maximizeVideoComponent
|
||||
NeochatMaximizeComponent {}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: 4
|
||||
|
||||
@@ -35,7 +35,7 @@ QQC2.Dialog {
|
||||
text: i18n("Sign out")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
Controller.logout(Controller.activeConnection, true);
|
||||
Controller.activeConnection.logout();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ Kirigami.Page {
|
||||
text: i18n("Decline")
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: dialog.session.cancelVerification("m.user", "Declined")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.CancelRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ Labs.MenuBar {
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "New Private Chat…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.activeConnection
|
||||
onTriggered: pushReplaceLayer("qrc:/StartChatPage.qml", {connection: Controller.activeConnection})
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "New Group…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.activeConnection
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
const dialog = createRoomDialog.createObject(root.overlay)
|
||||
|
||||
@@ -6,7 +6,6 @@ import QtQuick.Controls 2.12 as QQC2
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
title: i18n("Loading…")
|
||||
Kirigami.LoadingPlaceholder {
|
||||
id: loadingIndicator
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -36,11 +36,10 @@ QQC2.Menu {
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Logout")
|
||||
icon.name: "list-remove-user"
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.overlay).open()
|
||||
onTriggered: confirmLogoutDialog.open()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: confirmLogoutDialogComponent
|
||||
Dialog.ConfirmLogout {}
|
||||
Dialog.ConfirmLogout {
|
||||
id: confirmLogoutDialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ Kirigami.ScrollablePage {
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.rowForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
|
||||
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ Kirigami.ScrollablePage {
|
||||
while (index++ !== listView.count - 1) {
|
||||
if (condition(listView.itemAtIndex(index))) {
|
||||
listView.currentIndex = index;
|
||||
listView.currentItem.clicked();
|
||||
listView.currentItem.action.trigger();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ Kirigami.ScrollablePage {
|
||||
while (index-- !== 0) {
|
||||
if (condition(listView.itemAtIndex(index))) {
|
||||
listView.currentIndex = index;
|
||||
listView.currentItem.clicked();
|
||||
listView.currentItem.action.trigger();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ Kirigami.BasicListItem {
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
visible: root.categoryVisible || root.filterText.length > 0 || Config.mergeRoomList
|
||||
tooltipVisible: false
|
||||
highlighted: ListView.view.currentIndex === index
|
||||
focus: true
|
||||
icon: undefined
|
||||
|
||||
@@ -76,7 +76,7 @@ QQC2.ToolBar {
|
||||
visible: switchUserButton.checked
|
||||
onVisibleChanged: if (visible) accounts.forceActiveFocus()
|
||||
clip: true
|
||||
model: AccountRegistry
|
||||
model: Accounts
|
||||
|
||||
keyNavigationEnabled: false
|
||||
Keys.onDownPressed: {
|
||||
@@ -168,7 +168,7 @@ QQC2.ToolBar {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: (Controller.activeAccountLabel.length > 0 ? (Controller.activeAccountLabel + " ") : "") + Controller.activeConnection.localUser.id
|
||||
text: (Controller.activeConnection.localUser.accountLabel.length > 0 ? (Controller.activeConnection.localUser.accountLabel + " ") : "") + Controller.activeConnection.localUser.id
|
||||
font.pointSize: displayNameLabel.font.pointSize * 0.8
|
||||
opacity: 0.7
|
||||
textFormat: Text.PlainText
|
||||
|
||||
502
src/qml/Page/RoomListPage.qml
Normal file
502
src/qml/Page/RoomListPage.qml
Normal file
@@ -0,0 +1,502 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
/**
|
||||
* @brief The current width of the room list.
|
||||
*
|
||||
* @note Other objects can access the value but the private function makes sure
|
||||
* that only the internal members can modify it.
|
||||
*/
|
||||
readonly property int currentWidth: _private.currentWidth
|
||||
|
||||
readonly property bool collapsed: Config.collapsed
|
||||
onCollapsedChanged: if (collapsed) {
|
||||
sortFilterRoomListModel.filterText = "";
|
||||
}
|
||||
|
||||
header: ColumnLayout {
|
||||
visible: !page.collapsed
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
id: spaceList
|
||||
property string activeSpaceId: ""
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
clip: true
|
||||
visible: spaceList.count > 0
|
||||
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: SortFilterSpaceListModel {
|
||||
id: sortFilterSpaceListModel
|
||||
sourceModel: roomListModel
|
||||
}
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
id: homeButton
|
||||
icon.name: "home"
|
||||
text: i18nc("@action:button", "Show All Rooms")
|
||||
height: parent.height
|
||||
width: height
|
||||
leftPadding: topPadding
|
||||
rightPadding: topPadding
|
||||
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "home"
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
sortFilterRoomListModel.activeSpaceId = "";
|
||||
spaceList.activeSpaceId = '';
|
||||
listView.positionViewAtIndex(0, ListView.Beginning);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: homeButton.text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
delegate: QQC2.ItemDelegate {
|
||||
required property string avatar
|
||||
required property var currentRoom
|
||||
required property int index
|
||||
required property string id
|
||||
|
||||
height: parent.height
|
||||
width: height
|
||||
leftPadding: topPadding
|
||||
rightPadding: topPadding
|
||||
|
||||
contentItem: Kirigami.Avatar {
|
||||
name: currentRoom.displayName
|
||||
source: avatar !== "" ? "image://mxc/" + avatar : ""
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
spaceList.activeSpaceId = id;
|
||||
sortFilterRoomListModel.activeSpaceId = id;
|
||||
}
|
||||
|
||||
Accessible.name: currentRoom.displayName
|
||||
|
||||
QQC2.ToolTip.text: currentRoom.displayName
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onPressAndHold: {
|
||||
spaceList.createContextMenu(currentRoom)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
onTapped: spaceList.createContextMenu(currentRoom)
|
||||
}
|
||||
}
|
||||
function createContextMenu(room) {
|
||||
const menu = spaceListContextMenu.createObject(page, {room: room})
|
||||
menu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Component {
|
||||
id: spaceListContextMenu
|
||||
SpaceListContextMenu {}
|
||||
}
|
||||
}
|
||||
|
||||
property var enteredRoom
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoomFiltered(condition) {
|
||||
let index = listView.currentIndex;
|
||||
while (index++ !== listView.count - 1) {
|
||||
if (condition(listView.itemAtIndex(index))) {
|
||||
listView.currentIndex = index;
|
||||
listView.currentItem.action.trigger();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousRoomFiltered(condition) {
|
||||
let index = listView.currentIndex;
|
||||
while (index-- !== 0) {
|
||||
if (condition(listView.itemAtIndex(index))) {
|
||||
listView.currentIndex = index;
|
||||
listView.currentItem.action.trigger();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoom() {
|
||||
goToNextRoomFiltered((item) => item.visible);
|
||||
}
|
||||
|
||||
function goToPreviousRoom() {
|
||||
goToPreviousRoomFiltered((item) => item.visible);
|
||||
}
|
||||
|
||||
function goToNextUnreadRoom() {
|
||||
goToNextRoomFiltered((item) => (item.visible && item.hasUnread));
|
||||
}
|
||||
|
||||
function goToPreviousUnreadRoom() {
|
||||
goToPreviousRoomFiltered((item) => (item.visible && item.hasUnread));
|
||||
}
|
||||
|
||||
titleDelegate: ExploreComponent {
|
||||
Layout.fillWidth: true
|
||||
desiredWidth: page.width - Kirigami.Units.largeSpacing
|
||||
collapsed: page.collapsed
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
activeFocusOnTab: true
|
||||
clip: Accounts.count > 1
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
visible: page.collapsed
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: quickView.item.open();
|
||||
}
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: visible ? ListView.view.width : 0
|
||||
height: visible ? Kirigami.Units.gridUnit * 2 : 0
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "search"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||
visible: listView.count == 0
|
||||
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("No rooms found") : i18n("Join some rooms to get started")
|
||||
helpfulAction: Kirigami.Action {
|
||||
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
|
||||
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
|
||||
onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", {
|
||||
connection: Controller.activeConnection,
|
||||
keyword: sortFilterRoomListModel.filterText
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemSelectionModel {
|
||||
id: itemSelection
|
||||
model: roomListModel
|
||||
onCurrentChanged: {
|
||||
listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row
|
||||
}
|
||||
}
|
||||
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
|
||||
onLayoutChanged: {
|
||||
listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row
|
||||
}
|
||||
}
|
||||
|
||||
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
|
||||
section.delegate: page.collapsed ? foldButton : sectionHeader
|
||||
|
||||
Component {
|
||||
id: sectionHeader
|
||||
Kirigami.ListSectionHeader {
|
||||
height: implicitHeight
|
||||
label: roomListModel.categoryName(section)
|
||||
action: Kirigami.Action {
|
||||
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
}
|
||||
contentItem.children: QQC2.ToolButton {
|
||||
icon.name: (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
|
||||
icon.width: Kirigami.Units.iconSizes.small
|
||||
icon.height: Kirigami.Units.iconSizes.small
|
||||
|
||||
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: foldButton
|
||||
Item {
|
||||
width: ListView.view.width
|
||||
height: visible ? width : 0
|
||||
QQC2.ToolButton {
|
||||
id: button
|
||||
anchors.centerIn: parent
|
||||
|
||||
icon.name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section)
|
||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
|
||||
QQC2.ToolTip.text: roomListModel.categoryName(section)
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reuseItems: true
|
||||
currentIndex: -1 // we don't want any room highlighted by default
|
||||
|
||||
delegate: page.collapsed ? collapsedModeListComponent : normalModeListComponent
|
||||
|
||||
Component {
|
||||
id: collapsedModeListComponent
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: ListView.view.width
|
||||
height: visible ? ListView.view.width : 0
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
|
||||
contentItem: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
enabled: text.length !== 0
|
||||
text: name ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListContextMenu
|
||||
RoomListContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: normalModeListComponent
|
||||
Kirigami.BasicListItem {
|
||||
id: roomListItem
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
highlighted: listView.currentIndex === index
|
||||
focus: true
|
||||
icon: undefined
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
bold: unreadCount > 0
|
||||
label: name ?? ""
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: subtitleText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
subtitleItem.visible: !Config.compactRoomList
|
||||
onPressAndHold: {
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
onTapped: createRoomListContextMenu()
|
||||
}
|
||||
|
||||
leading: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
implicitWidth: visible ? height : 0
|
||||
visible: Config.showAvatarInRoomDrawer
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
trailing: RowLayout {
|
||||
Kirigami.Icon {
|
||||
source: "notifications-disabled"
|
||||
enabled: false
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && unreadCount <= 0
|
||||
Accessible.name: i18n("Muted room")
|
||||
}
|
||||
QQC2.Label {
|
||||
id: notificationCountLabel
|
||||
text: notificationCount > 0 ? notificationCount : "●"
|
||||
visible: unreadCount > 0
|
||||
color: Kirigami.Theme.textColor
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
visible: notificationCount > 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
|
||||
opacity: highlightCount > 0 ? 1 : 0.3
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: notificationCountTextMetrics
|
||||
text: notificationCountLabel.text
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
|
||||
Accessible.name: i18n("Configure room")
|
||||
|
||||
action: Kirigami.Action {
|
||||
id: optionAction
|
||||
icon.name: "configure"
|
||||
onTriggered: {
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRoomListContextMenu() {
|
||||
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
|
||||
if (!Kirigami.Settings.isMobile && !Config.compactRoomList) {
|
||||
configButton.visible = true
|
||||
configButton.down = true
|
||||
}
|
||||
menu.closed.connect(function() {
|
||||
configButton.down = undefined
|
||||
configButton.visible = Qt.binding(function() { return roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList })
|
||||
})
|
||||
menu.open()
|
||||
}
|
||||
|
||||
readonly property bool hasUnread: unreadCount > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: UserInfo {
|
||||
width: parent.width
|
||||
visible: !page.collapsed
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
parent: applicationWindow().overlay.parent
|
||||
|
||||
x: page.currentWidth - width / 2
|
||||
width: Kirigami.Units.smallSpacing * 2
|
||||
z: page.z + 1
|
||||
enabled: RoomManager.hasOpenRoom && applicationWindow().width >= Kirigami.Units.gridUnit * 35
|
||||
visible: enabled
|
||||
cursorShape: Qt.SplitHCursor
|
||||
|
||||
property int _lastX
|
||||
|
||||
onPressed: mouse => {
|
||||
_lastX = mouse.x;
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (_lastX == -1) {
|
||||
return;
|
||||
}
|
||||
if (mouse.x > _lastX) {
|
||||
// we moved to the right
|
||||
if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) {
|
||||
// Here we get back directly to a more wide mode.
|
||||
_private.currentWidth = _private.collapseWidth;
|
||||
Config.collapsed = false;
|
||||
} else if (_private.currentWidth >= _private.collapseWidth) {
|
||||
// Increase page width
|
||||
_private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX));
|
||||
}
|
||||
} else if (mouse.x < _lastX) {
|
||||
const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
|
||||
if (tmpWidth < _private.collapseWidth) {
|
||||
_private.currentWidth = Qt.binding(() => _private.collapsedSize + (page.contentItem.QQC2.ScrollBar.vertical.visible ? page.contentItem.QQC2.ScrollBar.vertical.width : 0));
|
||||
Config.collapsed = true;
|
||||
} else {
|
||||
_private.currentWidth = tmpWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Hold the modifiable currentWidth in a private object so that only internal
|
||||
* members can modify it.
|
||||
*/
|
||||
QtObject {
|
||||
id: _private
|
||||
property int currentWidth: Config.collapsed ? collapsedSize : defaultWidth
|
||||
readonly property int defaultWidth: Kirigami.Units.gridUnit * 17
|
||||
readonly property int collapseWidth: Kirigami.Units.gridUnit * 10
|
||||
readonly property int collapsedSize: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3
|
||||
}
|
||||
}
|
||||
@@ -433,6 +433,12 @@ Kirigami.ScrollablePage {
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Kirigami.ScrollablePage {
|
||||
MobileForm.FormTextFieldDelegate {
|
||||
id: accountLabel
|
||||
label: i18n("Label:")
|
||||
text: root.connection ? Controller.activeAccountLabel : ""
|
||||
text: root.connection ? root.connection.localUser.accountLabel : ""
|
||||
}
|
||||
MobileForm.AbstractFormDelegate {
|
||||
Layout.fillWidth: true
|
||||
@@ -112,8 +112,8 @@ Kirigami.ScrollablePage {
|
||||
if (root.connection.localUser.displayName !== name.text) {
|
||||
root.connection.localUser.rename(name.text);
|
||||
}
|
||||
if (Controller.activeAccountLabel !== accountLabel.text) {
|
||||
Controller.activeAccountLabel = accountLabel.text;
|
||||
if (root.connection.localUser.accountLabel !== accountLabel.text) {
|
||||
root.connection.localUser.setAccountLabel(accountLabel.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AccountRegistry
|
||||
model: Accounts
|
||||
delegate: MobileForm.AbstractFormDelegate {
|
||||
Layout.fillWidth: true
|
||||
onClicked: pageSettingStack.pushDialogLayer("qrc:/AccountEditorPage.qml", {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user