Compare commits
142 Commits
work/redst
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df8a09ce90 | ||
|
|
150b9c4038 | ||
|
|
7550b2b000 | ||
|
|
daa66c4abc | ||
|
|
e0b229e040 | ||
|
|
c07db82552 | ||
|
|
4c1301da51 | ||
|
|
adf0544eb0 | ||
|
|
44ec1e3529 | ||
|
|
7cfac2151e | ||
|
|
3f577adf34 | ||
|
|
0375ef99a5 | ||
|
|
23c0bdb647 | ||
|
|
611374d4dd | ||
|
|
c3ea0db82d | ||
|
|
eb58d96d77 | ||
|
|
9cad195704 | ||
|
|
c08693c508 | ||
|
|
7f91c57945 | ||
|
|
8b9e470907 | ||
|
|
63ddfb6cb8 | ||
|
|
ce84562829 | ||
|
|
7eda07d788 | ||
|
|
3105fb8aa8 | ||
|
|
e3fdcb8779 | ||
|
|
594018b188 | ||
|
|
774eb6a505 | ||
|
|
f9a6533ca9 | ||
|
|
c804b9ce14 | ||
|
|
d1acb97fe2 | ||
|
|
0a99e90591 | ||
|
|
c6313d2951 | ||
|
|
d2d48110cb | ||
|
|
a235f39c84 | ||
|
|
8e1303edbe | ||
|
|
f66acb5f78 | ||
|
|
0fe4ae82ea | ||
|
|
8e516422b7 | ||
|
|
1dc566f9a9 | ||
|
|
cdfda55ac1 | ||
|
|
a27c8971b9 | ||
|
|
b7688d4373 | ||
|
|
73e6a536db | ||
|
|
a60d6c4a60 | ||
|
|
b49ce25246 | ||
|
|
6c52cccf20 | ||
|
|
1eba52bb7a | ||
|
|
3b00e14a9d | ||
|
|
f6abbda1e3 | ||
|
|
2d0fa43f4f | ||
|
|
502e300d5f | ||
|
|
b7ea6f265e | ||
|
|
5f0f9135fe | ||
|
|
b0dce5fd0a | ||
|
|
2c5bef6e96 | ||
|
|
aaaaf91248 | ||
|
|
7e3db19ead | ||
|
|
f0ea5d1e90 | ||
|
|
d2a6d8e447 | ||
|
|
57c20d4c5a | ||
|
|
d7d348322f | ||
|
|
ae9b2abdc7 | ||
|
|
3a964bae20 | ||
|
|
4078d3f2dc | ||
|
|
53989ff4fe | ||
|
|
2d33cbf6b1 | ||
|
|
b42a82a455 | ||
|
|
99aed0993e | ||
|
|
7ede740aa8 | ||
|
|
7137b60da9 | ||
|
|
30462ddebb | ||
|
|
428a196f8d | ||
|
|
2d2291fd78 | ||
|
|
646f5476c3 | ||
|
|
03382496b9 | ||
|
|
52302f0f5c | ||
|
|
b1595a4556 | ||
|
|
436b3a1008 | ||
|
|
44ff2daad2 | ||
|
|
9f64457521 | ||
|
|
d2aa8d672d | ||
|
|
942221b59f | ||
|
|
7e6b79d5d4 | ||
|
|
85b731e9fb | ||
|
|
38b3e65618 | ||
|
|
89e5a605c4 | ||
|
|
a438173403 | ||
|
|
98a224ebdf | ||
|
|
7b8576e203 | ||
|
|
f1b5ad7392 | ||
|
|
78beb9ffff | ||
|
|
431dbf6457 | ||
|
|
e1b267622d | ||
|
|
47a8221f53 | ||
|
|
c554c40b3b | ||
|
|
0341da5868 | ||
|
|
c5457a893f | ||
|
|
9f6853f771 | ||
|
|
526cb07840 | ||
|
|
dcc394677e | ||
|
|
a9a73ab24d | ||
|
|
2289dbb3fe | ||
|
|
7758175334 | ||
|
|
c79753716c | ||
|
|
3aa4a915b1 | ||
|
|
60cf12524f | ||
|
|
f02366ee48 | ||
|
|
13d7f9b322 | ||
|
|
7afce01a23 | ||
|
|
0d1f5c950d | ||
|
|
690bc0d385 | ||
|
|
c32235ffe3 | ||
|
|
dff6ab66f1 | ||
|
|
80047acf87 | ||
|
|
234d823366 | ||
|
|
a3cd0c0e8d | ||
|
|
cad90d0c4c | ||
|
|
6e28ada1a4 | ||
|
|
007ebbc003 | ||
|
|
289c6c4f20 | ||
|
|
39573c1650 | ||
|
|
b7a329c199 | ||
|
|
6b318ec754 | ||
|
|
79de8a792c | ||
|
|
b45ded678e | ||
|
|
f4cb660422 | ||
|
|
d64e6fc206 | ||
|
|
d0abfe60f9 | ||
|
|
d10fe4a684 | ||
|
|
9ea76ca5d0 | ||
|
|
22d7d90cf4 | ||
|
|
45163944d0 | ||
|
|
f31e9062e6 | ||
|
|
416d85af3b | ||
|
|
02bed79265 | ||
|
|
aadc441686 | ||
|
|
4db1e1c437 | ||
|
|
11bf741554 | ||
|
|
c128450cf5 | ||
|
|
9cbe9f7280 | ||
|
|
1f723d1fdf | ||
|
|
b88e27d6d5 |
@@ -12,7 +12,7 @@ include:
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/linux-qt6-next.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
- /gitlab-templates/freebsd-qt6.yml
|
||||
# - /gitlab-templates/freebsd-qt6.yml
|
||||
- /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/snap-snapcraft-lxd.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
|
||||
@@ -28,7 +28,6 @@ Dependencies:
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
|
||||
@@ -88,7 +88,7 @@ if(ANDROID)
|
||||
)
|
||||
else()
|
||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem)
|
||||
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
|
||||
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
|
||||
TYPE RUNTIME
|
||||
@@ -106,7 +106,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
|
||||
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(QuotientQt6 0.9.3)
|
||||
find_package(QuotientQt6 0.9.5)
|
||||
set_package_properties(QuotientQt6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
DESCRIPTION "Qt wrapper around Matrix API"
|
||||
@@ -178,7 +178,7 @@ add_definitions(-DQT_NO_FOREACH)
|
||||
add_subdirectory(src)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer QuickTest)
|
||||
add_subdirectory(autotests)
|
||||
# add_subdirectory(appiumtests)
|
||||
if (NOT ANDROID)
|
||||
|
||||
@@ -45,12 +45,6 @@ ecm_add_test(
|
||||
TEST_NAME chatbarcachetest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
chatdocumenthandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME chatdocumenthandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
timelinemessagemodeltest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
@@ -110,3 +104,45 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
|
||||
TEST_NAME modeltest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
blockcachetest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME blockcachetest
|
||||
)
|
||||
|
||||
macro(add_qml_tests)
|
||||
if (WIN32)
|
||||
set(_extra_args -platform offscreen)
|
||||
endif()
|
||||
|
||||
foreach(test ${ARGV})
|
||||
add_test(NAME ${test}
|
||||
COMMAND qmltest
|
||||
${_extra_args}
|
||||
-input ${test}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
add_executable(qmltest qmltest.cpp
|
||||
chatkeyhelpertesthelper.h
|
||||
chatmarkdownhelpertestwrapper.h
|
||||
chattextitemhelpertesthelper.h
|
||||
)
|
||||
qt_add_qml_module(qmltest URI NeoChatTestUtils)
|
||||
|
||||
target_link_libraries(qmltest
|
||||
PRIVATE
|
||||
Qt6::Qml
|
||||
Qt6::QuickTest
|
||||
LibNeoChat
|
||||
LibNeoChatplugin
|
||||
)
|
||||
|
||||
add_qml_tests(
|
||||
chattextitemhelpertest.qml
|
||||
chatmarkdownhelpertest.qml
|
||||
chatkeyhelpertest.qml
|
||||
)
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <QVariantList>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "blockcache.h"
|
||||
#include "chatbarcache.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "models/actionsmodel.h"
|
||||
|
||||
#include "server.h"
|
||||
@@ -88,8 +90,8 @@ void ActionsTest::testActions()
|
||||
QFETCH(std::optional<QString>, resultText);
|
||||
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
|
||||
|
||||
auto cache = new ChatBarCache(this);
|
||||
cache->setText(command);
|
||||
auto cache = new ChatBarCache(room);
|
||||
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)};
|
||||
auto result = ActionsModel::handleAction(room, cache);
|
||||
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
|
||||
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result));
|
||||
|
||||
52
autotests/blockcachetest.cpp
Normal file
52
autotests/blockcachetest.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "blockcache.h"
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
|
||||
using namespace Block;
|
||||
|
||||
class BlockCacheTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void toStringTest_data();
|
||||
void toStringTest();
|
||||
};
|
||||
|
||||
void BlockCacheTest::toStringTest_data()
|
||||
{
|
||||
QTest::addColumn<QString>("inputString");
|
||||
QTest::addColumn<MessageComponentType::Type>("itemType");
|
||||
QTest::addColumn<QString>("outputstring");
|
||||
|
||||
QTest::newRow("plainText") << u"test string"_s << MessageComponentType::Text << u"test string"_s;
|
||||
QTest::newRow("list") << u"- list 1\n- list 2\n- list 3\n"_s << MessageComponentType::Text << u"- list 1\n- list 2\n- list 3"_s;
|
||||
QTest::newRow("code") << u"for (some code) {\n\n do something\n\n}"_s << MessageComponentType::Code
|
||||
<< u"```\nfor (some code) {\n do something\n}\n```"_s;
|
||||
QTest::newRow("quote") << u"\"this is a quote\""_s << MessageComponentType::Quote << u"> this is a quote"_s;
|
||||
QTest::newRow("heading") << u"# heading\n\nnext line"_s << MessageComponentType::Text << u"# heading\n\nnext line"_s;
|
||||
}
|
||||
|
||||
void BlockCacheTest::toStringTest()
|
||||
{
|
||||
QFETCH(QString, inputString);
|
||||
QFETCH(MessageComponentType::Type, itemType);
|
||||
QFETCH(QString, outputstring);
|
||||
|
||||
Cache cache;
|
||||
cache += CacheItem{
|
||||
.type = itemType,
|
||||
.content = QTextDocumentFragment::fromMarkdown(inputString),
|
||||
};
|
||||
|
||||
QCOMPARE(cache.toString(), outputstring);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BlockCacheTest)
|
||||
#include "blockcachetest.moc"
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "blockcache.h"
|
||||
#include "chatbarcache.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
@@ -36,8 +37,6 @@ private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void empty();
|
||||
void noRoom();
|
||||
void badParent();
|
||||
void reply();
|
||||
void replyMissingUser();
|
||||
void edit();
|
||||
@@ -77,7 +76,7 @@ void ChatBarCacheTest::empty()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
|
||||
QCOMPARE(chatBarCache->text(), QString());
|
||||
QCOMPARE(chatBarCache->cache().toString(), QString());
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
@@ -87,47 +86,14 @@ void ChatBarCacheTest::empty()
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::noRoom()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::badParent()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QScopedPointer<QObject> badParent(new QObject());
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::reply()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
@@ -141,11 +107,11 @@ void ChatBarCacheTest::reply()
|
||||
void ChatBarCacheTest::replyMissingUser()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
@@ -172,7 +138,7 @@ void ChatBarCacheTest::edit()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||
QCOMPARE(oldEventId, QString());
|
||||
@@ -180,7 +146,7 @@ void ChatBarCacheTest::edit()
|
||||
});
|
||||
chatBarCache->setEditId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), true);
|
||||
@@ -193,11 +159,11 @@ void ChatBarCacheTest::edit()
|
||||
void ChatBarCacheTest::attachment()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setEditId(eventId);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
class ChatDocumentHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
ChatDocumentHandler emptyHandler;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void nullComplete();
|
||||
};
|
||||
|
||||
void ChatDocumentHandlerTest::initTestCase()
|
||||
{
|
||||
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
|
||||
NeoChatConfig::self()->setSystemTray(false);
|
||||
}
|
||||
|
||||
void ChatDocumentHandlerTest::nullComplete()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
|
||||
emptyHandler.complete(0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ChatDocumentHandlerTest)
|
||||
#include "chatdocumenthandlertest.moc"
|
||||
88
autotests/chatkeyhelpertest.qml
Normal file
88
autotests/chatkeyhelpertest.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatKeyHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
event.accepted = testHelper.keyHelper.handleKey(event.key, event.modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
ChatTextItemHelper {
|
||||
id: textItemHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
ChatKeyHelperTestHelper {
|
||||
id: testHelper
|
||||
|
||||
textItem: textItemHelper
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyUp
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledUp"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyDown
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledDown"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyDelete
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledDelete"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyBackSpace
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledBackspace"
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
textEdit.clear();
|
||||
spyUp.clear();
|
||||
spyDown.clear();
|
||||
spyDelete.clear();
|
||||
spyBackSpace.clear();
|
||||
textEdit.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cleanupTestCase(): void {
|
||||
testHelper.textItem = null;
|
||||
textItemHelper.textItem = null;
|
||||
}
|
||||
|
||||
function test_upDown(): void {
|
||||
textEdit.insert(0, "line 1\nline 2\nline 3")
|
||||
textEdit.cursorPosition = 0;
|
||||
keyClick(Qt.Key_Up);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 1);
|
||||
}
|
||||
}
|
||||
54
autotests/chatkeyhelpertesthelper.h
Normal file
54
autotests/chatkeyhelpertesthelper.h
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include "chatkeyhelper.h"
|
||||
#include "chattextitemhelper.h"
|
||||
|
||||
class ChatKeyHelperTestHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT)
|
||||
|
||||
public:
|
||||
explicit ChatKeyHelperTestHelper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_keyHelper(new ChatKeyHelper(this))
|
||||
{
|
||||
}
|
||||
|
||||
ChatTextItemHelper *textItem() const
|
||||
{
|
||||
return m_keyHelper->textItem();
|
||||
}
|
||||
void setTextItem(ChatTextItemHelper *textItem)
|
||||
{
|
||||
if (textItem == m_keyHelper->textItem()) {
|
||||
return;
|
||||
}
|
||||
m_keyHelper->setTextItem(textItem);
|
||||
Q_EMIT textItemChanged();
|
||||
}
|
||||
|
||||
ChatKeyHelper *keyHelper() const
|
||||
{
|
||||
return m_keyHelper;
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
|
||||
private:
|
||||
QPointer<ChatKeyHelper> m_keyHelper;
|
||||
};
|
||||
173
autotests/chatmarkdownhelpertest.qml
Normal file
173
autotests/chatmarkdownhelpertest.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatMarkdownHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
|
||||
textFormat: TextEdit.RichText
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: textEdit2
|
||||
}
|
||||
|
||||
ChatMarkdownHelperTestWrapper {
|
||||
id: chatMarkdownHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyItem
|
||||
target: chatMarkdownHelper
|
||||
signalName: "textItemChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyUnhandledFormat
|
||||
target: chatMarkdownHelper
|
||||
signalName: "unhandledBlockFormat"
|
||||
}
|
||||
|
||||
function initTestCase(): void {
|
||||
textEdit.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
chatMarkdownHelper.clear();
|
||||
compare(chatMarkdownHelper.checkText(""), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
}
|
||||
|
||||
function test_item(): void {
|
||||
spyItem.clear();
|
||||
compare(chatMarkdownHelper.textItem, textEdit);
|
||||
chatMarkdownHelper.textItem = textEdit2;
|
||||
compare(chatMarkdownHelper.textItem, textEdit2);
|
||||
chatMarkdownHelper.textItem = textEdit;
|
||||
compare(chatMarkdownHelper.textItem, textEdit);
|
||||
}
|
||||
|
||||
function test_textFormat_data() {
|
||||
return [
|
||||
{tag: "bold", input: "**b** ", outText: ["*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold], [RichFormat.Bold], []], unhandled: 0},
|
||||
{tag: "italic", input: "*i* ", outText: ["*", "i", "i*", "i "], outFormats: [[], [RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "heading 1", input: "# h", outText: ["#", "# ", "h"], outFormats: [[], [], [RichFormat.Bold, RichFormat.Heading1]], unhandled: 0},
|
||||
{tag: "heading 2", input: "## h", outText: ["#", "##", "## ", "h"], outFormats: [[], [], [], [RichFormat.Bold, RichFormat.Heading2]], unhandled: 0},
|
||||
{tag: "heading 3", input: "### h", outText: ["#", "##", "###", "### ", "h"], outFormats: [[], [], [], [], [RichFormat.Bold, RichFormat.Heading3]], unhandled: 0},
|
||||
{tag: "heading 4", input: "#### h", outText: ["#", "##", "###", "####", "#### ", "h"], outFormats: [[], [], [], [], [], [RichFormat.Bold, RichFormat.Heading4]], unhandled: 0},
|
||||
{tag: "heading 5", input: "##### h", outText: ["#", "##", "###", "####", "#####", "##### ", "h"], outFormats: [[], [], [], [], [], [], [RichFormat.Bold, RichFormat.Heading5]], unhandled: 0},
|
||||
{tag: "heading 6", input: "###### h", outText: ["#", "##", "###", "####", "#####", "######", "###### ", "h"], outFormats: [[], [], [], [], [], [] ,[], [RichFormat.Bold, RichFormat.Heading6]], unhandled: 0},
|
||||
{tag: "quote", input: "> q", outText: [">", "> ", "q"], outFormats: [[], [], []], unhandled: 1},
|
||||
{tag: "quote - no space", input: ">q", outText: [">", "q"], outFormats: [[], [], []], unhandled: 1},
|
||||
{tag: "unorderedlist 1", input: "* l", outText: ["*", "* ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "unorderedlist 2", input: "- l", outText: ["-", "- ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 1", input: "1. l", outText: ["1", "1.", "1. ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 2", input: "1) l", outText: ["1", "1)", "1) ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "inline code", input: "`c` ", outText: ["`", "c", "c`", "c "], outFormats: [[], [RichFormat.InlineCode], [RichFormat.InlineCode], []], unhandled: 0},
|
||||
{tag: "code", input: "``` ", outText: ["`", "``", "```", " "], outFormats: [[], [], [], []], unhandled: 1},
|
||||
{tag: "strikethrough", input: "~~s~~ ", outText: ["~", "~~", "s", "s~", "s~~", "s "], outFormats: [[], [], [RichFormat.Strikethrough], [RichFormat.Strikethrough], [RichFormat.Strikethrough], []], unhandled: 0},
|
||||
{tag: "underline", input: "_u_ ", outText: ["_", "u", "u_", "u "], outFormats: [[], [RichFormat.Underline], [RichFormat.Underline], []], unhandled: 0},
|
||||
{tag: "multiple closable", input: "***_~~t~~_*** ", outText: ["*", "**", "*", "_", "~", "~~", "t", "t~", "t~~", "t_", "t*", "t**", "t*", "t "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "nonclosable closable", input: "* **b** ", outText: ["*", "* ", "*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.UnorderedList], [RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "not at line start", input: " 1) ", outText: [" ", " 1", " 1)", " 1) "], outFormats: [[], [], [], []], unhandled: 0},
|
||||
]
|
||||
}
|
||||
|
||||
function test_textFormat(data): void {
|
||||
spyUnhandledFormat.clear();
|
||||
compare(spyUnhandledFormat.count, 0);
|
||||
|
||||
for (let i = 0; i < data.input.length; i++) {
|
||||
keyClick(data.input[i]);
|
||||
compare(chatMarkdownHelper.checkText(data.outText[i]), true);
|
||||
compare(chatMarkdownHelper.checkFormats(data.outFormats[i]), true);
|
||||
}
|
||||
|
||||
compare(spyUnhandledFormat.count, data.unhandled);
|
||||
}
|
||||
|
||||
function test_backspace(): void {
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("*"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("**"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("b"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("o");
|
||||
compare(chatMarkdownHelper.checkText("bo"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("l");
|
||||
compare(chatMarkdownHelper.checkText("bol"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("d");
|
||||
compare(chatMarkdownHelper.checkText("bold"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(Qt.Key_Backspace);
|
||||
compare(chatMarkdownHelper.checkText("bol"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(Qt.Key_Backspace);
|
||||
compare(chatMarkdownHelper.checkText("bo"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("bo*"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("bo**"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(" ");
|
||||
compare(chatMarkdownHelper.checkText("bo "), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
|
||||
function test_cursorMove(): void {
|
||||
keyClick("t");
|
||||
keyClick("e");
|
||||
keyClick("s");
|
||||
keyClick("t");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("*");
|
||||
keyClick("*");
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("testb"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
textEdit.cursorPosition = 2;
|
||||
keyClick("*");
|
||||
keyClick("*");
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("tebstb"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
|
||||
function test_insertText(): void {
|
||||
textEdit.insert(0, "test");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
textEdit.insert(4, "**b");
|
||||
compare(chatMarkdownHelper.checkText("test**b"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "test");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
textEdit.insert(2, "**b");
|
||||
compare(chatMarkdownHelper.checkText("te**bst"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
}
|
||||
82
autotests/chatmarkdownhelpertestwrapper.h
Normal file
82
autotests/chatmarkdownhelpertestwrapper.h
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include "chatmarkdownhelper.h"
|
||||
#include "chattextitemhelper.h"
|
||||
#include "enums/richformat.h"
|
||||
|
||||
class ChatMarkdownHelperTestWrapper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QML text Item the ChatMerkdownHelper is handling.
|
||||
*/
|
||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
public:
|
||||
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
|
||||
, m_textItem(new ChatTextItemHelper(this))
|
||||
{
|
||||
m_chatMarkdownHelper->setTextItem(m_textItem);
|
||||
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
|
||||
}
|
||||
|
||||
QQuickItem *textItem() const
|
||||
{
|
||||
return m_textItem->textItem();
|
||||
}
|
||||
void setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
m_textItem->setTextItem(textItem);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkText(const QString &text)
|
||||
{
|
||||
const auto doc = m_textItem->document();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
return text == doc->toPlainText();
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFormats(QList<RichFormat::Format> formats)
|
||||
{
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return RichFormat::formatsAtCursor(cursor) == formats;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void clear()
|
||||
{
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.select(QTextCursor::Document);
|
||||
cursor.removeSelectedText();
|
||||
cursor.setBlockCharFormat(RichFormat::charFormatForFormat(RichFormat::Paragraph));
|
||||
cursor.setBlockFormat(RichFormat::blockFormatForFormat(RichFormat::Paragraph));
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
void unhandledBlockFormat(RichFormat::Format format);
|
||||
|
||||
private:
|
||||
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
|
||||
QPointer<ChatTextItemHelper> m_textItem;
|
||||
};
|
||||
301
autotests/chattextitemhelpertest.qml
Normal file
301
autotests/chattextitemhelpertest.qml
Normal file
@@ -0,0 +1,301 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatTextItemHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: textEdit2
|
||||
}
|
||||
|
||||
ChatTextItemHelper {
|
||||
id: textItemHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
ChatTextItemHelperTestHelper {
|
||||
id: testHelper
|
||||
|
||||
textItem: textItemHelper
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyItem
|
||||
target: textItemHelper
|
||||
signalName: "textItemChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyContentsChanged
|
||||
target: textItemHelper
|
||||
signalName: "contentsChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyContentsChange
|
||||
target: textItemHelper
|
||||
signalName: "contentsChange"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyCursor
|
||||
target: textItemHelper
|
||||
signalName: "cursorPositionChanged"
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
testHelper.setFixedChars("", "");
|
||||
textEdit.clear();
|
||||
textEdit2.clear();
|
||||
spyItem.clear();
|
||||
spyContentsChange.clear();
|
||||
spyContentsChanged.clear();
|
||||
spyCursor.clear();
|
||||
}
|
||||
|
||||
function cleanupTestCase(): void {
|
||||
testHelper.textItem = null;
|
||||
textItemHelper.textItem = null;
|
||||
}
|
||||
|
||||
function test_item(): void {
|
||||
compare(textItemHelper.textItem, textEdit);
|
||||
compare(spyItem.count, 0);
|
||||
textItemHelper.textItem = textEdit2;
|
||||
compare(textItemHelper.textItem, textEdit2);
|
||||
compare(spyItem.count, 1);
|
||||
textItemHelper.textItem = textEdit;
|
||||
compare(textItemHelper.textItem, textEdit);
|
||||
compare(spyItem.count, 2);
|
||||
}
|
||||
|
||||
function test_fixedChars(): void {
|
||||
textEdit.forceActiveFocus();
|
||||
testHelper.setFixedChars("1", "2");
|
||||
compare(textEdit.text, "12");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 0);
|
||||
keyClick("b");
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 1);
|
||||
keyClick(Qt.Key_Left);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
keyClick(Qt.Key_Left);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 3);
|
||||
keyClick(Qt.Key_Right);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 4);
|
||||
keyClick(Qt.Key_Right);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 5);
|
||||
}
|
||||
|
||||
function test_document(): void {
|
||||
// We can't get to the QTextDocument from QML so we have to use a helper function.
|
||||
compare(testHelper.compareDocuments(textEdit.textDocument), true);
|
||||
|
||||
textEdit.insert(0, "test text");
|
||||
compare(testHelper.lineCount(), 1);
|
||||
textEdit.insert(textEdit.text.length, "\ntest text");
|
||||
compare(testHelper.lineCount(), 2);
|
||||
textEdit.clear()
|
||||
compare(textEdit.text.length, 0);
|
||||
}
|
||||
|
||||
function test_takeFirstBlock(): void {
|
||||
textEdit.insert(0, "test text");
|
||||
compare(testHelper.firstBlockText(), "test text");
|
||||
compare(textEdit.text.length, 0);
|
||||
textEdit.insert(0, "test text\nmore test text");
|
||||
compare(testHelper.firstBlockText(), "test text");
|
||||
compare(textEdit.text, "more test text");
|
||||
compare(testHelper.firstBlockText(), "more test text");
|
||||
compare(textEdit.text, "");
|
||||
compare(textEdit.text.length, 0);
|
||||
}
|
||||
|
||||
function test_fillFragments(): void {
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
textEdit.cursorPosition = 16;
|
||||
compare(testHelper.checkFragments("before fragment", "mid fragment", "after fragment"), true);
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
textEdit.cursorPosition = 29;
|
||||
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
|
||||
textEdit.clear();
|
||||
}
|
||||
|
||||
function test_insertFragment(): void {
|
||||
testHelper.insertFragment("test text");
|
||||
compare(textEdit.text, "test text");
|
||||
compare(textEdit.cursorPosition, 9);
|
||||
testHelper.insertFragment("beginning ", 1);
|
||||
compare(textEdit.text, "beginning test text");
|
||||
compare(textEdit.cursorPosition, 10);
|
||||
testHelper.insertFragment(" end", 2);
|
||||
compare(textEdit.text, "beginning test text end");
|
||||
compare(textEdit.cursorPosition, 23);
|
||||
textEdit.clear();
|
||||
|
||||
testHelper.insertFragment("test text", 0, true);
|
||||
compare(textEdit.text, "test text");
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
}
|
||||
|
||||
function test_cursor(): void {
|
||||
// We can't get to the QTextCursor from QML so we have to use a helper function.
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get the appropriate content and cursor change signals when inserting text.
|
||||
textEdit.insert(0, "test text")
|
||||
compare(spyContentsChange.count, 1);
|
||||
compare(spyContentsChange.signalArguments[0][0], 0);
|
||||
compare(spyContentsChange.signalArguments[0][1], 0);
|
||||
compare(spyContentsChange.signalArguments[0][2], 9);
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 1);
|
||||
compare(spyCursor.signalArguments[0][0], true);
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get only get a cursor change signal when moving the cursor.
|
||||
textEdit.cursorPosition = 4;
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
compare(spyCursor.signalArguments[1][0], false);
|
||||
textEdit.selectAll();
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get the appropriate content and cursor change signals when removing text.
|
||||
textEdit.clear();
|
||||
compare(spyContentsChange.count, 2);
|
||||
compare(spyContentsChange.signalArguments[1][0], 0);
|
||||
compare(spyContentsChange.signalArguments[1][1], 9);
|
||||
compare(spyContentsChange.signalArguments[1][2], 0);
|
||||
compare(spyContentsChanged.count, 2);
|
||||
compare(spyCursor.count, 3);
|
||||
compare(spyCursor.signalArguments[2][0], true);
|
||||
}
|
||||
|
||||
function test_setCursor(): void {
|
||||
textEdit.insert(0, "test text");
|
||||
compare(textEdit.cursorPosition, 9);
|
||||
compare(spyCursor.count, 1);
|
||||
testHelper.setCursorPosition(5);
|
||||
compare(textEdit.cursorPosition, 5);
|
||||
compare(spyCursor.count, 2);
|
||||
testHelper.setCursorPosition(1);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 3);
|
||||
|
||||
textEdit.cursorVisible = false;
|
||||
compare(textEdit.cursorVisible, false);
|
||||
testHelper.setCursorVisible(true);
|
||||
compare(textEdit.cursorVisible, true);
|
||||
testHelper.setCursorVisible(false);
|
||||
compare(textEdit.cursorVisible, false);
|
||||
}
|
||||
|
||||
function test_setCursorFromTextItem(): void {
|
||||
textEdit.insert(0, "line 1\nline 2");
|
||||
textEdit2.insert(0, "line 1\nline 2");
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 0);
|
||||
compare(textEdit.cursorPosition, 7);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 7);
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 1);
|
||||
compare(textEdit.cursorPosition, 8);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 8);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
|
||||
testHelper.setFixedChars("1", "2");
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 0);
|
||||
compare(textEdit.cursorPosition, 8);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 7);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
}
|
||||
|
||||
function test_mergeFormat(): void {
|
||||
textEdit.insert(0, "lots of text");
|
||||
testHelper.setCursorPosition(0);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Bold);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Italic);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Italic]), true);
|
||||
testHelper.setCursorPosition(6);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Underline);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Underline]), true);
|
||||
testHelper.setCursorPosition(9);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Strikethrough);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Strikethrough]), true);
|
||||
textEdit.clear();
|
||||
|
||||
textEdit.insert(0, "heading");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Heading1);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading1]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Heading2);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading2]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Paragraph);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
textEdit.clear();
|
||||
|
||||
textEdit.insert(0, "text");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.UnorderedList);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.UnorderedList]), true);
|
||||
compare(testHelper.markdownText(), "- text");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.OrderedList);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.OrderedList]), true);
|
||||
compare(testHelper.markdownText(), "1. text");
|
||||
textEdit.clear();
|
||||
}
|
||||
|
||||
function test_list(): void {
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), false);
|
||||
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), false);
|
||||
}
|
||||
|
||||
function test_forceActiveFocus(): void {
|
||||
textEdit2.forceActiveFocus();
|
||||
compare(textEdit.activeFocus, false);
|
||||
testHelper.forceActiveFocus();
|
||||
compare(textEdit.activeFocus, true);
|
||||
}
|
||||
}
|
||||
218
autotests/chattextitemhelpertesthelper.h
Normal file
218
autotests/chattextitemhelpertesthelper.h
Normal file
@@ -0,0 +1,218 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include "chattextitemhelper.h"
|
||||
|
||||
class ChatTextItemHelperTestHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QML text Item the TextItemHelper is handling.
|
||||
*/
|
||||
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
public:
|
||||
explicit ChatTextItemHelperTestHelper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ChatTextItemHelper *textItem() const
|
||||
{
|
||||
return m_textItem;
|
||||
}
|
||||
void setTextItem(ChatTextItemHelper *textItem)
|
||||
{
|
||||
if (textItem == m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem = textItem;
|
||||
Q_EMIT textItemChanged();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setFixedChars(const QString &startChars, const QString &endChars)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setFixedChars(startChars, endChars);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return document->textDocument() == m_textItem->document();
|
||||
}
|
||||
|
||||
Q_INVOKABLE int lineCount()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return -1;
|
||||
}
|
||||
return m_textItem->lineCount();
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString firstBlockText()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return {};
|
||||
}
|
||||
return m_textItem->takeFirstBlock().toPlainText();
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFragments(const QString &before, const QString &mid, const QString &after)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasBefore = false;
|
||||
QTextDocumentFragment midFragment;
|
||||
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
||||
m_textItem->fillFragments(hasBefore, midFragment, afterFragment);
|
||||
|
||||
return hasBefore && m_textItem->document()->toPlainText() == before && midFragment.toPlainText() == mid && after.isEmpty()
|
||||
? !afterFragment
|
||||
: afterFragment->toPlainText() == after;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void insertFragment(const QString &text, ChatTextItemHelper::InsertPosition position = ChatTextItemHelper::Cursor, bool keepPosition = false)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
const auto fragment = QTextDocumentFragment::fromPlainText(text);
|
||||
m_textItem->insertFragment(fragment, position, keepPosition);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
const auto posSame = cursor.position() == cursorPosition;
|
||||
const auto startSame = cursor.selectionStart() == selectionStart;
|
||||
const auto endSame = cursor.selectionEnd() == selectionEnd;
|
||||
return posSame && startSame && endSame;
|
||||
}
|
||||
|
||||
Q_INVOKABLE int cursorPosition() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return -1;
|
||||
}
|
||||
return *m_textItem->cursorPosition();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorPosition(int pos)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setCursorPosition(pos);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorVisible(bool visible)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setCursorVisible(visible);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorFromTextItem(QQuickItem *item, bool infront, int cursorPos)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
const auto textItem = new ChatTextItemHelper(this);
|
||||
textItem->setTextItem(item);
|
||||
textItem->setCursorPosition(cursorPos);
|
||||
m_textItem->setCursorFromTextItem(textItem, infront);
|
||||
textItem->deleteLater();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void mergeFormatOnCursor(RichFormat::Format format)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->mergeFormatOnCursor(format);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFormatsAtCursor(QList<RichFormat::Format> formats)
|
||||
{
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return RichFormat::formatsAtCursor(cursor) == formats;
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool canIndentListMoreAtCursor() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return m_textItem->canIndentListMoreAtCursor();
|
||||
}
|
||||
Q_INVOKABLE bool canIndentListLessAtCursor() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return m_textItem->canIndentListLessAtCursor();
|
||||
}
|
||||
Q_INVOKABLE void indentListMoreAtCursor()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->indentListMoreAtCursor();
|
||||
}
|
||||
Q_INVOKABLE void indentListLessAtCursor()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->indentListLessAtCursor();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void forceActiveFocus() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->forceActiveFocus();
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString markdownText() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return {};
|
||||
}
|
||||
return m_textItem->markdownText();
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
|
||||
private:
|
||||
QPointer<ChatTextItemHelper> m_textItem;
|
||||
};
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "models/livelocationsmodel.h"
|
||||
#include "models/locationsmodel.h"
|
||||
#include "models/messagecontentfiltermodel.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "models/notificationsmodel.h"
|
||||
#include "models/permissionsmodel.h"
|
||||
#include "models/pinnedmessagemodel.h"
|
||||
@@ -179,7 +180,7 @@ void ModelTest::testRoomTreeModel()
|
||||
|
||||
void ModelTest::testMessageContentModel()
|
||||
{
|
||||
auto contentModel = std::make_unique<MessageContentModel>(room, nullptr, eventId);
|
||||
auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
|
||||
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
@@ -193,21 +194,23 @@ void ModelTest::testEventMessageContentModel()
|
||||
|
||||
void ModelTest::testThreadModel()
|
||||
{
|
||||
auto model = new ThreadModel(eventId, room);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
auto model = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreadFetchModel()
|
||||
{
|
||||
auto model = new ThreadFetchModel(new ThreadModel(eventId, room));
|
||||
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto model = new ThreadFetchModel(threadModel.get());
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreadChatBarModel()
|
||||
{
|
||||
auto model = new ThreadChatBarModel(new ThreadModel(eventId, room), room);
|
||||
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto model = new ThreadChatBarModel(threadModel.get(), room);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
@@ -397,9 +400,7 @@ void ModelTest::testCompletionModel()
|
||||
auto model = new CompletionModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
model->setAutoCompletionType(CompletionModel::Room);
|
||||
model->setText(u"foo"_s, u"#foo"_s);
|
||||
auto roomListModel = new RoomListModel(this);
|
||||
roomListModel->setConnection(connection);
|
||||
model->setRoomListModel(roomListModel);
|
||||
|
||||
9
autotests/qmltest.cpp
Normal file
9
autotests/qmltest.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <quicktest.h>
|
||||
|
||||
QUICK_TEST_MAIN(NeoChat)
|
||||
@@ -121,7 +121,7 @@ void Server::start()
|
||||
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
|
||||
void(key.open(QFile::ReadOnly));
|
||||
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
|
||||
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
|
||||
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).constFirst());
|
||||
m_sslServer.setSslConfiguration(config);
|
||||
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
|
||||
qFatal() << "Server failed to listen on a port.";
|
||||
@@ -227,7 +227,8 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
QJsonObject joinRooms;
|
||||
auto token = request.query().queryItemValue(u"since"_s).toInt();
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
const auto changes = m_state.mid(token);
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &newRoom : change.newRooms) {
|
||||
QJsonArray stateEvents;
|
||||
stateEvents += QJsonObject{
|
||||
@@ -272,7 +273,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &invitation : change.invitations) {
|
||||
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
|
||||
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
|
||||
@@ -299,7 +300,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &ban : change.bans) {
|
||||
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
|
||||
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
|
||||
@@ -326,7 +327,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &join : change.joins) {
|
||||
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
|
||||
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
|
||||
@@ -353,7 +354,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &state : change.stateEvents) {
|
||||
const auto &roomId = state.fullJson[u"room_id"_s].toString();
|
||||
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
|
||||
@@ -365,7 +366,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : m_state.mid(token)) {
|
||||
for (const auto &change : changes) {
|
||||
for (const auto &event : change.events) {
|
||||
// TODO the room might be in a different join state.
|
||||
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
|
||||
|
||||
@@ -73,6 +73,16 @@ void WindowControllerTest::toggle()
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.windowState(), Qt::WindowNoState);
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
|
||||
// make sure we restore maximized state when toggling
|
||||
instance.toggleWindow();
|
||||
window.setVisibility(QWindow::Maximized);
|
||||
QCOMPARE(window.windowState(), Qt::WindowMaximized);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.windowState(), Qt::WindowMaximized);
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
}
|
||||
|
||||
QTEST_MAIN(WindowControllerTest)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1417
po/ar/neochat.po
1417
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1273
po/ast/neochat.po
1273
po/ast/neochat.po
File diff suppressed because it is too large
Load Diff
1426
po/az/neochat.po
1426
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1419
po/ca/neochat.po
1419
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1348
po/cs/neochat.po
1348
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1381
po/da/neochat.po
1381
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
2008
po/de/neochat.po
2008
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1456
po/el/neochat.po
1456
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1512
po/en_GB/neochat.po
1512
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1482
po/eo/neochat.po
1482
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
1338
po/es/neochat.po
1338
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1500
po/eu/neochat.po
1500
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1490
po/fi/neochat.po
1490
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1527
po/fr/neochat.po
1527
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1278
po/ga/neochat.po
1278
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
1486
po/gl/neochat.po
1486
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
1398
po/he/neochat.po
1398
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
1496
po/hi/neochat.po
1496
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
1483
po/hu/neochat.po
1483
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1425
po/ia/neochat.po
1425
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1435
po/id/neochat.po
1435
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1410
po/ie/neochat.po
1410
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1483
po/it/neochat.po
1483
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1273
po/ja/neochat.po
1273
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1417
po/ka/neochat.po
1417
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1478
po/ko/neochat.po
1478
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
1393
po/lt/neochat.po
1393
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1472
po/lv/neochat.po
1472
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
1422
po/nl/neochat.po
1422
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1393
po/nn/neochat.po
1393
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1428
po/pa/neochat.po
1428
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1529
po/pl/neochat.po
1529
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1421
po/pt/neochat.po
1421
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1420
po/pt_BR/neochat.po
1420
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1397
po/ro/neochat.po
1397
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
2745
po/ru/neochat.po
2745
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1496
po/sa/neochat.po
1496
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
1423
po/sk/neochat.po
1423
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1404
po/sl/neochat.po
1404
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1478
po/sv/neochat.po
1478
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1481
po/ta/neochat.po
1481
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1371
po/tok/neochat.po
1371
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
1409
po/tr/neochat.po
1409
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1417
po/uk/neochat.po
1417
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1287
po/zh_CN/neochat.po
1287
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1483
po/zh_TW/neochat.po
1483
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/AttachmentPane.qml
|
||||
qml/QuickFormatBar.qml
|
||||
qml/UserDetailDialog.qml
|
||||
qml/OpenFileDialog.qml
|
||||
qml/KeyVerificationDialog.qml
|
||||
qml/ConfirmLogoutDialog.qml
|
||||
qml/VerificationMessage.qml
|
||||
@@ -79,7 +78,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/EmojiSas.qml
|
||||
qml/VerificationCanceled.qml
|
||||
qml/MessageSourceSheet.qml
|
||||
qml/LocationChooser.qml
|
||||
qml/InvitationView.qml
|
||||
qml/AvatarTabButton.qml
|
||||
qml/OsmLocationPlugin.qml
|
||||
@@ -105,7 +103,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/HoverLinkIndicator.qml
|
||||
qml/AvatarNotification.qml
|
||||
qml/ReasonDialog.qml
|
||||
qml/NewPollDialog.qml
|
||||
qml/UserMenu.qml
|
||||
qml/MeetingDialog.qml
|
||||
qml/SeenByDialog.qml
|
||||
@@ -174,12 +171,7 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
||||
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
|
||||
|
||||
if(NOT ANDROID)
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
|
||||
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||
endif()
|
||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||
target_link_libraries(neochat PUBLIC KF6::WindowSystem)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
||||
endif()
|
||||
|
||||
@@ -29,10 +29,8 @@
|
||||
#include "proxycontroller.h"
|
||||
#include "roommanager.h"
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||
#if !defined(Q_OS_ANDROID)
|
||||
#include "trayicon.h"
|
||||
#elif !defined(Q_OS_ANDROID)
|
||||
#include "trayicon_sni.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
|
||||
@@ -74,6 +74,11 @@ QHash<int, QByteArray> CommonRoomsModel::roleNames() const
|
||||
};
|
||||
}
|
||||
|
||||
bool CommonRoomsModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void CommonRoomsModel::reload()
|
||||
{
|
||||
if (!m_connection || m_userId.isEmpty()) {
|
||||
@@ -89,15 +94,26 @@ void CommonRoomsModel::reload()
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
|
||||
const auto &replyData = job->jsonData();
|
||||
beginResetModel();
|
||||
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
||||
m_commonRooms.push_back(roomId.toString());
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
m_loading = true;
|
||||
Q_EMIT loadingChanged();
|
||||
|
||||
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId)
|
||||
.then([this](const auto job) {
|
||||
const auto &replyData = job->jsonData();
|
||||
beginResetModel();
|
||||
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
||||
m_commonRooms.push_back(roomId.toString());
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
})
|
||||
.onFailure([this] {
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
});
|
||||
}
|
||||
|
||||
#include "moc_commonroomsmodel.cpp"
|
||||
|
||||
@@ -21,6 +21,7 @@ class CommonRoomsModel : public QAbstractListModel
|
||||
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
|
||||
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
@@ -43,10 +44,13 @@ public:
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
bool loading() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void userIdChanged();
|
||||
void countChanged();
|
||||
void loadingChanged();
|
||||
|
||||
private:
|
||||
void reload();
|
||||
@@ -54,4 +58,5 @@ private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
QString m_userId;
|
||||
QList<QString> m_commonRooms;
|
||||
bool m_loading = false;
|
||||
};
|
||||
|
||||
@@ -207,10 +207,6 @@
|
||||
</entry>
|
||||
</group>
|
||||
<group name="FeatureFlags">
|
||||
<entry name="Threads" type="bool">
|
||||
<label>Enable threads</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="Phone3PId" type="bool">
|
||||
<label>Enable add phone numbers as 3PIDs</label>
|
||||
<default>false</default>
|
||||
|
||||
@@ -21,6 +21,7 @@ Delegates.RoundedItemDelegate {
|
||||
signal contextMenuRequested
|
||||
signal selected
|
||||
|
||||
activeFocusOnTab: true
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
|
||||
@@ -8,45 +8,26 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
import Quotient
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
|
||||
subtitle: {
|
||||
if (root.room) {
|
||||
let message = xi18nc("@info Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml)
|
||||
|
||||
// List any possible side-effects the user needs to be made aware of.
|
||||
if (root.room.historyVisibility !== "world_readable" && root.room.historyVisibility !== "shared") {
|
||||
message += xi18nc("@info", "<br><strong>This room's history is limited to when you rejoin the room.</strong>")
|
||||
}
|
||||
|
||||
if (root.room.joinRule === JoinRule.JoinRule.Invite) {
|
||||
message += xi18nc("@info", "<br><strong>This room can only be rejoined with an invite.</strong>");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
onAccepted: root.room.forget()
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Leave Room")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
text: i18nc("@action:button Leave this room/space", "Leave")
|
||||
icon.name: "arrow-left-symbolic"
|
||||
//onClicked: root.room.forget();
|
||||
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,16 @@ Kirigami.PromptDialog {
|
||||
title: i18nc("@title:dialog", "Sign out")
|
||||
subtitle: i18n("Are you sure you want to sign out?")
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
onAccepted: root.connection.logout(true)
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Sign out")
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.connection.logout(true);
|
||||
root.close();
|
||||
root.accepted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,5 @@ Kirigami.PromptDialog {
|
||||
|
||||
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
|
||||
|
||||
onAccepted: {
|
||||
Qt.openUrlExternally(root.link);
|
||||
root.close();
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
onAccepted: Qt.openUrlExternally(root.link)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
@@ -24,6 +25,8 @@ Kirigami.Dialog {
|
||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||
|
||||
title: i18nc("@title", "Manually Enter a Room")
|
||||
showCloseButton: false
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
|
||||
leftPadding: 0
|
||||
@@ -31,35 +34,26 @@ Kirigami.Dialog {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: roomIdAliasText.isValidText
|
||||
text: i18n("OK")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
// We don't necessarily have all the info so fill out the best we can.
|
||||
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
||||
let displayName = "";
|
||||
let avatarUrl = "";
|
||||
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
||||
let topic = "";
|
||||
let memberCount = -1;
|
||||
let isJoined = false;
|
||||
if (roomIdAliasText.room) {
|
||||
roomId = roomIdAliasText.room.id;
|
||||
displayName = roomIdAliasText.room.displayName;
|
||||
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
||||
alias = roomIdAliasText.room.canonicalAlias;
|
||||
topic = roomIdAliasText.room.topic;
|
||||
memberCount = roomIdAliasText.room.joinedCount;
|
||||
isJoined = true;
|
||||
}
|
||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||
root.close();
|
||||
}
|
||||
onAccepted: {
|
||||
// We don't necessarily have all the info so fill out the best we can.
|
||||
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
||||
let displayName = "";
|
||||
let avatarUrl = "";
|
||||
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
||||
let topic = "";
|
||||
let memberCount = -1;
|
||||
let isJoined = false;
|
||||
if (roomIdAliasText.room) {
|
||||
roomId = roomIdAliasText.room.id;
|
||||
displayName = roomIdAliasText.room.displayName;
|
||||
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
||||
alias = roomIdAliasText.room.canonicalAlias;
|
||||
topic = roomIdAliasText.room.topic;
|
||||
memberCount = roomIdAliasText.room.joinedCount;
|
||||
isJoined = true;
|
||||
}
|
||||
]
|
||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
@@ -110,4 +104,16 @@ Kirigami.Dialog {
|
||||
roomIdAliasText.forceActiveFocus();
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button Join this room/space", "Join")
|
||||
icon.name: "checkmark"
|
||||
enabled: roomIdAliasText.isValidText
|
||||
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,25 +24,16 @@ Kirigami.Dialog {
|
||||
signal userSelected(string userId)
|
||||
|
||||
title: i18nc("@title", "User ID")
|
||||
showCloseButton: false
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: userIdText.isValidText
|
||||
text: i18n("OK")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
root.userSelected(userIdText.text)
|
||||
root.accept();
|
||||
}
|
||||
}
|
||||
]
|
||||
onAccepted: root.userSelected(userIdText.text)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
@@ -79,4 +70,16 @@ Kirigami.Dialog {
|
||||
userIdText.forceActiveFocus();
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button Perform an action with this user ID", "Ok")
|
||||
icon.name: "checkmark"
|
||||
enabled: userIdText.isValidText
|
||||
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
@@ -14,11 +15,16 @@ Kirigami.PromptDialog {
|
||||
|
||||
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
|
||||
subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.")
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
customFooterActions: Kirigami.Action {
|
||||
icon.name: "camera-video-symbolic"
|
||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
onTriggered: root.accept()
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
icon.name: "camera-video-symbolic"
|
||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ Kirigami.Page {
|
||||
|
||||
spacing: 0
|
||||
|
||||
readonly property bool shouldShowPins: root.currentRoom.pinnedMessage.length > 0 && !Kirigami.Settings.isMobile
|
||||
readonly property bool shouldShowPins: root.currentRoom?.pinnedMessage.length > 0 && !Kirigami.Settings.isMobile
|
||||
|
||||
QQC2.Control {
|
||||
id: pinControl
|
||||
@@ -361,7 +361,6 @@ Kirigami.Page {
|
||||
id: chatBar
|
||||
width: parent.width
|
||||
currentRoom: root.currentRoom
|
||||
connection: root.currentRoom.connection as NeoChatConnection
|
||||
|
||||
// Creating a reply (or doing anything in the chat bar) can change the height, but this isn't picked up on the root's onHeightChanged.
|
||||
onHeightChanged: root.resetViewSettling()
|
||||
|
||||
@@ -13,7 +13,7 @@ FormCard.FormCardPage {
|
||||
|
||||
property bool processing: false
|
||||
|
||||
title: i18nc("@title:window", "Manage Secret Backup")
|
||||
title: i18nc("@title:window", "Manage Key Storage")
|
||||
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
leftPadding: 0
|
||||
@@ -32,7 +32,7 @@ FormCard.FormCardPage {
|
||||
function onKeyBackupError(): void {
|
||||
securityKeyField.clear()
|
||||
root.processing = false
|
||||
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
|
||||
banner.text = i18nc("@info:status", "The recovery key was not correct.")
|
||||
banner.visible = true
|
||||
}
|
||||
|
||||
@@ -45,20 +45,20 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
|
||||
title: i18nc("@title", "Unlock using Recovery Key")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
|
||||
description: i18nc("@info", "If you have a recovery key (also known as a “security key” or “backup passphrase”), enter it below or upload it as a file.")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: securityKeyField
|
||||
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
|
||||
label: i18nc("@label:textbox", "Recovery Key:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: uploadSecurityKeyButton
|
||||
text: i18nc("@action:button", "Upload from File")
|
||||
text: i18nc("@action:button", "Upload From File")
|
||||
icon.name: "cloud-upload"
|
||||
enabled: !root.processing
|
||||
onClicked: {
|
||||
@@ -83,12 +83,12 @@ FormCard.FormCardPage {
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
description: i18nc("@info", "If you have previously verified this device, you can try loading the backup key from other devices by clicking the button below.")
|
||||
description: i18nc("@info", "If you have previously verified this device, you request encryption keys from other verified devices.")
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: unlockCrossSigningButton
|
||||
icon.name: "emblem-shared-symbolic"
|
||||
text: i18nc("@action:button", "Request from other Devices")
|
||||
text: i18nc("@action:button", "Request From Other Devices")
|
||||
enabled: !root.processing
|
||||
onClicked: {
|
||||
root.processing = true
|
||||
|
||||
@@ -125,7 +125,7 @@ Kirigami.Dialog {
|
||||
text: root.shareUrl,
|
||||
title: root.displayName,
|
||||
subtitle: root.user.id,
|
||||
avatarColor: root.room?.member(root.user.id).color,
|
||||
avatarColor: root.room?.member(root.user.id).color ?? null,
|
||||
avatarSource: avatar.source,
|
||||
}) as QrCodeMaximizeComponent;
|
||||
root.close();
|
||||
@@ -401,18 +401,36 @@ Kirigami.Dialog {
|
||||
Kirigami.Heading {
|
||||
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
|
||||
level: 4
|
||||
visible: !root.isSelf && root.hasMutualRooms
|
||||
visible: !root.isSelf && root.connection.canCheckMutualRooms
|
||||
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
visible: !root.isSelf && root.hasMutualRooms
|
||||
visible: !root.isSelf && root.connection.canCheckMutualRooms
|
||||
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
visible: roomRepeater.count === 0 && root.model.loading
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: roomRepeater.count === 0 && !root.model.loading
|
||||
text: i18nc("@info:label", "No rooms in common")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: roomRepeater
|
||||
|
||||
model: root.limiterModel
|
||||
|
||||
delegate: KirigamiComponents.AvatarButton {
|
||||
|
||||
@@ -16,7 +16,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
required property Kirigami.ApplicationWindow window
|
||||
required property var author
|
||||
required property NeochatRoomMember author
|
||||
|
||||
headerContentItem: RowLayout {
|
||||
id: detailRow
|
||||
@@ -68,7 +68,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
text: i18nc("@action:button", "Mention")
|
||||
icon.name: "username-copy-symbolic"
|
||||
onTriggered: {
|
||||
RoomManager.currentRoom.mainCache.mentionAdded(root.author.id);
|
||||
RoomManager.currentRoom.mainCache.mentionAdded(root.author.disambiguatedName, "https://matrix.to/#/" + root.author.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,15 +129,6 @@ RoomManager::RoomManager(QObject *parent)
|
||||
m_messageFilterModel->invalidate();
|
||||
}
|
||||
});
|
||||
ContentProvider::self().setThreadsEnabled(NeoChatConfig::threads());
|
||||
MessageModel::setThreadsEnabled(NeoChatConfig::threads());
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this] {
|
||||
ContentProvider::self().setThreadsEnabled(NeoChatConfig::threads());
|
||||
MessageModel::setThreadsEnabled(NeoChatConfig::threads());
|
||||
if (m_timelineModel) {
|
||||
Q_EMIT m_timelineModel->threadsEnabledChanged();
|
||||
}
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SortOrderChanged, this, [this]() {
|
||||
m_sortFilterRoomTreeModel->invalidate();
|
||||
});
|
||||
@@ -546,13 +537,14 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
||||
LastRoomBlocker blocker(this);
|
||||
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home":
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString());
|
||||
!lastRoom.isEmpty() && m_connection->room(lastRoom)) {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no last room was opened, go to the space home:
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s && m_connection->room(spaceId)) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public:
|
||||
*
|
||||
* @sa Quotient::UriResolverBase::visitResource()
|
||||
*/
|
||||
Q_INVOKABLE void resolveResource(Uri uri, const QString &action = {});
|
||||
Q_INVOKABLE void resolveResource(Quotient::Uri uri, const QString &action = {});
|
||||
|
||||
bool hasOpenRoom() const;
|
||||
|
||||
@@ -235,7 +235,7 @@ public:
|
||||
* @brief Show a context menu for the given event.
|
||||
*/
|
||||
Q_INVOKABLE void
|
||||
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
viewEventMenu(QObject *parent, const Quotient::RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
|
||||
/**
|
||||
* @brief Set a URL to be loaded as the initial room.
|
||||
@@ -316,7 +316,7 @@ Q_SIGNALS:
|
||||
const QString &plainText,
|
||||
const QString &richtText,
|
||||
const QString &mimeType,
|
||||
const FileTransferInfo &progressInfo,
|
||||
const Quotient::FileTransferInfo &progressInfo,
|
||||
const QString &selectedText,
|
||||
const QString &hoveredLink);
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "trayicon_sni.h"
|
||||
#include <KWindowSystem>
|
||||
|
||||
#include "windowcontroller.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
TrayIcon::TrayIcon(QObject *parent)
|
||||
: KStatusNotifierItem(parent)
|
||||
{
|
||||
setCategory(KStatusNotifierItem::ItemCategory::Communications);
|
||||
setIconByName(u"org.kde.neochat.tray"_s);
|
||||
|
||||
connect(&WindowController::instance(), &WindowController::windowChanged, this, [this] {
|
||||
setAssociatedWindow(WindowController::instance().window());
|
||||
});
|
||||
}
|
||||
|
||||
void TrayIcon::show()
|
||||
{
|
||||
setStatus(Active);
|
||||
}
|
||||
|
||||
void TrayIcon::hide()
|
||||
{
|
||||
setStatus(Passive);
|
||||
}
|
||||
|
||||
#include "moc_trayicon_sni.cpp"
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <KStatusNotifierItem>
|
||||
|
||||
/**
|
||||
* @class TrayIcon
|
||||
*
|
||||
* A class inheriting KStatusNotifierItem to provide a tray icon.
|
||||
*
|
||||
* @sa KStatusNotifierItem
|
||||
*/
|
||||
class TrayIcon : public KStatusNotifierItem
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TrayIcon(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Show the tray icon.
|
||||
*/
|
||||
void show();
|
||||
|
||||
/**
|
||||
* @brief Hide the tray icon.
|
||||
*/
|
||||
void hide();
|
||||
};
|
||||
@@ -32,6 +32,16 @@ void WindowController::setWindow(QWindow *window)
|
||||
{
|
||||
m_window = window;
|
||||
|
||||
if (window != nullptr) {
|
||||
// to restore maximized state after reopening from the system tray
|
||||
connect(m_window, &QWindow::windowStateChanged, this, [this]() {
|
||||
if (m_window->isVisible()) {
|
||||
m_wasMaximized = m_window->windowStates() & Qt::WindowMaximized;
|
||||
}
|
||||
});
|
||||
m_wasMaximized = m_window->windowStates() & Qt::WindowMaximized;
|
||||
}
|
||||
|
||||
Q_EMIT windowChanged();
|
||||
}
|
||||
|
||||
@@ -46,7 +56,11 @@ void WindowController::showAndRaiseWindow(const QString &startupId)
|
||||
return;
|
||||
}
|
||||
if (!m_window->isVisible()) {
|
||||
m_window->show();
|
||||
if (m_wasMaximized) {
|
||||
m_window->showMaximized();
|
||||
} else {
|
||||
m_window->show();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
|
||||
@@ -67,5 +67,6 @@ Q_SIGNALS:
|
||||
private:
|
||||
WindowController() = default;
|
||||
|
||||
bool m_wasMaximized;
|
||||
QWindow *m_window = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Popup {
|
||||
id: root
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
signal chosen(string path)
|
||||
|
||||
contentItem: RowLayout {
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.fillHeight: true
|
||||
|
||||
icon.name: 'mail-attachment'
|
||||
|
||||
text: i18nc("@action:button", "Choose local file")
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay) as OpenFileDialog;
|
||||
fileDialog.chosen.connect(path => root.chosen(path));
|
||||
fileDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {}
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.fillHeight: true
|
||||
|
||||
icon.name: 'insert-image'
|
||||
text: i18nc("@action:button", "Clipboard image")
|
||||
onClicked: {
|
||||
const path = StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + "/screenshots/" + (new Date()).getTime() + ".png";
|
||||
if (!Clipboard.saveImage(path)) {
|
||||
return;
|
||||
}
|
||||
root.chosen(path);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
parentWindow: Window.window
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.chatbar
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
|
||||
QML_FILES
|
||||
AttachDialog.qml
|
||||
ChatBar.qml
|
||||
ChatBarCore.qml
|
||||
RichEditBar.qml
|
||||
SendBar.qml
|
||||
CompletionMenu.qml
|
||||
EmojiDelegate.qml
|
||||
EmojiGrid.qml
|
||||
@@ -15,6 +17,25 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
EmojiPicker.qml
|
||||
EmojiDialog.qml
|
||||
EmojiTonesPicker.qml
|
||||
StylePicker.qml
|
||||
StyleDelegate.qml
|
||||
ImageEditorPage.qml
|
||||
VoiceMessageDialog.qml
|
||||
LinkDialog.qml
|
||||
LocationChooser.qml
|
||||
NewPollDialog.qml
|
||||
TableDialog.qml
|
||||
StyleButton.qml
|
||||
SOURCES
|
||||
chatbuttonhelper.cpp
|
||||
styledelegatehelper.cpp
|
||||
)
|
||||
|
||||
target_include_directories(Chatbar PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(Chatbar PRIVATE
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
Qt::QuickControls2
|
||||
KF6::Kirigami
|
||||
LibNeoChat
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
@@ -25,27 +26,26 @@ import org.kde.neochat.libneochat as LibNeoChat
|
||||
*
|
||||
* @sa ChatBar
|
||||
*/
|
||||
QQC2.Control {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom currentRoom
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
onActiveFocusChanged: textField.forceActiveFocus()
|
||||
|
||||
required property LibNeoChat.NeoChatRoom currentRoom
|
||||
onCurrentRoomChanged: {
|
||||
_private.chatBarCache = currentRoom.mainCache
|
||||
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
|
||||
contentModel.focusedTextItem.
|
||||
textField.text = ShareHandler.text;
|
||||
ShareHandler.text = "";
|
||||
ShareHandler.room = "";
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged: if (activeFocus) {
|
||||
core.forceActiveFocus();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShareHandler
|
||||
function onRoomChanged(): void {
|
||||
@@ -60,356 +60,31 @@ QQC2.Control {
|
||||
Connections {
|
||||
target: root.currentRoom.mainCache
|
||||
|
||||
function onMentionAdded(mention: string): void {
|
||||
// add mention text
|
||||
textField.append(mention + " ");
|
||||
// move cursor to the end
|
||||
textField.cursorPosition = textField.text.length;
|
||||
function onMentionAdded(text: string, hRef: string): void {
|
||||
core.completionModel.insertCompletion(text, hRef);
|
||||
// move the focus back to the chat bar
|
||||
textField.forceActiveFocus(Qt.OtherFocusReason);
|
||||
core.model.refocusCurrentComponent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The list of actions in the ChatBar.
|
||||
*
|
||||
* Each of these will be visualised in the ChatBar so new actions can be added
|
||||
* by appending to this list.
|
||||
*/
|
||||
property list<BusyAction> actions: [
|
||||
BusyAction {
|
||||
id: attachmentAction
|
||||
implicitHeight: column.implicitHeight + Kirigami.Units.largeSpacing
|
||||
|
||||
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||
|
||||
// Matrix does not allow sending attachments in replies
|
||||
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
||||
icon.name: "mail-attachment"
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
if (Clipboard.hasImage) {
|
||||
let dialog = attachDialog.createObject(root.QQC2.Overlay.overlay) as AttachDialog;
|
||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
} else {
|
||||
let dialog = openFileDialog.createObject(root.QQC2.Overlay.overlay) as OpenFileDialog;
|
||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: emojiAction
|
||||
|
||||
isBusy: false
|
||||
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
icon.name: "smiley"
|
||||
text: i18nc("@action:button", "Emojis & Stickers")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
onTriggered: {
|
||||
if (emojiDialog.visible) {
|
||||
emojiDialog.close();
|
||||
} else {
|
||||
emojiDialog.open();
|
||||
}
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: mapButton
|
||||
icon.name: "mark-location-symbolic"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Location")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
(locationChooser.createObject(QQC2.Overlay.overlay, {
|
||||
room: root.currentRoom
|
||||
}) as LocationChooser).open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: pollButton
|
||||
icon.name: "amarok_playcount"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
(newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||
room: root.currentRoom
|
||||
}) as NewPollDialog).open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
icon.name: "microphone"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
onTriggered: {
|
||||
let dialog = voiceMessageDialog.createObject(root, {
|
||||
room: root.currentRoom
|
||||
}) as VoiceMessageDialog;
|
||||
dialog.open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: sendAction
|
||||
|
||||
isBusy: false
|
||||
|
||||
icon.name: "document-send"
|
||||
text: i18nc("@action:button", "Send message")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
onTriggered: {
|
||||
_private.postMessage();
|
||||
}
|
||||
|
||||
tooltip: text
|
||||
ColumnLayout {
|
||||
id: column
|
||||
anchors.top: root.top
|
||||
anchors.horizontalCenter: root.horizontalCenter
|
||||
ChatBarCore {
|
||||
id: core
|
||||
Message.room: root.currentRoom
|
||||
room: root.currentRoom
|
||||
maxAvailableWidth: chatBarSizeHelper.availableWidth
|
||||
}
|
||||
]
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Separator {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
}
|
||||
}
|
||||
|
||||
leftPadding: rightPadding
|
||||
rightPadding: (root.width - chatBarSizeHelper.availableWidth) / 2
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
Item {
|
||||
// Required to adjust for the top separator
|
||||
Layout.preferredHeight: 1
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Loader {
|
||||
id: replyLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.currentRoom.mainCache.replyId.length > 0
|
||||
sourceComponent: replyPane
|
||||
}
|
||||
RowLayout {
|
||||
visible: replyLoader.visible && !root.currentRoom.mainCache.relationAuthorIsPresent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "help-hint-symbolic"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18nc("@info", "The user you're replying to has left the room, and can't be notified.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: attachLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.currentRoom.mainCache.attachmentPath.length > 0
|
||||
sourceComponent: attachmentPane
|
||||
}
|
||||
RowLayout {
|
||||
QQC2.ScrollView {
|
||||
id: chatBarScrollView
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 3
|
||||
|
||||
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
||||
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
id: chatBarHeightAnimation
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.TextArea {
|
||||
id: textField
|
||||
|
||||
placeholderText: root.currentRoom.usesEncryption ? i18nc("@placeholder", "Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18nc("@placeholder", "Set an attachment caption…") : i18nc("@placeholder", "Send a message…")
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
wrapMode: TextEdit.Wrap
|
||||
// This has to stay PlainText or else formatting starts breaking in strange ways
|
||||
textFormat: TextEdit.PlainText
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||
|
||||
Accessible.description: placeholderText
|
||||
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
Timer {
|
||||
id: repeatTimer
|
||||
interval: 5000
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (!repeatTimer.running && NeoChatConfig.typingNotifications) {
|
||||
var textExists = text.length > 0;
|
||||
root.currentRoom.sendTypingNotification(textExists);
|
||||
textExists ? repeatTimer.start() : repeatTimer.stop();
|
||||
}
|
||||
}
|
||||
onSelectedTextChanged: {
|
||||
if (selectedText.length > 0) {
|
||||
quickFormatBar.selectionStart = selectionStart;
|
||||
quickFormatBar.selectionEnd = selectionEnd;
|
||||
quickFormatBar.open();
|
||||
} else if (quickFormatBar.visible) {
|
||||
quickFormatBar.close();
|
||||
}
|
||||
}
|
||||
|
||||
QuickFormatBar {
|
||||
id: quickFormatBar
|
||||
|
||||
x: textField.cursorRectangle.x
|
||||
y: textField.cursorRectangle.y - height
|
||||
|
||||
onFormattingSelected: (format, selectionStart, selectionEnd) => _private.formatText(format, selectionStart, selectionEnd)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: event => {
|
||||
const controlIsPressed = event.modifiers & Qt.ControlModifier;
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
|
||||
textField.insert(cursorPosition, "\n");
|
||||
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
|
||||
_private.postMessage();
|
||||
}
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
const controlIsPressed = event.modifiers & Qt.ControlModifier;
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
|
||||
textField.insert(cursorPosition, "\n");
|
||||
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
|
||||
_private.postMessage();
|
||||
}
|
||||
}
|
||||
Keys.onTabPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else {
|
||||
contextDrawer.handle.children[0].forceActiveFocus()
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||
event.accepted = _private.pasteImage();
|
||||
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
||||
root.currentRoom.replyLastMessage();
|
||||
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
|
||||
root.currentRoom.editLastMessage();
|
||||
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
|
||||
completionMenu.decrementIndex();
|
||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||
completionMenu.incrementIndex();
|
||||
} else if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) {
|
||||
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||
root.currentRoom.sendTypingNotification(false);
|
||||
repeatTimer.stop();
|
||||
}
|
||||
if (quickFormatBar.visible && selectedText.length > 0) {
|
||||
quickFormatBar.close();
|
||||
}
|
||||
} else if (event.key === Qt.Key_Escape && completionMenu.visible) {
|
||||
completionMenu.close();
|
||||
}
|
||||
}
|
||||
Keys.onShortcutOverride: event => {
|
||||
if ((_private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0) && event.key === Qt.Key_Escape) {
|
||||
_private.chatBarCache.attachmentPath = "";
|
||||
_private.chatBarCache.replyId = "";
|
||||
_private.chatBarCache.threadId = "";
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
background: MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
id: actionsRow
|
||||
spacing: 0
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
|
||||
|
||||
Repeater {
|
||||
model: root.actions
|
||||
delegate: QQC2.ToolButton {
|
||||
id: actionDelegate
|
||||
required property BusyAction modelData
|
||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||
onClicked: if (!pieProgress.visible) {
|
||||
modelData.trigger()
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: modelData.tooltip
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
PieProgressBar {
|
||||
id: pieProgress
|
||||
anchors.fill: parent
|
||||
visible: actionDelegate.modelData.isBusy
|
||||
progress: root.currentRoom.fileUploadingProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visible: !Kirigami.Setting.isMobile
|
||||
text: NeoChatConfig.sendMessageWith === 1 ? i18nc("As in enter starts a new line in the chat bar", "Enter starts a new line") : i18nc("As in enter starts send the chat message", "Enter sends the message")
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale * 0.75
|
||||
}
|
||||
}
|
||||
LibNeoChat.DelegateSizeHelper {
|
||||
@@ -419,195 +94,8 @@ QQC2.Control {
|
||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||
startPercentWidth: 100
|
||||
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
|
||||
leftPadding: NeoChatConfig.compactLayout ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
rightPadding: NeoChatConfig.compactLayout ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
maxWidth: NeoChatConfig.compactLayout ? root.width - Kirigami.Units.largeSpacing * 2 : Kirigami.Units.gridUnit * 60
|
||||
}
|
||||
|
||||
Component {
|
||||
id: replyPane
|
||||
Item {
|
||||
implicitHeight: replyComponent.implicitHeight
|
||||
ReplyComponent {
|
||||
id: replyComponent
|
||||
replyContentModel: ContentProvider.contentModelForEvent(root.currentRoom, _private.chatBarCache.replyId, true)
|
||||
Message.maxContentWidth: (replyLoader.item as Item).width
|
||||
|
||||
// When the user replies to a message and the preview is loaded, make sure the text field is focused again
|
||||
Component.onCompleted: textField.forceActiveFocus(Qt.OtherFocusReason)
|
||||
}
|
||||
QQC2.Button {
|
||||
id: cancelButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Cancel reply")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: {
|
||||
_private.chatBarCache.replyId = "";
|
||||
_private.chatBarCache.attachmentPath = "";
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: attachmentPane
|
||||
AttachmentPane {
|
||||
attachmentPath: _private.chatBarCache.attachmentPath
|
||||
|
||||
onAttachmentCancelled: {
|
||||
_private.chatBarCache.attachmentPath = "";
|
||||
root.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
property ChatBarCache chatBarCache
|
||||
|
||||
function postMessage() {
|
||||
_private.chatBarCache.postMessage();
|
||||
repeatTimer.stop();
|
||||
root.currentRoom.markAllMessagesAsRead();
|
||||
textField.clear();
|
||||
}
|
||||
|
||||
function formatText(format, selectionStart, selectionEnd) {
|
||||
let index = textField.cursorPosition;
|
||||
|
||||
/*
|
||||
* There cannot be white space at the beginning or end of the string for the
|
||||
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
|
||||
*/
|
||||
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
||||
if (innerText.charAt(innerText.length - 1) === " ") {
|
||||
let trimmedRightString = innerText.replace(/\s*$/, "");
|
||||
let trimDifference = innerText.length - trimmedRightString.length;
|
||||
selectionEnd -= trimDifference;
|
||||
}
|
||||
if (innerText.charAt(0) === " ") {
|
||||
let trimmedLeftString = innerText.replace(/^\s*/, "");
|
||||
let trimDifference = innerText.length - trimmedLeftString.length;
|
||||
selectionStart = selectionStart + trimDifference;
|
||||
}
|
||||
let startText = textField.text.substr(0, selectionStart);
|
||||
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
|
||||
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
||||
let endText = textField.text.substr(selectionEnd);
|
||||
textField.text = "";
|
||||
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
|
||||
|
||||
/*
|
||||
* Put the cursor where it was when the popup was opened accounting for the
|
||||
* new markup.
|
||||
*
|
||||
* The exception is for a hyperlink where it is placed ready to start typing
|
||||
* the url.
|
||||
*/
|
||||
if (format.extra !== "") {
|
||||
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
|
||||
} else if (index == selectionStart) {
|
||||
textField.cursorPosition = index;
|
||||
} else {
|
||||
textField.cursorPosition = index + format.start.length + format.end.length;
|
||||
}
|
||||
}
|
||||
|
||||
function pasteImage() {
|
||||
let localPath = Clipboard.saveImage();
|
||||
if (localPath.length === 0) {
|
||||
return false;
|
||||
}
|
||||
_private.chatBarCache.attachmentPath = localPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
type: ChatBarType.Room
|
||||
textItem: textField
|
||||
room: root.currentRoom
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
parentWindow: Window.window
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: attachDialog
|
||||
|
||||
AttachDialog {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: locationChooser
|
||||
LocationChooser {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: newPollDialog
|
||||
NewPollDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: voiceMessageDialog
|
||||
VoiceMessageDialog {}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: documentHandler
|
||||
connection: root.connection
|
||||
|
||||
x: 1
|
||||
y: -height
|
||||
width: parent.width - 1
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
|
||||
x: root.width - width
|
||||
y: -implicitHeight
|
||||
|
||||
modal: false
|
||||
includeCustom: true
|
||||
closeOnChosen: false
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
onChosen: emoji => root.insertText(emoji)
|
||||
onClosed: if (emojiAction.checked) {
|
||||
emojiAction.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
function insertText(text) {
|
||||
let initialCursorPosition = textField.cursorPosition;
|
||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition);
|
||||
textField.cursorPosition = initialCursorPosition + text.length;
|
||||
}
|
||||
|
||||
component BusyAction : Kirigami.Action {
|
||||
required property bool isBusy
|
||||
}
|
||||
}
|
||||
|
||||
168
src/chatbar/ChatBarCore.qml
Normal file
168
src/chatbar/ChatBarCore.qml
Normal file
@@ -0,0 +1,168 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property LibNeoChat.NeoChatRoom room
|
||||
|
||||
property int chatBarType: LibNeoChat.ChatBarType.Room
|
||||
|
||||
required property real maxAvailableWidth
|
||||
|
||||
readonly property ChatBarMessageContentModel model: ChatBarMessageContentModel {
|
||||
type: root.chatBarType
|
||||
room: root.room
|
||||
sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0
|
||||
sendTypingNotifications: NeoChatConfig.typingNotifications
|
||||
}
|
||||
|
||||
readonly property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel {
|
||||
textItem: root.model.focusedTextItem
|
||||
roomListModel: RoomManager.roomListModel
|
||||
userListModel: RoomManager.userListModel
|
||||
|
||||
onIsCompletingChanged: {
|
||||
if (!isCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'CompletionMenu').createObject(root.model.focusedTextItem.textItem, {
|
||||
model: root.completionModel,
|
||||
keyHelper: root.model.keyHelper
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
Message.contentModel: root.model
|
||||
|
||||
onActiveFocusChanged: root.model.refocusCurrentComponent()
|
||||
|
||||
implicitWidth: root.maxAvailableWidth - (root.maxAvailableWidth >= (parent?.width ?? 0) ? Kirigami.Units.largeSpacing * 2 : 0)
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
RichEditBar {
|
||||
id: richEditBar
|
||||
visible: NeoChatConfig.sendMessageWith === 1
|
||||
maxAvailableWidth: root.maxAvailableWidth - Kirigami.Units.largeSpacing * 2
|
||||
|
||||
room: root.room
|
||||
contentModel: root.model
|
||||
|
||||
onClicked: root.model.refocusCurrentComponent()
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: NeoChatConfig.sendMessageWith === 1
|
||||
}
|
||||
RowLayout {
|
||||
QQC2.ToolButton {
|
||||
id: emojiButton
|
||||
property EmojiDialog dialog
|
||||
|
||||
icon.name: "smiley"
|
||||
text: i18n("Emojis & Stickers")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: dialog !== null
|
||||
|
||||
onClicked: {
|
||||
if(!checked){
|
||||
if(dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
dialog = Qt.createComponent('org.kde.neochat.chatbar', 'EmojiDialog').createObject(root, {
|
||||
modal: false,
|
||||
includeCustom: true,
|
||||
closeOnChosen: false,
|
||||
currentRoom: root.room,
|
||||
});
|
||||
dialog.y = -dialog.implicitHeight - Kirigami.Units.smallSpacing;
|
||||
dialog.onChosen.connect((emoji) => {
|
||||
root.chatButtonHelper.insertText(emoji);
|
||||
close();
|
||||
});
|
||||
dialog.onClosed.connect(() => {
|
||||
dialog = null;
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: chatScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * (root.model.hasAttachment ? 12 : 8)
|
||||
|
||||
contentWidth: availableWidth
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
readonly property real visibleTop: chatScrollView.QQC2.ScrollBar.vertical.position * chatScrollView.contentHeight
|
||||
readonly property real visibleBottom: chatScrollView.QQC2.ScrollBar.vertical.position * chatScrollView.contentHeight + chatScrollView.QQC2.ScrollBar.vertical.size * chatScrollView.contentHeight
|
||||
readonly property rect cursorRectInColumn: mapFromItem(root.model.focusedTextItem.textItem, root.model.focusedTextItem.cursorRectangle);
|
||||
onCursorRectInColumnChanged: {
|
||||
if (chatScrollView.QQC2.ScrollBar.vertical.visible) {
|
||||
if (cursorRectInColumn.y < visibleTop) {
|
||||
chatScrollView.QQC2.ScrollBar.vertical.position = cursorRectInColumn.y / chatScrollView.contentHeight
|
||||
} else if (cursorRectInColumn.y + cursorRectInColumn.height > visibleBottom) {
|
||||
chatScrollView.QQC2.ScrollBar.vertical.position = (cursorRectInColumn.y + cursorRectInColumn.height - (chatScrollView.QQC2.ScrollBar.vertical.size * chatScrollView.contentHeight)) / chatScrollView.contentHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width: chatScrollView.width
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
id: chatContentView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: root.model
|
||||
|
||||
delegate: BaseMessageComponentChooser {
|
||||
rightAnchorMargin: chatScrollView.QQC2.ScrollBar.vertical.visible ? chatScrollView.QQC2.ScrollBar.vertical.width : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SendBar {
|
||||
room: root.room
|
||||
contentModel: root.model
|
||||
maxAvailableWidth: root.maxAvailableWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
border {
|
||||
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,54 @@ import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
QQC2.Popup {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property var chatDocumentHandler
|
||||
property alias model: completions.model
|
||||
|
||||
visible: completions.count > 0
|
||||
required property LibNeoChat.ChatKeyHelper keyHelper
|
||||
|
||||
onVisibleChanged: if (visible) {
|
||||
root.open();
|
||||
Connections {
|
||||
target: keyHelper
|
||||
|
||||
function onUnhandledUp(isCompleting: bool): void {
|
||||
if (!isCompleting) {
|
||||
return;
|
||||
}
|
||||
root.decrementIndex();
|
||||
}
|
||||
|
||||
function onUnhandledDown(isCompleting: bool): void {
|
||||
if (!isCompleting) {
|
||||
return;
|
||||
}
|
||||
root.incrementIndex();
|
||||
}
|
||||
|
||||
function onUnhandledTab(isCompleting: bool): void {
|
||||
if (!isCompleting) {
|
||||
return;
|
||||
}
|
||||
root.completeCurrent();
|
||||
}
|
||||
|
||||
function onUnhandledReturn(isCompleting: bool): void {
|
||||
if (!isCompleting) {
|
||||
return;
|
||||
}
|
||||
root.completeCurrent();
|
||||
}
|
||||
|
||||
function onCloseCompletion(): void {
|
||||
root.close();
|
||||
root.model.ignoreCurrentCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
||||
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
||||
}
|
||||
x: model.textItem.textItem.cursorRectangle.x - Kirigami.Units.largeSpacing
|
||||
y: model.textItem.textItem.cursorRectangle.y - implicitHeight - Kirigami.Units.smallSpacing
|
||||
|
||||
function incrementIndex() {
|
||||
completions.incrementCurrentIndex();
|
||||
@@ -38,8 +68,8 @@ QQC2.Popup {
|
||||
completions.decrementCurrentIndex();
|
||||
}
|
||||
|
||||
function complete() {
|
||||
root.chatDocumentHandler.complete(completions.currentIndex);
|
||||
function completeCurrent() {
|
||||
model.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
|
||||
}
|
||||
|
||||
leftPadding: 0
|
||||
@@ -49,61 +79,57 @@ QQC2.Popup {
|
||||
|
||||
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
|
||||
contentItem: QQC2.ScrollView {
|
||||
contentWidth: Kirigami.Units.gridUnit * 20
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
ListView {
|
||||
id: completions
|
||||
currentIndex: 0
|
||||
keyNavigationWraps: true
|
||||
highlightMoveDuration: 100
|
||||
onCountChanged: currentIndex = 0
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: completionDelegate
|
||||
|
||||
ListView {
|
||||
id: completions
|
||||
required property int index
|
||||
required property string displayName
|
||||
required property string subtitle
|
||||
required property string iconName
|
||||
required property string replacedText
|
||||
required property url hRef
|
||||
|
||||
model: root.chatDocumentHandler.completionModel
|
||||
currentIndex: 0
|
||||
keyNavigationWraps: true
|
||||
highlightMoveDuration: 100
|
||||
onCountChanged: currentIndex = 0
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: completionDelegate
|
||||
text: displayName
|
||||
|
||||
required property int index
|
||||
required property string displayName
|
||||
required property string subtitle
|
||||
required property string iconName
|
||||
|
||||
text: displayName
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
visible: completionDelegate.iconName !== "invalid"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
||||
name: completionDelegate.text
|
||||
}
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: completionDelegate
|
||||
labelItem.textFormat: Text.PlainText
|
||||
labelItem.clip: true // Intentional to limit insane Unicode in display names
|
||||
subtitle: completionDelegate.subtitle ?? ""
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
visible: completionDelegate.iconName !== "invalid"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
||||
name: completionDelegate.text
|
||||
}
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: completionDelegate
|
||||
labelItem.textFormat: Text.PlainText
|
||||
labelItem.clip: true // Intentional to limit insane Unicode in display names
|
||||
subtitle: completionDelegate.subtitle ?? ""
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
|
||||
}
|
||||
onClicked: root.model.insertCompletion(replacedText, hRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
border {
|
||||
width: 1
|
||||
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
src/chatbar/LinkDialog.qml
Normal file
33
src/chatbar/LinkDialog.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// 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
|
||||
|
||||
FormCard.FormCardDialog {
|
||||
id: root
|
||||
|
||||
property alias linkText: linkTextField.text
|
||||
property alias linkUrl: linkUrlField.text
|
||||
|
||||
title: i18nc("@title:window", "Insert Link")
|
||||
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: linkTextField
|
||||
|
||||
label: i18nc("@label:textbox", "Link Text:")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: linkUrlField
|
||||
|
||||
label: i18nc("@label:textbox", "Link URL:")
|
||||
}
|
||||
}
|
||||
@@ -19,22 +19,12 @@ Kirigami.Dialog {
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
|
||||
text: i18nc("@action:button", "Send")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title: create new poll in the room", "Create Poll")
|
||||
showCloseButton: false
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
onAccepted: root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
@@ -148,4 +138,16 @@ Kirigami.Dialog {
|
||||
onClicked: optionModel.append({optionText: ""})
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
|
||||
text: i18nc("@action:button", "Send")
|
||||
icon.name: "document-send"
|
||||
|
||||
onClicked: root.accept()
|
||||
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
}
|
||||
385
src/chatbar/RichEditBar.qml
Normal file
385
src/chatbar/RichEditBar.qml
Normal file
@@ -0,0 +1,385 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
import org.kde.neochat.messagecontent as MessageContent
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property LibNeoChat.NeoChatRoom room
|
||||
|
||||
required property MessageContent.ChatBarMessageContentModel contentModel
|
||||
|
||||
required property real maxAvailableWidth
|
||||
|
||||
readonly property real uncompressedImplicitWidth: boldButton.implicitWidth +
|
||||
italicButton.implicitWidth +
|
||||
extraTextFormatRow.implicitWidth +
|
||||
listRow.implicitWidth +
|
||||
styleButton.implicitWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
root.spacing * 7 +
|
||||
Kirigami.Units.gridUnit
|
||||
|
||||
readonly property real listCompressedImplicitWidth: boldButton.implicitWidth +
|
||||
italicButton.implicitWidth +
|
||||
extraTextFormatRow.implicitWidth +
|
||||
compressedListButton.implicitWidth +
|
||||
styleButton.uncompressedWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
root.spacing * 7 +
|
||||
Kirigami.Units.gridUnit
|
||||
|
||||
readonly property real extraTextCompressedImplicitWidth: boldButton.implicitWidth +
|
||||
italicButton.implicitWidth +
|
||||
compressedExtraTextFormatButton.implicitWidth +
|
||||
compressedListButton.implicitWidth +
|
||||
styleButton.uncompressedWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
root.spacing * 7 +
|
||||
Kirigami.Units.gridUnit
|
||||
|
||||
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
|
||||
textItem: root.contentModel.focusedTextItem
|
||||
inQuote: root.contentModel.focusType == LibNeoChat.MessageComponentType.Quote
|
||||
hasAttachment: root.contentModel.hasAttachment
|
||||
}
|
||||
|
||||
signal clicked
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: boldButton
|
||||
Shortcut {
|
||||
sequence: "Ctrl+B"
|
||||
onActivated: boldButton.clicked()
|
||||
}
|
||||
icon.name: "format-text-bold"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Bold")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.bold
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
|
||||
root.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: italicButton
|
||||
Shortcut {
|
||||
sequence: "Ctrl+I"
|
||||
onActivated: italicButton.clicked()
|
||||
}
|
||||
icon.name: "format-text-italic"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Italic")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.italic
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
|
||||
root.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
RowLayout {
|
||||
id: extraTextFormatRow
|
||||
visible: root.maxAvailableWidth > root.listCompressedImplicitWidth
|
||||
QQC2.ToolButton {
|
||||
id: underlineButton
|
||||
Shortcut {
|
||||
sequence: "Ctrl+U"
|
||||
onActivated: underlineButton.clicked()
|
||||
}
|
||||
icon.name: "format-text-underline"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Underline")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.underline
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-strikethrough"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Strikethrough")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.strikethrough
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
||||
root.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: compressedExtraTextFormatButton
|
||||
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
|
||||
icon.name: "dialog-text-and-font"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Format Text")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
onClicked: {
|
||||
let dialog = compressedTextFormatMenu.createObject(compressedExtraTextFormatButton) as QQC2.Menu
|
||||
dialog.onClosed.connect(() => {
|
||||
compressedExtraTextFormatButton.checked = false;
|
||||
});
|
||||
dialog.open();
|
||||
compressedExtraTextFormatButton.checked = true;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: compressedTextFormatMenu
|
||||
QQC2.Menu {
|
||||
y: -implicitHeight
|
||||
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-underline"
|
||||
text: i18nc("@action:button", "Underline")
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.underline
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-strikethrough"
|
||||
text: i18nc("@action:button", "Strikethrough")
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.strikethrough
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
StyleButton {
|
||||
id: styleButton
|
||||
Layout.minimumWidth: compressed ? -1 : Kirigami.Units.gridUnit * 10 + Kirigami.Units.largeSpacing * 2
|
||||
|
||||
icon.name: "typewriter"
|
||||
text: i18nc("@action:button", "Text Style")
|
||||
style: root.chatButtonHelper.currentStyle
|
||||
inQuote: root.contentModel.focusType == LibNeoChat.MessageComponentType.Quote
|
||||
compressed: root.maxAvailableWidth < root.extraTextCompressedImplicitWidth
|
||||
enabled: root.chatButtonHelper.styleFormatEnabled
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: styleMenu.visible
|
||||
onClicked: {
|
||||
if (styleMenu.visible) {
|
||||
styleMenu.close();
|
||||
return;
|
||||
}
|
||||
open = true;
|
||||
styleMenu.open();
|
||||
}
|
||||
|
||||
StylePicker {
|
||||
id: styleMenu
|
||||
width: styleButton.compressed ? implicitWidth : styleButton.width
|
||||
chatContentModel: root.contentModel
|
||||
chatButtonHelper: root.chatButtonHelper
|
||||
inQuote: root.contentModel.focusType == LibNeoChat.MessageComponentType.Quote
|
||||
|
||||
onClosed: {
|
||||
root.clicked()
|
||||
styleButton.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: linkButton
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
icon.name: "insert-link-symbolic"
|
||||
text: root.chatButtonHelper.currentLinkUrl.length > 0 ? i18nc("@action:button", "Edit link") : i18nc("@action:button", "Insert link")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
|
||||
linkText: root.chatButtonHelper.currentLinkText,
|
||||
linkUrl: root.chatButtonHelper.currentLinkUrl
|
||||
})
|
||||
dialog.onAccepted.connect(() => {
|
||||
root.chatButtonHelper.updateLink(dialog.linkUrl, dialog.linkText)
|
||||
root.clicked();
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
RowLayout {
|
||||
id: listRow
|
||||
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-list-unordered"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Unordered List")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.unorderedList
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-list-ordered"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
text: i18nc("@action:button", "Ordered List")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.chatButtonHelper.orderedList
|
||||
onClicked: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: indentAction
|
||||
icon.name: "format-indent-more"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListMore
|
||||
text: i18nc("@action:button", "Increase List Level")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.chatButtonHelper.indentListMore();
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: dedentAction
|
||||
icon.name: "format-indent-less"
|
||||
enabled: root.chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListLess
|
||||
text: i18nc("@action:button", "Decrease List Level")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.chatButtonHelper.indentListLess();
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: compressedListButton
|
||||
enabled: root.chatButtonHelper.richFormatEnabled
|
||||
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
|
||||
icon.name: "format-list-unordered"
|
||||
text: i18nc("@action:button", "List Style")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: compressedListMenu.visible
|
||||
onClicked: {
|
||||
compressedListMenu.open()
|
||||
}
|
||||
|
||||
QQC2.Menu {
|
||||
id: compressedListMenu
|
||||
y: -implicitHeight
|
||||
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-list-unordered"
|
||||
text: i18nc("@action:button", "Unordered List")
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-list-ordered"
|
||||
text: i18nc("@action:button", "Ordered List")
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-indent-more"
|
||||
text: i18nc("@action:button", "Increase List Level")
|
||||
enabled: root.chatButtonHelper.canIndentListMore
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.indentListMore();
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-indent-less"
|
||||
text: i18nc("@action:button", "Decrease List Level")
|
||||
enabled: root.chatButtonHelper.canIndentListLess
|
||||
onTriggered: {
|
||||
root.chatButtonHelper.indentListLess();
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: linkDialog
|
||||
LinkDialog {}
|
||||
}
|
||||
|
||||
}
|
||||
235
src/chatbar/SendBar.qml
Normal file
235
src/chatbar/SendBar.qml
Normal file
@@ -0,0 +1,235 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
import org.kde.neochat.messagecontent as MessageContent
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property LibNeoChat.NeoChatRoom room
|
||||
|
||||
property LibNeoChat.ChatBarCache chatBarCache
|
||||
|
||||
required property MessageContent.ChatBarMessageContentModel contentModel
|
||||
|
||||
required property real maxAvailableWidth
|
||||
|
||||
readonly property real overflowWidth: Kirigami.Units.gridUnit * 50
|
||||
|
||||
function openLocationChooser(): void {
|
||||
Qt.createComponent('org.kde.neochat.chatbar', 'LocationChooser').createObject(QQC2.ApplicationWindow.overlay, {
|
||||
room: root.room
|
||||
}).open();
|
||||
}
|
||||
|
||||
function openNewPollDialog(): void {
|
||||
Qt.createComponent('org.kde.neochat.chatbar', 'NewPollDialog').createObject(QQC2.Overlay.overlay, {
|
||||
room: root.room
|
||||
}).open();
|
||||
}
|
||||
|
||||
function addAttachment(): void {
|
||||
if (!root.contentModel.hasRichFormatting) {
|
||||
fileDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
let warningDialog = Qt.createComponent('org.kde.kirigami', 'PromptDialog').createObject(QQC2.Overlay.overlay, {
|
||||
dialogType: Kirigami.PromptDialog.Warning,
|
||||
title: attachmentButton.text,
|
||||
subtitle: i18nc("@Warning: that any rich text in the chat bar will be switched for the plain text equivalent.", "Attachments can only have plain text captions, all rich formatting will be removed"),
|
||||
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||
});
|
||||
warningDialog.onAccepted.connect(() => {
|
||||
attachmentButton.fileDialog();
|
||||
});
|
||||
warningDialog.open();
|
||||
}
|
||||
|
||||
function fileDialog(): void {
|
||||
let dialog = Qt.createComponent('org.kde.neochat.libneochat', 'OpenFileDialog').createObject(QQC2.Overlay.overlay, {
|
||||
parentWindow: Window.window,
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
});
|
||||
dialog.chosen.connect(path => root.contentModel.addAttachment(path));
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
function openVoiceDialog(): void {
|
||||
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'VoiceMessageDialog').createObject(root, {
|
||||
room: root.currentRoom
|
||||
}) as VoiceMessageDialog;
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: compressedExtraSendButton
|
||||
property QQC2.Menu overflowMenu
|
||||
|
||||
visible: root.maxAvailableWidth < root.overflowWidth && (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room
|
||||
icon.name: "list-add-symbolic"
|
||||
text: i18nc("@action:button", "Add to message")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: overflowMenu !== null
|
||||
|
||||
Accessible.role: Accessible.ButtonMenu
|
||||
|
||||
onClicked: {
|
||||
if (!checked) {
|
||||
if (overflowMenu) {
|
||||
overflowMenu.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
overflowMenu = compressedExtraSendMenu.createObject(compressedExtraSendButton)
|
||||
overflowMenu.onClosed.connect(() => {
|
||||
overflowMenu = null;
|
||||
});
|
||||
overflowMenu.open();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: compressedExtraSendMenu
|
||||
QQC2.Menu {
|
||||
y: -implicitHeight
|
||||
|
||||
QQC2.MenuItem {
|
||||
visible: !root.contentModel.hasAttachment
|
||||
icon.name: "mail-attachment"
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
onTriggered: root.addAttachment()
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "globe"
|
||||
text: i18nc("@action:button", "Send a Location")
|
||||
onTriggered: root.openLocationChooser()
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "amarok_playcount"
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
onTriggered: root.openNewPollDialog();
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "microphone"
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
onTriggered: root.openVoiceDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: attachmentButton
|
||||
visible: !root.contentModel.hasAttachment &&
|
||||
((root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room || (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Thread) &&
|
||||
root.maxAvailableWidth >= root.overflowWidth
|
||||
icon.name: "mail-attachment"
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.addAttachment()
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: mapButton
|
||||
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room && root.maxAvailableWidth >= root.overflowWidth
|
||||
icon.name: "globe"
|
||||
text: i18nc("@action:button", "Send a Location")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.openLocationChooser();
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: pollButton
|
||||
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room && root.maxAvailableWidth >= root.overflowWidth
|
||||
icon.name: "amarok_playcount"
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.openNewPollDialog();
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room && root.maxAvailableWidth >= root.overflowWidth
|
||||
icon.name: "microphone"
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.openVoiceDialog();
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "edit-select-text-symbolic"
|
||||
text: NeoChatConfig.sendMessageWith === 1 ? i18nc("@action:button", "Hide Rich Text Controls") : i18nc("@action:button", "Show Rich Text Controls")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: NeoChatConfig.sendMessageWith === 1
|
||||
onClicked: NeoChatConfig.sendMessageWith = checked
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: sendButton
|
||||
icon.name: "document-send"
|
||||
text: i18nc("@action:button", "Send message")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
enabled: root.contentModel.hasAnyContent
|
||||
|
||||
onClicked: root.contentModel.postMessage();
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: cancelButton
|
||||
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Edit
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Cancel")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: root.room.cacheForType(contentModel.type).clearRelations()
|
||||
|
||||
Kirigami.Action {
|
||||
shortcut: "Escape"
|
||||
onTriggered: cancelButton.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
79
src/chatbar/StyleButton.qml
Normal file
79
src/chatbar/StyleButton.qml
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
QQC2.AbstractButton {
|
||||
id: root
|
||||
|
||||
required property int style
|
||||
|
||||
required property bool inQuote
|
||||
|
||||
property bool open: false
|
||||
|
||||
property bool compressed: false
|
||||
|
||||
readonly property real uncompressedWidth: styleDelegate.implicitWidth + arrowIcon.implicitWidth + 1 + contentRow.spacing * 2 + padding * 2
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
icon {
|
||||
width: Kirigami.Units.iconSizes.smallMedium
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentRow
|
||||
StyleDelegate {
|
||||
id: styleDelegate
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: !root.compressed
|
||||
style: root.style
|
||||
inQuote: root.inQuote
|
||||
sizeText: false
|
||||
|
||||
onPressed: root.clicked()
|
||||
}
|
||||
Kirigami.Icon {
|
||||
id: styleIcon
|
||||
visible: root.compressed
|
||||
source: root.icon.name
|
||||
implicitWidth: root.icon.width
|
||||
implicitHeight: root.icon.height
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
Kirigami.Icon {
|
||||
id: arrowIcon
|
||||
source: root.open ? "arrow-down" : "arrow-up"
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border {
|
||||
width: root.hovered || root.open ? 1 : 0
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/chatbar/StyleDelegate.qml
Normal file
76
src/chatbar/StyleDelegate.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
QQC2.TextArea {
|
||||
id: root
|
||||
|
||||
required property int style
|
||||
|
||||
required property bool inQuote
|
||||
|
||||
property bool highlight: false
|
||||
|
||||
property bool sizeText: true
|
||||
|
||||
leftPadding: lineRow.visible ? lineRow.width + lineRow.anchors.leftMargin + Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: false
|
||||
|
||||
RowLayout {
|
||||
id: lineRow
|
||||
anchors {
|
||||
top: root.top
|
||||
bottom: root.bottom
|
||||
left: root.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
visible: root.style === LibNeoChat.RichFormat.Code
|
||||
|
||||
QQC2.Label {
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: "1"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
font.family: "monospace"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
StyleDelegateHelper {
|
||||
textItem: root
|
||||
inQuote: root.inQuote
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: root.style === LibNeoChat.RichFormat.Quote || (root.inQuote && !(root.style == LibNeoChat.RichFormat.Paragraph || root.style == LibNeoChat.RichFormat.Code)) ? Kirigami.Theme.Window : Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border {
|
||||
width: 1
|
||||
color: root.highlight ?
|
||||
Kirigami.Theme.highlightColor :
|
||||
Kirigami.ColorUtils.linearInterpolation(
|
||||
Kirigami.Theme.backgroundColor,
|
||||
Kirigami.Theme.textColor,
|
||||
Kirigami.Theme.frameContrast
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user