Compare commits
30 Commits
work/multi
...
work/fix_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0083199493 | ||
|
|
d4cb27eca4 | ||
|
|
541350e678 | ||
|
|
843deefaf8 | ||
|
|
070d579bc2 | ||
|
|
add283c9fb | ||
|
|
fe4230b5fd | ||
|
|
e8f40d98de | ||
|
|
eba62103a4 | ||
|
|
925393deab | ||
|
|
abe881caf7 | ||
|
|
237a3c9dfb | ||
|
|
9715440854 | ||
|
|
ecdad9f965 | ||
|
|
08711fc927 | ||
|
|
e44cd405b7 | ||
|
|
8945e004e2 | ||
|
|
c04d8d6f59 | ||
|
|
58a73c0208 | ||
|
|
852110debd | ||
|
|
6b71d3c78d | ||
|
|
f3a0adee39 | ||
|
|
6e7b6c9ce0 | ||
|
|
f67cd7deb5 | ||
|
|
931b4b1f9a | ||
|
|
167ed4eca3 | ||
|
|
7d5b2c1b6a | ||
|
|
be7b1e49b4 | ||
|
|
957419070a | ||
|
|
f22107c8ab |
@@ -63,9 +63,9 @@ set_package_properties(KF6 PROPERTIES
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
set_package_properties(KF6Kirigami2 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
if(ANDROID)
|
||||
@@ -81,6 +81,12 @@ else()
|
||||
TYPE RUNTIME
|
||||
)
|
||||
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
||||
|
||||
find_package(ICU 61.0 COMPONENTS uc)
|
||||
set_package_properties(ICU PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Unicode library"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
|
||||
@@ -4,25 +4,26 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
import unittest
|
||||
from appium import webdriver
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from appium import webdriver
|
||||
from appium.options.common.base import AppiumOptions
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
|
||||
class LoginTest(unittest.TestCase):
|
||||
|
||||
mockServerProcess: subprocess.Popen
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
desired_caps = {}
|
||||
desired_caps["app"] = "neochat --ignore-ssl-errors"
|
||||
desired_caps["timeouts"] = {'implicit': 10000}
|
||||
self.driver = webdriver.Remote(
|
||||
command_executor='http://127.0.0.1:4723',
|
||||
desired_capabilities=desired_caps)
|
||||
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
def setUpClass(cls):
|
||||
options = AppiumOptions()
|
||||
options.set_capability("app", "neochat --ignore-ssl-errors")
|
||||
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
|
||||
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
@@ -45,5 +46,6 @@ class LoginTest(unittest.TestCase):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Join some rooms to get started").click()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
|
||||
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
|
||||
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
|
||||
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
|
||||
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
|
||||
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
|
||||
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<svg width="22" height="22" fill="none" version="1.1" id="svg13" xmlns="http://www.w3.org/2000/svg"><style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style>
|
||||
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M3 3H19V14H8.68787L4 18.1019V14H3V3ZM4 13H5V15.8981L8.31213 13H18V4H4V13Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M17 15.2929L14.8536 13.1465L14.1465 13.8536L18 17.7071V13.5H17V15.2929Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 6H15V7H5V6Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 8H13V9H5V8Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 10H11V11H5V10Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 928 B After Width: | Height: | Size: 752 B |
5425
po/ar/neochat.po
5425
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
5160
po/az/neochat.po
5160
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
4498
po/ca/neochat.po
4498
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4990
po/cs/neochat.po
4990
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
5097
po/da/neochat.po
5097
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
4802
po/de/neochat.po
4802
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
5206
po/el/neochat.po
5206
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
5649
po/en_GB/neochat.po
5649
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
4409
po/eo/neochat.po
4409
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
4492
po/es/neochat.po
4492
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
5370
po/eu/neochat.po
5370
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
4991
po/fi/neochat.po
4991
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
5000
po/fr/neochat.po
5000
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
4750
po/hu/neochat.po
4750
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
4981
po/ia/neochat.po
4981
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
4673
po/id/neochat.po
4673
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
5330
po/ie/neochat.po
5330
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
4501
po/it/neochat.po
4501
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
4878
po/ja/neochat.po
4878
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
4427
po/ka/neochat.po
4427
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
4887
po/ko/neochat.po
4887
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
4922
po/lt/neochat.po
4922
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
4482
po/nl/neochat.po
4482
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
4497
po/nn/neochat.po
4497
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
5501
po/pa/neochat.po
5501
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
4456
po/pl/neochat.po
4456
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
4681
po/pt/neochat.po
4681
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
5196
po/pt_BR/neochat.po
5196
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
5172
po/ru/neochat.po
5172
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
5314
po/sk/neochat.po
5314
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
4618
po/sl/neochat.po
4618
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
4679
po/sv/neochat.po
4679
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
4457
po/ta/neochat.po
4457
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
5750
po/tok/neochat.po
5750
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
4463
po/tr/neochat.po
4463
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
5021
po/uk/neochat.po
5021
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
5269
po/zh_CN/neochat.po
5269
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
4878
po/zh_TW/neochat.po
4878
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,12 @@ add_library(neochat STATIC
|
||||
models/userfiltermodel.h
|
||||
models/publicroomlistmodel.cpp
|
||||
models/publicroomlistmodel.h
|
||||
models/spacechildrenmodel.cpp
|
||||
models/spacechildrenmodel.h
|
||||
models/spacechildsortfiltermodel.cpp
|
||||
models/spacechildsortfiltermodel.h
|
||||
models/spacetreeitem.cpp
|
||||
models/spacetreeitem.h
|
||||
models/userdirectorylistmodel.cpp
|
||||
models/userdirectorylistmodel.h
|
||||
models/pushrulemodel.cpp
|
||||
@@ -127,6 +133,8 @@ add_library(neochat STATIC
|
||||
mediasizehelper.h
|
||||
eventhandler.cpp
|
||||
enums/delegatetype.h
|
||||
roomlastmessageprovider.cpp
|
||||
roomlastmessageprovider.h
|
||||
)
|
||||
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
@@ -207,7 +215,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/Sso.qml
|
||||
qml/UserDetailDialog.qml
|
||||
qml/CreateRoomDialog.qml
|
||||
qml/CreateSpaceDialog.qml
|
||||
qml/EmojiDialog.qml
|
||||
qml/OpenFileDialog.qml
|
||||
qml/KeyVerificationDialog.qml
|
||||
@@ -271,6 +278,9 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/RoomMedia.qml
|
||||
qml/ChooseRoomDialog.qml
|
||||
qml/ShareAction.qml
|
||||
qml/SpaceHomePage.qml
|
||||
qml/SpaceHierarchyDelegate.qml
|
||||
qml/RemoveChildDialog.qml
|
||||
RESOURCES
|
||||
qml/confetti.png
|
||||
qml/glowdot.png
|
||||
@@ -319,9 +329,10 @@ if(NOT ANDROID)
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||
endif()
|
||||
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem)
|
||||
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem ICU::uc)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
|
||||
@@ -93,8 +93,9 @@ public:
|
||||
Q_ENUM(PasswordStatus)
|
||||
|
||||
static Controller &instance();
|
||||
static Controller *create(QQmlEngine *, QJSEngine *)
|
||||
static Controller *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
@@ -231,8 +231,11 @@ bool EventHandler::isHidden()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
|
||||
return true;
|
||||
if (m_event->isStateEvent()) {
|
||||
auto *stateEvent = eventCast<const StateEvent>(m_event);
|
||||
if (stateEvent && stateEvent->repeatsState()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// isReplacement?
|
||||
@@ -986,3 +989,5 @@ QString EventHandler::getReadMarkersString() const
|
||||
readMarkersString.chop(2);
|
||||
return readMarkersString;
|
||||
}
|
||||
|
||||
#include "moc_eventhandler.cpp"
|
||||
|
||||
46
src/main.cpp
46
src/main.cpp
@@ -34,69 +34,25 @@
|
||||
|
||||
#include "neochat-version.h"
|
||||
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/keyverificationsession.h>
|
||||
#include <Quotient/networkaccessmanager.h>
|
||||
#include <Quotient/room.h>
|
||||
#include <Quotient/user.h>
|
||||
#include <Quotient/util.h>
|
||||
|
||||
#include "actionshandler.h"
|
||||
#include "blurhashimageprovider.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "controller.h"
|
||||
#include "delegatesizehelper.h"
|
||||
#include "enums/delegatetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "locationhelper.h"
|
||||
#include "logger.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "mediasizehelper.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
#include "models/devicesproxymodel.h"
|
||||
#include "models/emojimodel.h"
|
||||
#include "models/emoticonfiltermodel.h"
|
||||
#include "models/imagepacksmodel.h"
|
||||
#include "models/livelocationsmodel.h"
|
||||
#include "models/locationsmodel.h"
|
||||
#include "models/mediamessagefiltermodel.h"
|
||||
#include "models/messageeventmodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/publicroomlistmodel.h"
|
||||
#include "models/pushrulemodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "models/roomlistmodel.h"
|
||||
#include "models/searchmodel.h"
|
||||
#include "models/serverlistmodel.h"
|
||||
#include "models/sortfilterroomlistmodel.h"
|
||||
#include "models/sortfilterspacelistmodel.h"
|
||||
#include "models/statefiltermodel.h"
|
||||
#include "models/stickermodel.h"
|
||||
#include "models/userdirectorylistmodel.h"
|
||||
#include "models/userfiltermodel.h"
|
||||
#include "models/userlistmodel.h"
|
||||
#include "models/webshortcutmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "pollhandler.h"
|
||||
#include "roommanager.h"
|
||||
#include "spacehierarchycache.h"
|
||||
#include "urlhelper.h"
|
||||
#include "windowcontroller.h"
|
||||
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
#include "models/completionmodel.h"
|
||||
#include "models/statemodel.h"
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
#endif
|
||||
#include "registration.h"
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
#include <Windows.h>
|
||||
@@ -223,7 +179,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
|
||||
|
||||
// qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
|
||||
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
|
||||
@@ -51,8 +51,9 @@ public:
|
||||
static CustomEmojiModel _instance;
|
||||
return _instance;
|
||||
}
|
||||
static CustomEmojiModel *create(QQmlEngine *, QJSEngine *)
|
||||
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,9 @@ public:
|
||||
static EmojiModel _instance;
|
||||
return _instance;
|
||||
}
|
||||
static EmojiModel *create(QQmlEngine *, QJSEngine *)
|
||||
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[ShowReactionsRole] = "showReactions";
|
||||
roles[AuthorIdRole] = "authorId";
|
||||
roles[VerifiedRole] = "verified";
|
||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||
roles[IsRedactedRole] = "isRedacted";
|
||||
@@ -659,10 +658,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return m_reactionModels.contains(evt.id());
|
||||
}
|
||||
|
||||
if (role == AuthorIdRole) {
|
||||
return evt.senderId();
|
||||
}
|
||||
|
||||
if (role == VerifiedRole) {
|
||||
if (evt.originalEvent()) {
|
||||
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
|
||||
|
||||
@@ -73,8 +73,6 @@ public:
|
||||
ReactionRole, /**< List model for this event. */
|
||||
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
|
||||
|
||||
AuthorIdRole, /**< Matrix ID of the message author. */
|
||||
|
||||
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
#include "reactionmodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
#ifdef HAVE_ICU
|
||||
#include <QTextBoundaryFinder>
|
||||
#include <QTextCharFormat>
|
||||
#include <unicode/uchar.h>
|
||||
#include <unicode/urename.h>
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
@@ -29,11 +35,38 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
const auto &reaction = m_reactions.at(index.row());
|
||||
|
||||
if (role == TextRole) {
|
||||
const auto isEmoji = [](const QString &text) {
|
||||
#ifdef HAVE_ICU
|
||||
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
|
||||
int from = 0;
|
||||
while (finder.toNextBoundary() != -1) {
|
||||
auto to = finder.position();
|
||||
if (text[from].isSpace()) {
|
||||
from = to;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto first = text.mid(from, to - from).toUcs4()[0];
|
||||
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
|
||||
return false;
|
||||
}
|
||||
from = to;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
};
|
||||
|
||||
const auto reactionText = isEmoji(reaction.reaction)
|
||||
? QStringLiteral("<span style=\"font-family: 'emoji';\">") + reaction.reaction + QStringLiteral("</span>")
|
||||
: reaction.reaction;
|
||||
|
||||
if (role == TextContentRole) {
|
||||
if (reaction.authors.count() > 1) {
|
||||
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
|
||||
return QStringLiteral("%1 %2").arg(reactionText, QString::number(reaction.authors.count()));
|
||||
} else {
|
||||
return reaction.reaction;
|
||||
return reactionText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +97,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
"%2 reacted with %3",
|
||||
reaction.authors.count(),
|
||||
text,
|
||||
reaction.reaction);
|
||||
reactionText);
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -101,7 +134,7 @@ void ReactionModel::setReactions(QList<Reaction> reactions)
|
||||
QHash<int, QByteArray> ReactionModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TextRole, "text"},
|
||||
{TextContentRole, "textContent"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ToolTipRole, "toolTip"},
|
||||
{AuthorsRole, "authors"},
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
TextRole = Qt::DisplayRole, /**< The text to show in the reaction. */
|
||||
TextContentRole = Qt::DisplayRole, /**< The text to show in the reaction. */
|
||||
ReactionRole, /**< The reaction emoji. */
|
||||
ToolTipRole, /**< The tool tip to show for the reaction. */
|
||||
AuthorsRole, /**< The list of authors who sent the given reaction. */
|
||||
|
||||
334
src/models/spacechildrenmodel.cpp
Normal file
334
src/models/spacechildrenmodel.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
// 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
|
||||
|
||||
#include "spacechildrenmodel.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
{
|
||||
m_rootItem = new SpaceTreeItem();
|
||||
}
|
||||
|
||||
SpaceChildrenModel::~SpaceChildrenModel()
|
||||
{
|
||||
delete m_rootItem;
|
||||
}
|
||||
|
||||
NeoChatRoom *SpaceChildrenModel::space() const
|
||||
{
|
||||
return m_space;
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::setSpace(NeoChatRoom *space)
|
||||
{
|
||||
if (space == m_space) {
|
||||
return;
|
||||
}
|
||||
// disconnect the new room signal from the old connection in case it is different.
|
||||
if (m_space != nullptr) {
|
||||
disconnect(m_space->connection(), &Quotient::Connection::loadedRoomState, this, nullptr);
|
||||
}
|
||||
|
||||
m_space = space;
|
||||
Q_EMIT spaceChanged();
|
||||
|
||||
for (auto job : m_currentJobs) {
|
||||
if (job) {
|
||||
job->abandon();
|
||||
}
|
||||
}
|
||||
m_currentJobs.clear();
|
||||
|
||||
auto connection = m_space->connection();
|
||||
connect(connection, &Quotient::Connection::loadedRoomState, this, [this](Quotient::Room *room) {
|
||||
if (m_pendingChildren.contains(room->name())) {
|
||||
m_pendingChildren.removeAll(room->name());
|
||||
refreshModel();
|
||||
}
|
||||
});
|
||||
connect(m_space, &Quotient::Room::changed, this, [this]() {
|
||||
refreshModel();
|
||||
});
|
||||
|
||||
refreshModel();
|
||||
}
|
||||
|
||||
bool SpaceChildrenModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::refreshModel()
|
||||
{
|
||||
beginResetModel();
|
||||
m_replacedRooms.clear();
|
||||
delete m_rootItem;
|
||||
m_loading = true;
|
||||
Q_EMIT loadingChanged();
|
||||
m_rootItem = new SpaceTreeItem(nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
|
||||
endResetModel();
|
||||
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), Quotient::none, Quotient::none, 1);
|
||||
m_currentJobs.append(job);
|
||||
connect(job, &Quotient::BaseJob::success, this, [this, job]() {
|
||||
insertChildren(job->rooms());
|
||||
});
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent)
|
||||
{
|
||||
SpaceTreeItem *parentItem = getItem(parent);
|
||||
|
||||
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
|
||||
children.erase(children.begin());
|
||||
}
|
||||
|
||||
// If this is the first set of children added to the root item then we need to
|
||||
// set it so that we are no longer loading.
|
||||
if (rowCount(QModelIndex()) == 0 && !children.empty()) {
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
beginInsertRows(parent, parentItem->childCount(), parentItem->childCount() + children.size() - 1);
|
||||
for (unsigned long i = 0; i < children.size(); ++i) {
|
||||
if (children[i].roomId == m_space->id() || children[i].roomId == parentItem->id()) {
|
||||
continue;
|
||||
} else {
|
||||
int insertRow = parentItem->childCount();
|
||||
if (const auto room = m_space->connection()->room(children[i].roomId)) {
|
||||
const auto predecessorId = room->predecessorId();
|
||||
if (!predecessorId.isEmpty()) {
|
||||
m_replacedRooms += predecessorId;
|
||||
}
|
||||
const auto successorId = room->successorId();
|
||||
if (!successorId.isEmpty()) {
|
||||
m_replacedRooms += successorId;
|
||||
}
|
||||
}
|
||||
parentItem->insertChild(insertRow,
|
||||
new SpaceTreeItem(parentItem,
|
||||
children[i].roomId,
|
||||
children[i].name,
|
||||
children[i].canonicalAlias,
|
||||
children[i].topic,
|
||||
children[i].numJoinedMembers,
|
||||
children[i].avatarUrl,
|
||||
children[i].guestCanJoin,
|
||||
children[i].worldReadable,
|
||||
children[i].roomType == QLatin1String("m.space")));
|
||||
if (children[i].childrenState.size() > 0) {
|
||||
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
|
||||
m_currentJobs.append(job);
|
||||
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
|
||||
insertChildren(job->rooms(), index(insertRow, 0, parent));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceChildrenModel::getItem(const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid()) {
|
||||
SpaceTreeItem *item = static_cast<SpaceTreeItem *>(index.internalPointer());
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return m_rootItem;
|
||||
}
|
||||
|
||||
QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
SpaceTreeItem *child = getItem(index);
|
||||
if (role == DisplayNameRole) {
|
||||
auto displayName = child->name();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = child->canonicalAlias();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return child->id();
|
||||
}
|
||||
if (role == AvatarUrlRole) {
|
||||
return child->avatarUrl();
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return child->topic();
|
||||
}
|
||||
if (role == RoomIDRole) {
|
||||
return child->id();
|
||||
}
|
||||
if (role == AliasRole) {
|
||||
return child->canonicalAlias();
|
||||
}
|
||||
if (role == MemberCountRole) {
|
||||
return child->memberCount();
|
||||
}
|
||||
if (role == AllowGuestsRole) {
|
||||
return child->allowGuests();
|
||||
}
|
||||
if (role == WorldReadableRole) {
|
||||
return child->worldReadable();
|
||||
}
|
||||
if (role == IsJoinedRole) {
|
||||
return child->isJoined();
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return child->isSpace();
|
||||
}
|
||||
if (role == CanAddChildrenRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->canSendState(QLatin1String("m.space.child"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == ParentDisplayNameRole) {
|
||||
const auto parent = child->parentItem();
|
||||
auto displayName = parent->name();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = parent->canonicalAlias();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return parent->id();
|
||||
}
|
||||
if (role == CanSetParentRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->canSendState(QLatin1String("m.space.parent"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == IsDeclaredParentRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->currentState().contains(QLatin1String("m.space.parent"), child->parentItem()->id());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == CanRemove) {
|
||||
const auto parent = child->parentItem();
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(parent->id()))) {
|
||||
return room->canSendState(QLatin1String("m.space.child"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == ParentRoomRole) {
|
||||
if (const auto parentRoom = static_cast<NeoChatRoom *>(m_space->connection()->room(child->parentItem()->id()))) {
|
||||
return QVariant::fromValue(parentRoom);
|
||||
}
|
||||
return QVariant::fromValue(nullptr);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex SpaceChildrenModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *parentItem = getItem(parent);
|
||||
if (!parentItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *childItem = parentItem->child(row);
|
||||
if (childItem) {
|
||||
return createIndex(row, column, childItem);
|
||||
}
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex SpaceChildrenModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *childItem = static_cast<SpaceTreeItem *>(index.internalPointer());
|
||||
SpaceTreeItem *parentItem = childItem->parentItem();
|
||||
|
||||
if (parentItem == m_rootItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
return createIndex(parentItem->row(), 0, parentItem);
|
||||
}
|
||||
|
||||
int SpaceChildrenModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
SpaceTreeItem *parentItem;
|
||||
if (parent.column() > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!parent.isValid()) {
|
||||
parentItem = m_rootItem;
|
||||
} else {
|
||||
parentItem = static_cast<SpaceTreeItem *>(parent.internalPointer());
|
||||
}
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
int SpaceChildrenModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarUrlRole] = "avatarUrl";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[RoomIDRole] = "roomId";
|
||||
roles[MemberCountRole] = "memberCount";
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[AliasRole] = "alias";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[CanAddChildrenRole] = "canAddChildren";
|
||||
roles[ParentDisplayNameRole] = "parentDisplayName";
|
||||
roles[CanSetParentRole] = "canSetParent";
|
||||
roles[IsDeclaredParentRole] = "isDeclaredParent";
|
||||
roles[CanRemove] = "canRemove";
|
||||
roles[ParentRoomRole] = "parentRoom";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
bool SpaceChildrenModel::isRoomReplaced(const QString &roomId) const
|
||||
{
|
||||
return m_replacedRooms.contains(roomId);
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::addPendingChild(const QString &childName)
|
||||
{
|
||||
m_pendingChildren += childName;
|
||||
}
|
||||
|
||||
#include "moc_spacechildrenmodel.cpp"
|
||||
144
src/models/spacechildrenmodel.h
Normal file
144
src/models/spacechildrenmodel.h
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/csapi/space_hierarchy.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "spacetreeitem.h"
|
||||
|
||||
/**
|
||||
* @class SpaceChildrenModel
|
||||
*
|
||||
* Create a model that contains a list of the child rooms for any given space id.
|
||||
*/
|
||||
class SpaceChildrenModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The current space that the hierarchy is being generated for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *space READ space WRITE setSpace NOTIFY spaceChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model is loading the initial set of children.
|
||||
*/
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
DisplayNameRole = Qt::DisplayRole,
|
||||
AvatarUrlRole,
|
||||
TopicRole,
|
||||
RoomIDRole,
|
||||
AliasRole,
|
||||
MemberCountRole,
|
||||
AllowGuestsRole,
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
IsSpaceRole,
|
||||
CanAddChildrenRole,
|
||||
ParentDisplayNameRole,
|
||||
CanSetParentRole,
|
||||
IsDeclaredParentRole,
|
||||
CanRemove,
|
||||
ParentRoomRole,
|
||||
};
|
||||
|
||||
explicit SpaceChildrenModel(QObject *parent = nullptr);
|
||||
~SpaceChildrenModel();
|
||||
|
||||
NeoChatRoom *space() const;
|
||||
void setSpace(NeoChatRoom *space);
|
||||
|
||||
bool loading() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the index of the item in the model specified by the given row, column and parent index.
|
||||
*
|
||||
* @sa QAbstractItemModel::index
|
||||
*/
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the parent of the model item with the given index.
|
||||
*
|
||||
* If the item has no parent, an invalid QModelIndex is returned.
|
||||
*
|
||||
* @sa QAbstractItemModel::parent
|
||||
*/
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of columns in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::columnCount
|
||||
*/
|
||||
int columnCount(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 Whether the room has been replaced.
|
||||
*
|
||||
* @note This information is only available if the local user is either a member
|
||||
* of the replaced room or is a member of the successor room as currently
|
||||
* there is no other way to obtain the required information.
|
||||
*/
|
||||
bool isRoomReplaced(const QString &roomId) const;
|
||||
|
||||
/**
|
||||
* @brief Add the name of new child room that is expected to be added soon.
|
||||
*
|
||||
* A pending child is one where Quotient::Connection::createRoom has been called
|
||||
* but the room hasn't synced with the server yet. This list is used to check
|
||||
* whether a new room loading should trigger a refresh of the model, as we only
|
||||
* want to trigger a refresh if the loading room is part of this space.
|
||||
*/
|
||||
Q_INVOKABLE void addPendingChild(const QString &childName);
|
||||
|
||||
Q_SIGNALS:
|
||||
void spaceChanged();
|
||||
void loadingChanged();
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_space = nullptr;
|
||||
SpaceTreeItem *m_rootItem;
|
||||
|
||||
bool m_loading = false;
|
||||
QList<QPointer<Quotient::GetSpaceHierarchyJob>> m_currentJobs;
|
||||
QList<QString> m_pendingChildren;
|
||||
|
||||
QList<QString> m_replacedRooms;
|
||||
|
||||
SpaceTreeItem *getItem(const QModelIndex &index) const;
|
||||
|
||||
void refreshModel();
|
||||
void insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent = QModelIndex());
|
||||
};
|
||||
46
src/models/spacechildsortfiltermodel.cpp
Normal file
46
src/models/spacechildsortfiltermodel.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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
|
||||
|
||||
#include "spacechildsortfiltermodel.h"
|
||||
|
||||
#include "spacechildrenmodel.h"
|
||||
|
||||
SpaceChildSortFilterModel::SpaceChildSortFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
setRecursiveFilteringEnabled(true);
|
||||
sort(0);
|
||||
}
|
||||
|
||||
void SpaceChildSortFilterModel::setFilterText(const QString &filterText)
|
||||
{
|
||||
m_filterText = filterText;
|
||||
Q_EMIT filterTextChanged();
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
QString SpaceChildSortFilterModel::filterText() const
|
||||
{
|
||||
return m_filterText;
|
||||
}
|
||||
|
||||
bool SpaceChildSortFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||
{
|
||||
if (!source_left.data(SpaceChildrenModel::IsSpaceRole).toBool() && source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
if (auto sourceModel = static_cast<SpaceChildrenModel *>(this->sourceModel())) {
|
||||
bool isReplaced = sourceModel->isRoomReplaced(index.data(SpaceChildrenModel::RoomIDRole).toString());
|
||||
bool acceptRoom = index.data(SpaceChildrenModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive);
|
||||
return !isReplaced && acceptRoom;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#include "moc_spacechildsortfiltermodel.cpp"
|
||||
54
src/models/spacechildsortfiltermodel.h
Normal file
54
src/models/spacechildsortfiltermodel.h
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class SpaceChildSortFilterModel
|
||||
*
|
||||
* This class creates a custom QSortFilterProxyModel for filtering and sorting spaces
|
||||
* in a SpaceChildrenModel.
|
||||
*
|
||||
* @sa SpaceChildrenModel
|
||||
*/
|
||||
class SpaceChildSortFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The text to use to filter room names.
|
||||
*/
|
||||
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
public:
|
||||
SpaceChildSortFilterModel(QObject *parent = nullptr);
|
||||
|
||||
void setFilterText(const QString &filterText);
|
||||
[[nodiscard]] QString filterText() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @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 Custom filter function checking if an event type has been filtered out.
|
||||
*
|
||||
* The filter rejects a row if the room is known been replaced or if a search
|
||||
* string is set it will only return rooms that match.
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTextChanged();
|
||||
|
||||
private:
|
||||
QString m_filterText;
|
||||
};
|
||||
140
src/models/spacetreeitem.cpp
Normal file
140
src/models/spacetreeitem.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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
|
||||
|
||||
#include "spacetreeitem.h"
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
SpaceTreeItem::SpaceTreeItem(SpaceTreeItem *parent,
|
||||
const QString &id,
|
||||
const QString &name,
|
||||
const QString &canonicalAlias,
|
||||
const QString &topic,
|
||||
int memberCount,
|
||||
const QUrl &avatarUrl,
|
||||
bool allowGuests,
|
||||
bool worldReadable,
|
||||
bool isSpace)
|
||||
: m_parentItem(parent)
|
||||
, m_id(id)
|
||||
, m_name(name)
|
||||
, m_canonicalAlias(canonicalAlias)
|
||||
, m_topic(topic)
|
||||
, m_memberCount(memberCount)
|
||||
, m_avatarUrl(avatarUrl)
|
||||
, m_allowGuests(allowGuests)
|
||||
, m_worldReadable(worldReadable)
|
||||
, m_isSpace(isSpace)
|
||||
{
|
||||
}
|
||||
|
||||
SpaceTreeItem::~SpaceTreeItem()
|
||||
{
|
||||
qDeleteAll(m_children);
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceTreeItem::child(int number)
|
||||
{
|
||||
if (number < 0 || number >= m_children.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_children[number];
|
||||
}
|
||||
|
||||
int SpaceTreeItem::childCount() const
|
||||
{
|
||||
return m_children.count();
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::insertChild(int row, SpaceTreeItem *newChild)
|
||||
{
|
||||
if (row < 0 || row > m_children.size()) {
|
||||
return false;
|
||||
}
|
||||
m_children.insert(row, newChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::removeChild(int row)
|
||||
{
|
||||
if (row < 0 || row >= m_children.size()) {
|
||||
return false;
|
||||
}
|
||||
delete m_children.takeAt(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
int SpaceTreeItem::row() const
|
||||
{
|
||||
if (m_parentItem) {
|
||||
return m_parentItem->m_children.indexOf(const_cast<SpaceTreeItem *>(this));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceTreeItem::parentItem()
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::canonicalAlias() const
|
||||
{
|
||||
return m_canonicalAlias;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::topic() const
|
||||
{
|
||||
return m_topic;
|
||||
}
|
||||
|
||||
int SpaceTreeItem::memberCount() const
|
||||
{
|
||||
return m_memberCount;
|
||||
}
|
||||
|
||||
QUrl SpaceTreeItem::avatarUrl() const
|
||||
{
|
||||
if (m_avatarUrl.isEmpty() || m_avatarUrl.scheme() != QLatin1String("mxc")) {
|
||||
return {};
|
||||
}
|
||||
auto connection = Controller::instance().activeConnection();
|
||||
auto url = connection->makeMediaUrl(m_avatarUrl);
|
||||
if (url.scheme() == QLatin1String("mxc")) {
|
||||
return url;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::allowGuests() const
|
||||
{
|
||||
return m_allowGuests;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::worldReadable() const
|
||||
{
|
||||
return m_worldReadable;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isJoined() const
|
||||
{
|
||||
auto connection = Controller::instance().activeConnection();
|
||||
if (!connection) {
|
||||
return false;
|
||||
}
|
||||
return connection->room(id(), Quotient::JoinState::Join) != nullptr;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isSpace() const
|
||||
{
|
||||
return m_isSpace;
|
||||
}
|
||||
136
src/models/spacetreeitem.h
Normal file
136
src/models/spacetreeitem.h
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
|
||||
#include <Quotient/csapi/space_hierarchy.h>
|
||||
|
||||
/**
|
||||
* @class SpaceTreeItem
|
||||
*
|
||||
* This class defines an item in the space tree hierarchy model.
|
||||
*
|
||||
* @note This is separate from Quotient::Room and NeoChatRoom because we don't have
|
||||
* full room information for any room/space the user hasn't joined and we
|
||||
* don't want to create one for ever possible child in a space as that would
|
||||
* be expensive.
|
||||
*
|
||||
* @sa Quotient::Room, NeoChatRoom
|
||||
*/
|
||||
class SpaceTreeItem
|
||||
{
|
||||
public:
|
||||
explicit SpaceTreeItem(SpaceTreeItem *parent = nullptr,
|
||||
const QString &id = {},
|
||||
const QString &name = {},
|
||||
const QString &canonicalAlias = {},
|
||||
const QString &topic = {},
|
||||
int memberCount = {},
|
||||
const QUrl &avatarUrl = {},
|
||||
bool allowGuests = {},
|
||||
bool worldReadable = {},
|
||||
bool isSpace = {});
|
||||
~SpaceTreeItem();
|
||||
|
||||
/**
|
||||
* @brief Return the child at the given row number.
|
||||
*
|
||||
* Nullptr is returned if there is no child at the given row number.
|
||||
*/
|
||||
SpaceTreeItem *child(int number);
|
||||
|
||||
/**
|
||||
* @brief The number of children this item has.
|
||||
*/
|
||||
int childCount() const;
|
||||
|
||||
/**
|
||||
* @brief Insert the given child at the given row number.
|
||||
*/
|
||||
bool insertChild(int row, SpaceTreeItem *newChild);
|
||||
|
||||
/**
|
||||
* @brief Remove the child at the given row number.
|
||||
*
|
||||
* @return True if a child was removed, false if the given row isn't valid.
|
||||
*/
|
||||
bool removeChild(int row);
|
||||
|
||||
/**
|
||||
* @brief Return this item's parent.
|
||||
*/
|
||||
SpaceTreeItem *parentItem();
|
||||
|
||||
/**
|
||||
* @brief Return the row number for this child relative to the parent.
|
||||
*
|
||||
* @return The row value if the child has a parent, 0 otherwise.
|
||||
*/
|
||||
int row() const;
|
||||
|
||||
/**
|
||||
* @brief The ID of the room.
|
||||
*/
|
||||
QString id() const;
|
||||
|
||||
/**
|
||||
* @brief The name of the room, if any.
|
||||
*/
|
||||
QString name() const;
|
||||
|
||||
/**
|
||||
* @brief The canonical alias of the room, if any.
|
||||
*/
|
||||
QString canonicalAlias() const;
|
||||
|
||||
/**
|
||||
* @brief The topic of the room, if any.
|
||||
*/
|
||||
QString topic() const;
|
||||
|
||||
/**
|
||||
* @brief The number of members joined to the room.
|
||||
*/
|
||||
int memberCount() const;
|
||||
|
||||
/**
|
||||
* @brief The URL for the room's avatar, if one is set.
|
||||
*
|
||||
* @return A CS API QUrl.
|
||||
*/
|
||||
QUrl avatarUrl() const;
|
||||
|
||||
/**
|
||||
* @brief Whether guest users may join the room and participate in it.
|
||||
*
|
||||
* If they can, they will be subject to ordinary power level rules like any other users.
|
||||
*/
|
||||
bool allowGuests() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the room may be viewed by guest users without joining.
|
||||
*/
|
||||
bool worldReadable() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the local user is a member of the rooom.
|
||||
*/
|
||||
bool isJoined() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the room is a space.
|
||||
*/
|
||||
bool isSpace() const;
|
||||
|
||||
private:
|
||||
QList<SpaceTreeItem *> m_children;
|
||||
SpaceTreeItem *m_parentItem;
|
||||
|
||||
QString m_id;
|
||||
QString m_name;
|
||||
QString m_canonicalAlias;
|
||||
QString m_topic;
|
||||
int m_memberCount;
|
||||
QUrl m_avatarUrl;
|
||||
bool m_allowGuests;
|
||||
bool m_worldReadable;
|
||||
bool m_isSpace;
|
||||
};
|
||||
@@ -156,9 +156,30 @@ void NeoChatConnection::deactivateAccount(const QString &password)
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::createRoom(const QString &name, const QString &topic)
|
||||
void NeoChatConnection::createRoom(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
|
||||
{
|
||||
const auto job = Connection::createRoom(Connection::PublishRoom, {}, name, topic, {});
|
||||
QVector<CreateRoomJob::StateEvent> initialStateEvents;
|
||||
if (!parent.isEmpty()) {
|
||||
initialStateEvents.append(CreateRoomJob::StateEvent{
|
||||
"m.space.parent"_ls,
|
||||
QJsonObject{
|
||||
{"canonical"_ls, true},
|
||||
{"via"_ls, QJsonArray{domain()}},
|
||||
},
|
||||
parent,
|
||||
});
|
||||
}
|
||||
|
||||
const auto job = Connection::createRoom(Connection::PublishRoom, QString(), name, topic, QStringList(), {}, {}, {}, initialStateEvents);
|
||||
if (!parent.isEmpty()) {
|
||||
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
|
||||
if (setChildParent) {
|
||||
if (auto parentRoom = room(parent)) {
|
||||
parentRoom->setState(QLatin1String("m.space.child"), job->roomId(), QJsonObject{{QLatin1String("via"), QJsonArray{domain()}}});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
|
||||
});
|
||||
@@ -167,9 +188,30 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic)
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::createSpace(const QString &name, const QString &topic)
|
||||
void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
|
||||
{
|
||||
const auto job = Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, {}, {}, QJsonObject{{"type"_ls, "m.space"_ls}});
|
||||
QVector<CreateRoomJob::StateEvent> initialStateEvents;
|
||||
if (!parent.isEmpty()) {
|
||||
initialStateEvents.append(CreateRoomJob::StateEvent{
|
||||
"m.space.parent"_ls,
|
||||
QJsonObject{
|
||||
{"canonical"_ls, true},
|
||||
{"via"_ls, QJsonArray{domain()}},
|
||||
},
|
||||
parent,
|
||||
});
|
||||
}
|
||||
|
||||
const auto job = Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_ls, "m.space"_ls}});
|
||||
if (!parent.isEmpty()) {
|
||||
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
|
||||
if (setChildParent) {
|
||||
if (auto parentRoom = room(parent)) {
|
||||
parentRoom->setState(QLatin1String("m.space.child"), job->roomId(), QJsonObject{{QLatin1String("via"), QJsonArray{domain()}}});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
|
||||
});
|
||||
|
||||
@@ -54,12 +54,12 @@ public:
|
||||
/**
|
||||
* @brief Create new room for a group chat.
|
||||
*/
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
|
||||
|
||||
/**
|
||||
* @brief Create new space.
|
||||
*/
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
|
||||
|
||||
Q_SIGNALS:
|
||||
void labelChanged();
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "roomlastmessageprovider.h"
|
||||
#include "texthandler.h"
|
||||
#include "urlhelper.h"
|
||||
#include "utils.h"
|
||||
@@ -72,12 +73,10 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
|
||||
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
|
||||
|
||||
// Load cached event if available.
|
||||
KConfig dataResource("data"_ls, KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
|
||||
KConfigGroup eventCacheGroup(&dataResource, "EventCache"_ls);
|
||||
const auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
||||
|
||||
if (eventCacheGroup.hasKey(id())) {
|
||||
auto eventJson = QJsonDocument::fromJson(eventCacheGroup.readEntry(id(), QByteArray())).object();
|
||||
if (roomLastMessageProvider.hasKey(id())) {
|
||||
auto eventJson = QJsonDocument::fromJson(roomLastMessageProvider.read(id())).object();
|
||||
if (!eventJson.isEmpty()) {
|
||||
auto event = loadEvent<RoomEvent>(eventJson);
|
||||
|
||||
@@ -309,11 +308,10 @@ void NeoChatRoom::cacheLastEvent()
|
||||
{
|
||||
auto event = lastEvent();
|
||||
if (event != nullptr) {
|
||||
KConfig dataResource("data"_ls, KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
|
||||
KConfigGroup eventCacheGroup(&dataResource, "EventCache"_ls);
|
||||
auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
||||
|
||||
auto eventJson = QJsonDocument(event->fullJson()).toJson();
|
||||
eventCacheGroup.writeEntry(id(), eventJson);
|
||||
roomLastMessageProvider.write(id(), eventJson);
|
||||
|
||||
auto uniqueEvent = loadEvent<RoomEvent>(event->fullJson());
|
||||
|
||||
@@ -1111,6 +1109,44 @@ bool NeoChatRoom::isSpace()
|
||||
return creationEvent->roomType() == RoomType::Space;
|
||||
}
|
||||
|
||||
void NeoChatRoom::addChild(const QString &childId, bool setChildParent)
|
||||
{
|
||||
if (!isSpace()) {
|
||||
return;
|
||||
}
|
||||
if (!canSendEvent("m.space.child"_ls)) {
|
||||
return;
|
||||
}
|
||||
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}});
|
||||
|
||||
if (setChildParent) {
|
||||
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
|
||||
if (child->canSendState("m.space.parent"_ls)) {
|
||||
child->setState("m.space.parent"_ls, id(), QJsonObject{{"canonical"_ls, true}, {"via"_ls, QJsonArray{connection()->domain()}}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent)
|
||||
{
|
||||
if (!isSpace()) {
|
||||
return;
|
||||
}
|
||||
if (!canSendEvent("m.space.child"_ls)) {
|
||||
return;
|
||||
}
|
||||
setState("m.space.child"_ls, childId, {});
|
||||
|
||||
if (unsetChildParent) {
|
||||
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
|
||||
if (child->canSendState("m.space.parent"_ls) && child->currentState().contains("m.space.parent"_ls, id())) {
|
||||
child->setState("m.space.parent"_ls, id(), {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PushNotificationState::State NeoChatRoom::pushNotificationState() const
|
||||
{
|
||||
return m_currentPushNotificationState;
|
||||
@@ -1134,9 +1170,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
|
||||
m_pushNotificationStateUpdating = true;
|
||||
|
||||
/**
|
||||
* First remove any exisiting room rules of the wrong type.
|
||||
* First remove any existing room rules of the wrong type.
|
||||
* Note to prevent race conditions any rule that is going ot be overridden later is not removed.
|
||||
* If the default push notification state is chosen any exisiting rule needs to be removed.
|
||||
* If the default push notification state is chosen any existing rule needs to be removed.
|
||||
*/
|
||||
QJsonObject accountData = connection()->accountDataJson("m.push_rules"_ls);
|
||||
|
||||
|
||||
@@ -589,6 +589,10 @@ public:
|
||||
|
||||
[[nodiscard]] bool isSpace();
|
||||
|
||||
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false);
|
||||
|
||||
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
|
||||
|
||||
bool isInvite() const;
|
||||
|
||||
Q_INVOKABLE void clearInvitationNotification();
|
||||
|
||||
@@ -59,8 +59,9 @@ class NotificationsManager : public QObject
|
||||
|
||||
public:
|
||||
static NotificationsManager &instance();
|
||||
static NotificationsManager *create(QQmlEngine *, QJSEngine *)
|
||||
static NotificationsManager *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ QQC2.Control {
|
||||
*/
|
||||
property var author
|
||||
|
||||
/**
|
||||
* @brief Whether the author should be shown.
|
||||
*/
|
||||
required property bool showAuthor
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
*/
|
||||
@@ -133,6 +138,7 @@ QQC2.Control {
|
||||
contentItem: ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
visible: root.showAuthor
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.author.displayName
|
||||
|
||||
@@ -2,44 +2,225 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
title: i18nc("@title", "Create a Room")
|
||||
property string parentId: ""
|
||||
|
||||
property bool isSpace: false
|
||||
|
||||
property bool showChildType: false
|
||||
|
||||
property bool showCreateChoice: false
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal addChild(string childId, bool setChildParent)
|
||||
signal newChild(string childName)
|
||||
|
||||
title: isSpace ? i18nc("@title", "Create a Space") : i18nc("@title", "Create a Room")
|
||||
|
||||
Component.onCompleted: roomNameField.forceActiveFocus()
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Room Information")
|
||||
title: root.isSpace ? i18n("New Space Information") : i18n("New Room Information")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: roomTypeCombo
|
||||
property bool isInitialising: true
|
||||
|
||||
visible: root.showChildType
|
||||
|
||||
text: i18n("Select type")
|
||||
model: ListModel {
|
||||
id: roomTypeModel
|
||||
}
|
||||
textRole: "text"
|
||||
valueRole: "isSpace"
|
||||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(root.isSpace)
|
||||
roomTypeModel.append({"text": i18n("Room"), "isSpace": false});
|
||||
roomTypeModel.append({"text": i18n("Space"), "isSpace": true});
|
||||
roomTypeCombo.currentIndex = 0
|
||||
roomTypeCombo.isInitialising = false
|
||||
}
|
||||
onCurrentValueChanged: {
|
||||
if (!isInitialising) {
|
||||
root.isSpace = currentValue
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: roomNameField
|
||||
label: i18n("Room name:")
|
||||
label: i18n("Name:")
|
||||
onAccepted: if (roomNameField.text.length > 0) roomTopicField.forceActiveFocus();
|
||||
}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: roomTopicField
|
||||
label: i18n("Room topic:")
|
||||
label: i18n("Topic:")
|
||||
onAccepted: ok.clicked()
|
||||
}
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: newOfficialCheck
|
||||
visible: root.parentId.length > 0
|
||||
text: i18n("Make this parent official")
|
||||
checked: true
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: ok
|
||||
text: i18nc("@action:button", "Ok")
|
||||
enabled: roomNameField.text.length > 0
|
||||
onClicked: {
|
||||
root.connection.createRoom(roomNameField.text, roomTopicField.text);
|
||||
if (root.isSpace) {
|
||||
root.connection.createSpace(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
|
||||
} else {
|
||||
root.connection.createRoom(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
|
||||
}
|
||||
root.newChild(roomNameField.text)
|
||||
root.closeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
visible: root.showChildType
|
||||
title: i18n("Select Existing Room")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: root.showChildType
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: !chosenRoomDelegate.visible
|
||||
text: i18nc("@action:button", "Pick room")
|
||||
onClicked: {
|
||||
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
chosenRoomDelegate.roomId = roomId;
|
||||
chosenRoomDelegate.displayName = displayName;
|
||||
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||
chosenRoomDelegate.alias = alias;
|
||||
chosenRoomDelegate.topic = topic;
|
||||
chosenRoomDelegate.memberCount = memberCount;
|
||||
chosenRoomDelegate.isJoined = isJoined;
|
||||
chosenRoomDelegate.visible = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: chosenRoomDelegate
|
||||
property string roomId
|
||||
property string displayName
|
||||
property url avatarUrl
|
||||
property string alias
|
||||
property string topic
|
||||
property int memberCount
|
||||
property bool isJoined
|
||||
|
||||
visible: false
|
||||
|
||||
contentItem: RowLayout {
|
||||
Components.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
source: chosenRoomDelegate.avatarUrl
|
||||
name: chosenRoomDelegate.displayName
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 4
|
||||
text: chosenRoomDelegate.displayName
|
||||
font.bold: true
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: chosenRoomDelegate.isJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text
|
||||
text: chosenRoomDelegate.topic ? chosenRoomDelegate.topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
QQC2.Label {
|
||||
text: chosenRoomDelegate.memberCount + " " + (chosenRoomDelegate.alias ?? chosenRoomDelegate.roomId)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
chosenRoomDelegate.roomId = roomId;
|
||||
chosenRoomDelegate.displayName = displayName;
|
||||
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||
chosenRoomDelegate.alias = alias;
|
||||
chosenRoomDelegate.topic = topic;
|
||||
chosenRoomDelegate.memberCount = memberCount;
|
||||
chosenRoomDelegate.isJoined = isJoined;
|
||||
chosenRoomDelegate.visible = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
id: existingOfficialCheck
|
||||
visible: root.parentId.length > 0
|
||||
text: i18n("Make this parent official")
|
||||
description: enabled ? i18n("You have the required privilege level in the child to set this state") : i18n("You do not have a high enough privilege level in the child to set this state")
|
||||
checked: enabled
|
||||
|
||||
enabled: {
|
||||
if (chosenRoomDelegate.visible) {
|
||||
let room = root.connection.room(chosenRoomDelegate.roomId);
|
||||
if (room) {
|
||||
if (room.canSendState("m.space.parent")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Ok")
|
||||
enabled: chosenRoomDelegate.visible
|
||||
onClicked: {
|
||||
root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: i18n("Create a Space")
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: nameDelegate
|
||||
label: i18n("Space name")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: topicDelegate
|
||||
label: i18n("Space topic (optional)")
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Create space")
|
||||
onClicked: {
|
||||
root.connection.createSpace(nameDelegate.text, topicDelegate.text)
|
||||
root.close()
|
||||
root.destroy()
|
||||
}
|
||||
enabled: nameDelegate.text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQml
|
||||
|
||||
import com.github.quotient_im.libquotient
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ RowLayout {
|
||||
text: i18n("Create a Space")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/CreateSpaceDialog.qml", {connection: root.connection}, {title: i18nc("@title", "Create a Space")})
|
||||
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/CreateRoomDialog.qml", {connection: root.connection, isSpace: true, title: i18nc("@title", "Create a Space")}, {title: i18nc("@title", "Create a Space")})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -26,8 +30,8 @@ ColumnLayout {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.large
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.large
|
||||
|
||||
name: room ? room.displayName : ""
|
||||
source: room && room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
|
||||
name: root.room ? root.room.displayName : ""
|
||||
source: root.room && root.room.avatarMediaId ? ("image://mxc/" + root.room.avatarMediaId) : ""
|
||||
|
||||
Rectangle {
|
||||
visible: room.usesEncryption
|
||||
@@ -58,7 +62,7 @@ ColumnLayout {
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
text: room ? room.displayName : i18n("No name")
|
||||
text: root.room ? root.room.displayName : i18n("No name")
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
@@ -67,8 +71,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: TextEdit.PlainText
|
||||
visible: room && room.canonicalAlias
|
||||
text: room && room.canonicalAlias ? room.canonicalAlias : ""
|
||||
visible: root.room && root.room.canonicalAlias
|
||||
text: root.room && root.room.canonicalAlias ? root.room.canonicalAlias : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +82,7 @@ ColumnLayout {
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
|
||||
text: root.room && root.room.topic ? root.room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
|
||||
readonly property var replaceLinks: /(http[s]?:\/\/[^ \r\n]*)/g
|
||||
textFormat: TextEdit.MarkdownText
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
@@ -6,6 +6,8 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml
|
||||
|
||||
import com.github.quotient_im.libquotient
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.neochat
|
||||
|
||||
@@ -24,5 +24,6 @@ Column {
|
||||
QQC2.Label {
|
||||
text: root.text
|
||||
textFormat: Text.MarkdownText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +394,7 @@ TimelineDelegate {
|
||||
]
|
||||
|
||||
author: root.author
|
||||
showAuthor: root.showAuthor || root.alwaysShowAuthor
|
||||
time: root.time
|
||||
timeString: root.timeString
|
||||
|
||||
|
||||
@@ -28,39 +28,45 @@ Flow {
|
||||
id: reactionRepeater
|
||||
|
||||
delegate: QQC2.AbstractButton {
|
||||
width: Math.max(reactionTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 4, height)
|
||||
id: reactionDelegate
|
||||
|
||||
required property string textContent
|
||||
required property string reaction
|
||||
required property string toolTip
|
||||
required property bool hasLocalUser
|
||||
|
||||
width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height)
|
||||
height: Math.round(Kirigami.Units.gridUnit * 1.5)
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
id: reactionLabel
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: model.text
|
||||
|
||||
TextMetrics {
|
||||
id: reactionTextMetrics
|
||||
text: reactionLabel.text
|
||||
}
|
||||
text: reactionDelegate.textContent
|
||||
background: null
|
||||
wrapMode: TextEdit.NoWrap
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: model.hasLocalUser ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
color: reactionDelegate.hasLocalUser ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
radius: height / 2
|
||||
shadow {
|
||||
size: Kirigami.Units.smallSpacing
|
||||
color: !model.hasLocalUser ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
color: !reactionDelegate.hasLocalUser ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: reactionClicked(model.reaction)
|
||||
onClicked: reactionClicked(reactionDelegate.reaction)
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: model.toolTip
|
||||
QQC2.ToolTip.text: reactionDelegate.toolTip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
src/qml/RemoveChildDialog.qml
Normal file
53
src/qml/RemoveChildDialog.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom parentRoom
|
||||
|
||||
required property string roomId
|
||||
|
||||
required property string displayName
|
||||
|
||||
required property string parentDisplayName
|
||||
|
||||
required property bool canSetParent
|
||||
|
||||
required property bool isDeclaredParent
|
||||
|
||||
title: i18nc("@title", "Remove Child")
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
|
||||
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||
|
||||
onAccepted: parentRoom.removeChild(root.roomId, removeOfficalCheck.checked)
|
||||
|
||||
contentItem: FormCard.FormCardPage {
|
||||
FormCard.FormCard {
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("The child %1 will be removed from the space %2", root.displayName, root.parentDisplayName)
|
||||
textItem.wrapMode: Text.Wrap
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
id: removeOfficalCheck
|
||||
visible: root.isDeclaredParent
|
||||
enabled: root.canSetParent
|
||||
text: i18n("The current space is the official parent of this room, should this be cleared?")
|
||||
checked: root.canSetParent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,18 +43,13 @@ TextEdit {
|
||||
*/
|
||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||
|
||||
property bool isDelegate: false
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||
|
||||
persistentSelection: true
|
||||
|
||||
// Work around QTBUG 93281
|
||||
Component.onCompleted: {
|
||||
updateSelection()
|
||||
if (text.includes("<img")) {
|
||||
Controller.forceRefreshTextDocument(root.textDocument, root)
|
||||
}
|
||||
Component.onCompleted: if (text.includes("<img")) {
|
||||
Controller.forceRefreshTextDocument(root.textDocument, root)
|
||||
}
|
||||
|
||||
text: "<style>
|
||||
@@ -121,26 +116,4 @@ a{
|
||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
||||
onTapped: spoilerRevealed = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: selectionArea
|
||||
enabled: root.isDelegate
|
||||
function onSelectionChanged() {
|
||||
updateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelection() {
|
||||
if (index < selectionArea.lowerIndex || index > selectionArea.upperIndex) {
|
||||
root.select(0, 0);
|
||||
} else if (index > selectionArea.lowerIndex && index < selectionArea.upperIndex) {
|
||||
root.selectAll();
|
||||
} else if (index === selectionArea.selectionStartIndex && index === selectionArea.selectionEndIndex) {
|
||||
root.select(selectionArea.upperPos, selectionArea.lowerPos);
|
||||
} else if (index === selectionArea.upperIndex) {
|
||||
root.select(selectionArea.upperPos, root.length);
|
||||
} else if (index === selectionArea.lowerIndex) {
|
||||
root.select(0, selectionArea.lowerPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ Kirigami.OverlayDrawer {
|
||||
Kirigami.NavigationTabBar {
|
||||
id: navigationBar
|
||||
Layout.fillWidth: true
|
||||
visible: !root.room.isSpace
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ Kirigami.Page {
|
||||
|
||||
footer: Kirigami.NavigationTabBar {
|
||||
id: navigationBar
|
||||
visible: !root.room.isSpace
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ QQC2.ScrollView {
|
||||
/**
|
||||
* @brief The title that should be displayed for this component if available.
|
||||
*/
|
||||
readonly property string title: i18nc("@action:title", "Room information")
|
||||
readonly property string title: root.room.isSpace ? i18nc("@action:title", "Space Members") : i18nc("@action:title", "Room information")
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
@@ -57,6 +57,7 @@ QQC2.ScrollView {
|
||||
active: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
visible: !root.room.isSpace
|
||||
sourceComponent: root.room.isDirectChat() ? directChatDrawerHeader : groupChatDrawerHeader
|
||||
onItemChanged: if (item) {
|
||||
userList.positionViewAtBeginning();
|
||||
@@ -64,6 +65,7 @@ QQC2.ScrollView {
|
||||
}
|
||||
|
||||
Kirigami.ListSectionHeader {
|
||||
visible: !root.room.isSpace
|
||||
label: i18n("Options")
|
||||
activeFocusOnTab: false
|
||||
|
||||
@@ -75,7 +77,7 @@ QQC2.ScrollView {
|
||||
|
||||
icon.name: "tools"
|
||||
text: i18n("Open developer tools")
|
||||
visible: Config.developerTools
|
||||
visible: Config.developerTools && !root.room.isSpace
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -86,7 +88,7 @@ QQC2.ScrollView {
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: searchButton
|
||||
|
||||
visible: !root.room.isSpace
|
||||
icon.name: "search"
|
||||
text: i18n("Search in this room")
|
||||
|
||||
@@ -104,7 +106,7 @@ QQC2.ScrollView {
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: favouriteButton
|
||||
|
||||
visible: !root.room.isSpace
|
||||
icon.name: root.room && root.room.isFavourite ? "rating" : "rating-unrated"
|
||||
text: root.room && root.room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
|
||||
|
||||
@@ -115,7 +117,7 @@ QQC2.ScrollView {
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: locationsButton
|
||||
|
||||
visible: !root.room.isSpace
|
||||
icon.name: "map-flat"
|
||||
text: i18n("Show locations for this room")
|
||||
|
||||
@@ -240,7 +242,9 @@ QQC2.ScrollView {
|
||||
|
||||
Component {
|
||||
id: groupChatDrawerHeader
|
||||
GroupChatDrawerHeader {}
|
||||
GroupChatDrawerHeader {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
|
||||
162
src/qml/SpaceHierarchyDelegate.qml
Normal file
162
src/qml/SpaceHierarchyDelegate.qml
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property TreeView treeView
|
||||
required property bool isTreeNode
|
||||
required property bool expanded
|
||||
required property int hasChildren
|
||||
required property int depth
|
||||
required property string roomId
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
required property bool isSpace
|
||||
required property int memberCount
|
||||
required property string topic
|
||||
required property bool isJoined
|
||||
required property bool canAddChildren
|
||||
required property string parentDisplayName
|
||||
required property bool canSetParent
|
||||
required property bool isDeclaredParent
|
||||
required property bool canRemove
|
||||
required property NeoChatRoom parentRoom
|
||||
|
||||
signal createRoom()
|
||||
signal enterRoom()
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
anchors.centerIn: root
|
||||
width: sizeHelper.currentWidth
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Item {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium * (root.depth + (root.isSpace ? 0 : 1))
|
||||
}
|
||||
Kirigami.Icon {
|
||||
visible: root.isSpace
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
source: root.hasChildren ? (root.expanded ? "go-up" : "go-down") : "go-next"
|
||||
}
|
||||
}
|
||||
Components.Avatar {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
implicitWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
source: root.avatarUrl
|
||||
name: root.displayName
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
QQC2.Label {
|
||||
id: label
|
||||
text: root.displayName
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: root.isJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: subtitle
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
|
||||
text: root.memberCount + (root.topic !== "" ? i18nc("number of room members", " members - ") + root.topic : i18nc("number of room members", " members"))
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: Text.PlainText
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: root.isSpace && root.canAddChildren
|
||||
text: i18nc("@button", "Add new child")
|
||||
icon.name: "list-add"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.createRoom()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: root.canRemove
|
||||
text: i18nc("@button", "Remove")
|
||||
icon.name: "list-remove"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
removeChildDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
parentRoom: root.parentRoom,
|
||||
roomId: root.roomId,
|
||||
displayName: root.displayName,
|
||||
parentDisplayName: root.parentDisplayName,
|
||||
canSetParent: root.canSetParent,
|
||||
isDeclaredParent: root.isDeclaredParent
|
||||
}).open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (root.isSpace) {
|
||||
root.treeView.toggleExpanded(row)
|
||||
} else {
|
||||
if (root.isJoined) {
|
||||
root.enterRoom()
|
||||
} else {
|
||||
Controller.joinRoom(root.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateSizeHelper {
|
||||
id: sizeHelper
|
||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||
startPercentWidth: 100
|
||||
endPercentWidth: 85
|
||||
maxWidth: Kirigami.Units.gridUnit * 60
|
||||
|
||||
parentWidth: root.treeView ? root.treeView.width : 0
|
||||
}
|
||||
|
||||
Component {
|
||||
id: removeChildDialog
|
||||
RemoveChildDialog {}
|
||||
}
|
||||
}
|
||||
178
src/qml/SpaceHomePage.qml
Normal file
178
src/qml/SpaceHomePage.qml
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
readonly property NeoChatRoom currentRoom: RoomManager.currentRoom
|
||||
|
||||
padding: 0
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: headerItem
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
implicitHeight: headerColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: headerColumn
|
||||
anchors.centerIn: headerItem
|
||||
width: sizeHelper.currentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
GroupChatDrawerHeader {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
room: root.currentRoom
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
QQC2.Button {
|
||||
visible: root.currentRoom.canSendState("invite")
|
||||
text: i18nc("@button", "Invite user to space")
|
||||
icon.name: "list-add-user"
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/InviteUserPage.qml", {room: root.currentRoom}, {title: i18nc("@title", "Invite a User")})
|
||||
}
|
||||
QQC2.Button {
|
||||
visible: root.currentRoom.canSendState("m.space.child")
|
||||
text: i18nc("@button", "Add new child")
|
||||
icon.name: "list-add"
|
||||
onClicked: _private.createRoom(root.currentRoom.id)
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@button", "Leave the space")
|
||||
icon.name: "go-previous"
|
||||
onClicked: RoomManager.leaveRoom(root.currentRoom)
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@button", "Space settings")
|
||||
icon.name: "settings-configure"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/org/kde/neochat/qml/Categories.qml', {room: root.currentRoom, connection: root.currentRoom.connection}, { title: i18n("Room Settings") })
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
Kirigami.SearchField {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
onTextChanged: spaceChildSortFilterModel.filterText = text
|
||||
}
|
||||
}
|
||||
DelegateSizeHelper {
|
||||
id: sizeHelper
|
||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||
startPercentWidth: 100
|
||||
endPercentWidth: 85
|
||||
maxWidth: Kirigami.Units.gridUnit * 60
|
||||
|
||||
parentWidth: columnLayout.width
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.ScrollView {
|
||||
id: hierarchyScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: !spaceChildrenModel.loading
|
||||
|
||||
TreeView {
|
||||
id: spaceTree
|
||||
columnWidthProvider: function (column) { return spaceTree.width }
|
||||
|
||||
clip: true
|
||||
|
||||
model: SpaceChildSortFilterModel {
|
||||
id: spaceChildSortFilterModel
|
||||
sourceModel: SpaceChildrenModel {
|
||||
id: spaceChildrenModel
|
||||
space: root.currentRoom
|
||||
}
|
||||
}
|
||||
|
||||
delegate: SpaceHierarchyDelegate {
|
||||
onCreateRoom: _private.createRoom(roomId)
|
||||
onEnterRoom: _private.enterRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: spaceChildrenModel.loading
|
||||
|
||||
Loader {
|
||||
active: spaceChildrenModel.loading
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: Kirigami.LoadingPlaceholder {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
function createRoom(parentId) {
|
||||
let dialog = applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/CreateRoomDialog.qml", {
|
||||
title: i18nc("@title", "Create a Child"),
|
||||
connection: root.currentRoom.connection,
|
||||
parentId : parentId,
|
||||
showChildType: true,
|
||||
showCreateChoice: true
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Child")
|
||||
})
|
||||
dialog.addChild.connect((childId, setChildParent) => {
|
||||
// We have to get a room object from the connection as we may not
|
||||
// be adding to the top level parent.
|
||||
let parent = root.currentRoom.connection.room(parentId)
|
||||
if (parent) {
|
||||
parent.addChild(childId, setChildParent)
|
||||
}
|
||||
})
|
||||
dialog.newChild.connect(childName => {spaceChildrenModel.addPendingChild(childName)})
|
||||
}
|
||||
|
||||
function enterRoom(roomId) {
|
||||
let room = root.currentRoom.connection.room(roomId)
|
||||
if (room) {
|
||||
RoomManager.enterRoom(room)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ Loader {
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("'Space' is a matrix space", "View Space")
|
||||
icon.name: "view-list-details"
|
||||
onTriggered: RoomManager.enterRoom(room);
|
||||
onTriggered: RoomManager.enterSpaceHome(room);
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
|
||||
@@ -17,14 +17,6 @@ import org.kde.neochat.config
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
function positionAt(x, y) {
|
||||
let point = label.mapFromItem(root, x, y)
|
||||
return label.positionAt(point.x, point.y)
|
||||
}
|
||||
|
||||
property alias selectedText: label.selectedText
|
||||
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
@@ -62,7 +54,6 @@ MessageDelegate {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: root.openContextMenu()
|
||||
}
|
||||
isDelegate: true
|
||||
}
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -341,66 +341,6 @@ QQC2.ScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
function indexAtRelative(x, y) {
|
||||
return indexAt(x + contentX, y + contentY)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: selectionArea
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
property int selectionStartIndex
|
||||
property int selectionEndIndex
|
||||
property int selectionStartPos
|
||||
property int selectionEndPos
|
||||
|
||||
property int upperIndex: selectionStartIndex > selectionEndIndex ? selectionStartIndex : selectionEndIndex
|
||||
property int upperPos: selectionStartIndex > selectionEndIndex ? selectionStartPos : (selectionStartIndex == selectionEndIndex ? (selectionStartPos > selectionEndPos ? selectionEndPos : selectionStartPos) : selectionEndPos)
|
||||
property int lowerIndex: selectionStartIndex > selectionEndIndex ? selectionEndIndex : selectionStartIndex
|
||||
property int lowerPos: selectionStartIndex > selectionEndIndex ? selectionEndPos : (selectionStartIndex == selectionEndIndex ? (selectionStartPos > selectionEndPos ? selectionStartPos : selectionEndPos) : selectionStartPos)
|
||||
|
||||
signal selectionChanged
|
||||
|
||||
function indexAndPos(x, y) {
|
||||
const index = messageListView.indexAtRelative(x, y);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
const item = messageListView.itemAtIndex(index);
|
||||
const relItemY = item.y - messageListView.contentY;
|
||||
const pos = item.positionAt(x, y - relItemY);
|
||||
return [index, pos]
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
[selectionArea.selectionEndIndex, selectionArea.selectionEndPos] = selectionArea.indexAndPos(mouse.x, mouse.y);
|
||||
[selectionArea.selectionStartIndex, selectionArea.selectionStartPos] = selectionArea.indexAndPos(mouse.x, mouse.y);
|
||||
selectionChanged();
|
||||
}
|
||||
onPositionChanged: {
|
||||
if (!pressed) {
|
||||
return
|
||||
}
|
||||
[selectionEndIndex, selectionEndPos] = selectionArea.indexAndPos(mouse.x, mouse.y);
|
||||
selectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
onTriggered: {
|
||||
var text = ""
|
||||
for (let i = selectionArea.upperIndex; i >= selectionArea.lowerIndex; i--) {
|
||||
text += messageListView.itemAtIndex(i).selectedText
|
||||
if (i > selectionArea.lowerIndex) {
|
||||
text += " "
|
||||
}
|
||||
}
|
||||
Clipboard.saveText(text)
|
||||
}
|
||||
shortcut: "Ctrl+C"
|
||||
}
|
||||
|
||||
function showMaximizedMedia(index) {
|
||||
var popup = maximizeComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
initialIndex: index
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import QtQuick
|
||||
import QtQml
|
||||
|
||||
import com.github.quotient_im.libquotient
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Message {
|
||||
@@ -40,7 +42,7 @@ Message {
|
||||
case KeyVerificationSession.KEY_MISMATCH:
|
||||
return i18n("The session verification was canceled because the keys are incorrect.");
|
||||
case KeyVerificationSession.REMOTE_KEY_MISMATCH:
|
||||
return i18n("The remote party canceled the session verification because the keys are incorrect.");
|
||||
return i18n("The remote party canceled the session verification because the keys are incorrect.\n\n**Please log out and log back in, your session is broken/corrupt.**");
|
||||
case KeyVerificationSession.USER_MISMATCH:
|
||||
return i18n("The session verification was canceled because it verifies an unexpected user.");
|
||||
case KeyVerificationSession.REMOTE_USER_MISMATCH:
|
||||
|
||||
@@ -21,6 +21,7 @@ Kirigami.ApplicationWindow {
|
||||
property bool roomListLoaded: false
|
||||
|
||||
property RoomPage roomPage
|
||||
property SpaceHomePage spaceHomePage
|
||||
|
||||
property NeoChatConnection connection: Controller.activeConnection
|
||||
|
||||
@@ -96,15 +97,36 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function onPushSpaceHome(room) {
|
||||
root.spaceHomePage = pageStack.push("qrc:/org/kde/neochat/qml/SpaceHomePage.qml");
|
||||
root.spaceHomePage.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onReplaceRoom(room, event) {
|
||||
const roomItem = pageStack.get(pageStack.depth - 1);
|
||||
pageStack.currentIndex = pageStack.depth - 1;
|
||||
if (root.roomPage) {
|
||||
pageStack.currentIndex = pageStack.depth - 1;
|
||||
} else {
|
||||
pageStack.pop();
|
||||
root.roomPage = pageStack.push("qrc:/org/kde/neochat/qml/RoomPage.qml", {connection: root.connection});
|
||||
root.spaceHomePage = null;
|
||||
}
|
||||
root.roomPage.forceActiveFocus();
|
||||
if (event.length > 0) {
|
||||
roomItem.goToEvent(event);
|
||||
root.roomPage.goToEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
function onReplaceSpaceHome(room) {
|
||||
if (root.spaceHomePage) {
|
||||
pageStack.currentIndex = pageStack.depth - 1;
|
||||
} else {
|
||||
pageStack.pop();
|
||||
root.spaceHomePage = pageStack.push("qrc:/org/kde/neochat/qml/SpaceHomePage.qml");
|
||||
root.roomPage = null;
|
||||
}
|
||||
root.spaceHomePage.forceActiveFocus();
|
||||
}
|
||||
|
||||
function goToEvent(event) {
|
||||
if (event.length > 0) {
|
||||
roomItem.goToEvent(event);
|
||||
@@ -335,13 +357,6 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: createSpaceDialog
|
||||
CreateSpaceDialog {
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomWindow
|
||||
RoomWindow {}
|
||||
|
||||
@@ -91,8 +91,9 @@ public:
|
||||
static Registration _instance;
|
||||
return _instance;
|
||||
}
|
||||
static Registration *create(QQmlEngine *, QJSEngine *)
|
||||
static Registration *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
38
src/roomlastmessageprovider.cpp
Normal file
38
src/roomlastmessageprovider.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "roomlastmessageprovider.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
RoomLastMessageProvider::RoomLastMessageProvider()
|
||||
: m_config(KSharedConfig::openConfig(u"data"_s, KConfig::SimpleConfig, QStandardPaths::AppDataLocation))
|
||||
, m_configGroup(KConfigGroup(m_config, u"EventCache"_s))
|
||||
{
|
||||
}
|
||||
|
||||
RoomLastMessageProvider::~RoomLastMessageProvider()
|
||||
{
|
||||
m_config->sync();
|
||||
}
|
||||
|
||||
RoomLastMessageProvider &RoomLastMessageProvider::self()
|
||||
{
|
||||
static RoomLastMessageProvider instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool RoomLastMessageProvider::hasKey(const QString &roomId) const
|
||||
{
|
||||
return m_configGroup.hasKey(roomId);
|
||||
}
|
||||
|
||||
QByteArray RoomLastMessageProvider::read(const QString &roomId) const
|
||||
{
|
||||
return m_configGroup.readEntry(roomId, QByteArray{});
|
||||
}
|
||||
|
||||
void RoomLastMessageProvider::write(const QString &roomId, const QByteArray &json)
|
||||
{
|
||||
m_configGroup.writeEntry(roomId, json);
|
||||
}
|
||||
41
src/roomlastmessageprovider.h
Normal file
41
src/roomlastmessageprovider.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KSharedConfig>
|
||||
|
||||
/**
|
||||
* Store and retrieve the last message of a room.
|
||||
*/
|
||||
class RoomLastMessageProvider
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Get the global instance of RoomLastMessageProvider.
|
||||
*/
|
||||
static RoomLastMessageProvider &self();
|
||||
~RoomLastMessageProvider();
|
||||
|
||||
/**
|
||||
* Check if we have the last message content for the specified roomId.
|
||||
*/
|
||||
bool hasKey(const QString &roomId) const;
|
||||
|
||||
/**
|
||||
* Read the last message content of the specified roomId.
|
||||
*/
|
||||
QByteArray read(const QString &roomId) const;
|
||||
|
||||
/**
|
||||
* Write the last message content for the specified roomId.
|
||||
*/
|
||||
void write(const QString &roomId, const QByteArray &json);
|
||||
|
||||
private:
|
||||
RoomLastMessageProvider();
|
||||
|
||||
KSharedConfig::Ptr m_config;
|
||||
KConfigGroup m_configGroup;
|
||||
};
|
||||
@@ -179,7 +179,11 @@ void RoomManager::openRoomForActiveConnection()
|
||||
const auto room = qobject_cast<NeoChatRoom *>(Controller::instance().activeConnection()->room(roomId));
|
||||
|
||||
if (room) {
|
||||
enterRoom(room);
|
||||
if (room->isSpace()) {
|
||||
enterSpaceHome(room);
|
||||
} else {
|
||||
enterRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,6 +226,34 @@ void RoomManager::openWindow(NeoChatRoom *room)
|
||||
Q_EMIT openRoomInNewWindow(room);
|
||||
}
|
||||
|
||||
void RoomManager::enterSpaceHome(NeoChatRoom *spaceRoom)
|
||||
{
|
||||
if (!spaceRoom->isSpace()) {
|
||||
return;
|
||||
}
|
||||
// If replacing a normal room message timeline make sure any edit is cancelled.
|
||||
if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) {
|
||||
m_currentRoom->setChatBoxEditId({});
|
||||
}
|
||||
// Save the chatbar text for the current room if any before switching
|
||||
if (m_currentRoom && m_chatDocumentHandler) {
|
||||
if (m_chatDocumentHandler->document()) {
|
||||
m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
|
||||
}
|
||||
}
|
||||
m_lastCurrentRoom = std::exchange(m_currentRoom, spaceRoom);
|
||||
Q_EMIT currentRoomChanged();
|
||||
|
||||
if (!m_lastCurrentRoom) {
|
||||
Q_EMIT pushSpaceHome(spaceRoom);
|
||||
} else {
|
||||
Q_EMIT replaceSpaceHome(m_currentRoom);
|
||||
}
|
||||
|
||||
// Save last open room
|
||||
m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), spaceRoom->id());
|
||||
}
|
||||
|
||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||
{
|
||||
if (action == "mention"_ls || action.isEmpty()) {
|
||||
|
||||
@@ -90,8 +90,9 @@ public:
|
||||
explicit RoomManager(QObject *parent = nullptr);
|
||||
virtual ~RoomManager();
|
||||
static RoomManager &instance();
|
||||
static RoomManager *create(QQmlEngine *, QJSEngine *)
|
||||
static RoomManager *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
@@ -127,6 +128,13 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void leaveRoom(NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Enter the home page of the given space.
|
||||
*
|
||||
* This method will tell NeoChat to open the home page for the given space.
|
||||
*/
|
||||
Q_INVOKABLE void enterSpaceHome(NeoChatRoom *spaceRoom);
|
||||
|
||||
// Overrided methods from UriResolverBase
|
||||
/**
|
||||
* @brief Resolve a user URI.
|
||||
@@ -263,6 +271,26 @@ Q_SIGNALS:
|
||||
*/
|
||||
void replaceRoom(NeoChatRoom *room, const QString &event);
|
||||
|
||||
/**
|
||||
* @brief Push a new space home page.
|
||||
*
|
||||
* Signal triggered when the main window pageStack should push a new page with
|
||||
* the space home for the given space room.
|
||||
*
|
||||
* @param spaceRoom the space room to be shown on the new page.
|
||||
*/
|
||||
void pushSpaceHome(NeoChatRoom *spaceRoom);
|
||||
|
||||
/**
|
||||
* @brief Replace the existing space home.
|
||||
*
|
||||
* Signal triggered when the currently displayed room page should be changed
|
||||
* to the space home for the given space room.
|
||||
*
|
||||
* @param spaceRoom the space room to be shown on the new page.
|
||||
*/
|
||||
void replaceSpaceHome(NeoChatRoom *spaceRoom);
|
||||
|
||||
/**
|
||||
* @brief Go to the specified event in the current room.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
@@ -33,8 +34,9 @@ public:
|
||||
static SpaceHierarchyCache _instance;
|
||||
return _instance;
|
||||
}
|
||||
static SpaceHierarchyCache *create(QQmlEngine *, QJSEngine *)
|
||||
static SpaceHierarchyCache *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user