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.yml
|
||||||
- /gitlab-templates/linux-qt6-next.yml
|
- /gitlab-templates/linux-qt6-next.yml
|
||||||
- /gitlab-templates/windows-qt6.yml
|
- /gitlab-templates/windows-qt6.yml
|
||||||
- /gitlab-templates/freebsd-qt6.yml
|
# - /gitlab-templates/freebsd-qt6.yml
|
||||||
- /gitlab-templates/flatpak.yml
|
- /gitlab-templates/flatpak.yml
|
||||||
- /gitlab-templates/snap-snapcraft-lxd.yml
|
- /gitlab-templates/snap-snapcraft-lxd.yml
|
||||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ Dependencies:
|
|||||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||||
'frameworks/kio': '@latest-kf6'
|
'frameworks/kio': '@latest-kf6'
|
||||||
'frameworks/kwindowsystem': '@latest-kf6'
|
'frameworks/kwindowsystem': '@latest-kf6'
|
||||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
|
||||||
- 'on': ['Linux', 'FreeBSD']
|
- 'on': ['Linux', 'FreeBSD']
|
||||||
'require':
|
'require':
|
||||||
'frameworks/kdbusaddons': '@latest-kf6'
|
'frameworks/kdbusaddons': '@latest-kf6'
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ if(ANDROID)
|
|||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
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)
|
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
|
||||||
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
|
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
|
||||||
TYPE RUNTIME
|
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)
|
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(QuotientQt6 0.9.3)
|
find_package(QuotientQt6 0.9.5)
|
||||||
set_package_properties(QuotientQt6 PROPERTIES
|
set_package_properties(QuotientQt6 PROPERTIES
|
||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
DESCRIPTION "Qt wrapper around Matrix API"
|
DESCRIPTION "Qt wrapper around Matrix API"
|
||||||
@@ -178,7 +178,7 @@ add_definitions(-DQT_NO_FOREACH)
|
|||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
if (BUILD_TESTING)
|
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(autotests)
|
||||||
# add_subdirectory(appiumtests)
|
# add_subdirectory(appiumtests)
|
||||||
if (NOT ANDROID)
|
if (NOT ANDROID)
|
||||||
|
|||||||
@@ -45,12 +45,6 @@ ecm_add_test(
|
|||||||
TEST_NAME chatbarcachetest
|
TEST_NAME chatbarcachetest
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_add_test(
|
|
||||||
chatdocumenthandlertest.cpp
|
|
||||||
LINK_LIBRARIES neochat Qt::Test
|
|
||||||
TEST_NAME chatdocumenthandlertest
|
|
||||||
)
|
|
||||||
|
|
||||||
ecm_add_test(
|
ecm_add_test(
|
||||||
timelinemessagemodeltest.cpp
|
timelinemessagemodeltest.cpp
|
||||||
LINK_LIBRARIES neochat Qt::Test
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
@@ -110,3 +104,45 @@ ecm_add_test(
|
|||||||
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
|
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
|
||||||
TEST_NAME modeltest
|
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 <QVariantList>
|
||||||
|
|
||||||
#include "accountmanager.h"
|
#include "accountmanager.h"
|
||||||
|
#include "blockcache.h"
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
|
#include "enums/messagecomponenttype.h"
|
||||||
#include "models/actionsmodel.h"
|
#include "models/actionsmodel.h"
|
||||||
|
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
@@ -88,8 +90,8 @@ void ActionsTest::testActions()
|
|||||||
QFETCH(std::optional<QString>, resultText);
|
QFETCH(std::optional<QString>, resultText);
|
||||||
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
|
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
|
||||||
|
|
||||||
auto cache = new ChatBarCache(this);
|
auto cache = new ChatBarCache(room);
|
||||||
cache->setText(command);
|
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)};
|
||||||
auto result = ActionsModel::handleAction(room, cache);
|
auto result = ActionsModel::handleAction(room, cache);
|
||||||
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
|
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
|
||||||
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(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 <KLocalizedString>
|
||||||
|
|
||||||
#include "accountmanager.h"
|
#include "accountmanager.h"
|
||||||
|
#include "blockcache.h"
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
@@ -36,8 +37,6 @@ private Q_SLOTS:
|
|||||||
void initTestCase();
|
void initTestCase();
|
||||||
|
|
||||||
void empty();
|
void empty();
|
||||||
void noRoom();
|
|
||||||
void badParent();
|
|
||||||
void reply();
|
void reply();
|
||||||
void replyMissingUser();
|
void replyMissingUser();
|
||||||
void edit();
|
void edit();
|
||||||
@@ -77,7 +76,7 @@ void ChatBarCacheTest::empty()
|
|||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->text(), QString());
|
QCOMPARE(chatBarCache->cache().toString(), QString());
|
||||||
QCOMPARE(chatBarCache->isReplying(), false);
|
QCOMPARE(chatBarCache->isReplying(), false);
|
||||||
QCOMPARE(chatBarCache->replyId(), QString());
|
QCOMPARE(chatBarCache->replyId(), QString());
|
||||||
QCOMPARE(chatBarCache->isEditing(), false);
|
QCOMPARE(chatBarCache->isEditing(), false);
|
||||||
@@ -87,47 +86,14 @@ void ChatBarCacheTest::empty()
|
|||||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
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()
|
void ChatBarCacheTest::reply()
|
||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
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->setAttachmentPath(u"some/path"_s);
|
||||||
chatBarCache->setReplyId(eventId);
|
chatBarCache->setReplyId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||||
QCOMPARE(chatBarCache->isReplying(), true);
|
QCOMPARE(chatBarCache->isReplying(), true);
|
||||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||||
QCOMPARE(chatBarCache->isEditing(), false);
|
QCOMPARE(chatBarCache->isEditing(), false);
|
||||||
@@ -141,11 +107,11 @@ void ChatBarCacheTest::reply()
|
|||||||
void ChatBarCacheTest::replyMissingUser()
|
void ChatBarCacheTest::replyMissingUser()
|
||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
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->setAttachmentPath(u"some/path"_s);
|
||||||
chatBarCache->setReplyId(eventId);
|
chatBarCache->setReplyId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||||
QCOMPARE(chatBarCache->isReplying(), true);
|
QCOMPARE(chatBarCache->isReplying(), true);
|
||||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||||
QCOMPARE(chatBarCache->isEditing(), false);
|
QCOMPARE(chatBarCache->isEditing(), false);
|
||||||
@@ -172,7 +138,7 @@ void ChatBarCacheTest::edit()
|
|||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
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->setAttachmentPath(u"some/path"_s);
|
||||||
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||||
QCOMPARE(oldEventId, QString());
|
QCOMPARE(oldEventId, QString());
|
||||||
@@ -180,7 +146,7 @@ void ChatBarCacheTest::edit()
|
|||||||
});
|
});
|
||||||
chatBarCache->setEditId(eventId);
|
chatBarCache->setEditId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||||
QCOMPARE(chatBarCache->isReplying(), false);
|
QCOMPARE(chatBarCache->isReplying(), false);
|
||||||
QCOMPARE(chatBarCache->replyId(), QString());
|
QCOMPARE(chatBarCache->replyId(), QString());
|
||||||
QCOMPARE(chatBarCache->isEditing(), true);
|
QCOMPARE(chatBarCache->isEditing(), true);
|
||||||
@@ -193,11 +159,11 @@ void ChatBarCacheTest::edit()
|
|||||||
void ChatBarCacheTest::attachment()
|
void ChatBarCacheTest::attachment()
|
||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
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->setEditId(eventId);
|
||||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
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->isReplying(), false);
|
||||||
QCOMPARE(chatBarCache->replyId(), QString());
|
QCOMPARE(chatBarCache->replyId(), QString());
|
||||||
QCOMPARE(chatBarCache->isEditing(), false);
|
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/livelocationsmodel.h"
|
||||||
#include "models/locationsmodel.h"
|
#include "models/locationsmodel.h"
|
||||||
#include "models/messagecontentfiltermodel.h"
|
#include "models/messagecontentfiltermodel.h"
|
||||||
|
#include "models/messagecontentmodel.h"
|
||||||
#include "models/notificationsmodel.h"
|
#include "models/notificationsmodel.h"
|
||||||
#include "models/permissionsmodel.h"
|
#include "models/permissionsmodel.h"
|
||||||
#include "models/pinnedmessagemodel.h"
|
#include "models/pinnedmessagemodel.h"
|
||||||
@@ -179,7 +180,7 @@ void ModelTest::testRoomTreeModel()
|
|||||||
|
|
||||||
void ModelTest::testMessageContentModel()
|
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());
|
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
@@ -193,21 +194,23 @@ void ModelTest::testEventMessageContentModel()
|
|||||||
|
|
||||||
void ModelTest::testThreadModel()
|
void ModelTest::testThreadModel()
|
||||||
{
|
{
|
||||||
auto model = new ThreadModel(eventId, room);
|
auto model = std::make_unique<ThreadModel>(eventId, room);
|
||||||
auto tester = new QAbstractItemModelTester(model, model);
|
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModelTest::testThreadFetchModel()
|
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);
|
auto tester = new QAbstractItemModelTester(model, model);
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModelTest::testThreadChatBarModel()
|
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);
|
auto tester = new QAbstractItemModelTester(model, model);
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
@@ -397,9 +400,7 @@ void ModelTest::testCompletionModel()
|
|||||||
auto model = new CompletionModel(this);
|
auto model = new CompletionModel(this);
|
||||||
auto tester = new QAbstractItemModelTester(model, model);
|
auto tester = new QAbstractItemModelTester(model, model);
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
model->setRoom(room);
|
|
||||||
model->setAutoCompletionType(CompletionModel::Room);
|
model->setAutoCompletionType(CompletionModel::Room);
|
||||||
model->setText(u"foo"_s, u"#foo"_s);
|
|
||||||
auto roomListModel = new RoomListModel(this);
|
auto roomListModel = new RoomListModel(this);
|
||||||
roomListModel->setConnection(connection);
|
roomListModel->setConnection(connection);
|
||||||
model->setRoomListModel(roomListModel);
|
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);
|
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
|
||||||
void(key.open(QFile::ReadOnly));
|
void(key.open(QFile::ReadOnly));
|
||||||
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
|
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);
|
m_sslServer.setSslConfiguration(config);
|
||||||
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
|
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
|
||||||
qFatal() << "Server failed to listen on a port.";
|
qFatal() << "Server failed to listen on a port.";
|
||||||
@@ -227,7 +227,8 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
QJsonObject joinRooms;
|
QJsonObject joinRooms;
|
||||||
auto token = request.query().queryItemValue(u"since"_s).toInt();
|
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) {
|
for (const auto &newRoom : change.newRooms) {
|
||||||
QJsonArray stateEvents;
|
QJsonArray stateEvents;
|
||||||
stateEvents += QJsonObject{
|
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) {
|
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.
|
// 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();
|
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) {
|
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.
|
// 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();
|
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) {
|
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.
|
// 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();
|
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) {
|
for (const auto &state : change.stateEvents) {
|
||||||
const auto &roomId = state.fullJson[u"room_id"_s].toString();
|
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.
|
// 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) {
|
for (const auto &event : change.events) {
|
||||||
// TODO the room might be in a different join state.
|
// 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();
|
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();
|
instance.toggleWindow();
|
||||||
QCOMPARE(window.windowState(), Qt::WindowNoState);
|
QCOMPARE(window.windowState(), Qt::WindowNoState);
|
||||||
QCOMPARE(window.isVisible(), false);
|
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)
|
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/AttachmentPane.qml
|
||||||
qml/QuickFormatBar.qml
|
qml/QuickFormatBar.qml
|
||||||
qml/UserDetailDialog.qml
|
qml/UserDetailDialog.qml
|
||||||
qml/OpenFileDialog.qml
|
|
||||||
qml/KeyVerificationDialog.qml
|
qml/KeyVerificationDialog.qml
|
||||||
qml/ConfirmLogoutDialog.qml
|
qml/ConfirmLogoutDialog.qml
|
||||||
qml/VerificationMessage.qml
|
qml/VerificationMessage.qml
|
||||||
@@ -79,7 +78,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/EmojiSas.qml
|
qml/EmojiSas.qml
|
||||||
qml/VerificationCanceled.qml
|
qml/VerificationCanceled.qml
|
||||||
qml/MessageSourceSheet.qml
|
qml/MessageSourceSheet.qml
|
||||||
qml/LocationChooser.qml
|
|
||||||
qml/InvitationView.qml
|
qml/InvitationView.qml
|
||||||
qml/AvatarTabButton.qml
|
qml/AvatarTabButton.qml
|
||||||
qml/OsmLocationPlugin.qml
|
qml/OsmLocationPlugin.qml
|
||||||
@@ -105,7 +103,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/HoverLinkIndicator.qml
|
qml/HoverLinkIndicator.qml
|
||||||
qml/AvatarNotification.qml
|
qml/AvatarNotification.qml
|
||||||
qml/ReasonDialog.qml
|
qml/ReasonDialog.qml
|
||||||
qml/NewPollDialog.qml
|
|
||||||
qml/UserMenu.qml
|
qml/UserMenu.qml
|
||||||
qml/MeetingDialog.qml
|
qml/MeetingDialog.qml
|
||||||
qml/SeenByDialog.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})
|
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
|
||||||
|
|
||||||
if(NOT ANDROID)
|
if(NOT ANDROID)
|
||||||
if (NOT WIN32 AND NOT APPLE)
|
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||||
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_link_libraries(neochat PUBLIC KF6::WindowSystem)
|
target_link_libraries(neochat PUBLIC KF6::WindowSystem)
|
||||||
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -29,10 +29,8 @@
|
|||||||
#include "proxycontroller.h"
|
#include "proxycontroller.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
|
||||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
#if !defined(Q_OS_ANDROID)
|
||||||
#include "trayicon.h"
|
#include "trayicon.h"
|
||||||
#elif !defined(Q_OS_ANDROID)
|
|
||||||
#include "trayicon_sni.h"
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_KUNIFIEDPUSH
|
#ifdef HAVE_KUNIFIEDPUSH
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ QHash<int, QByteArray> CommonRoomsModel::roleNames() const
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CommonRoomsModel::loading() const
|
||||||
|
{
|
||||||
|
return m_loading;
|
||||||
|
}
|
||||||
|
|
||||||
void CommonRoomsModel::reload()
|
void CommonRoomsModel::reload()
|
||||||
{
|
{
|
||||||
if (!m_connection || m_userId.isEmpty()) {
|
if (!m_connection || m_userId.isEmpty()) {
|
||||||
@@ -89,15 +94,26 @@ void CommonRoomsModel::reload()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
|
m_loading = true;
|
||||||
const auto &replyData = job->jsonData();
|
Q_EMIT loadingChanged();
|
||||||
beginResetModel();
|
|
||||||
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId)
|
||||||
m_commonRooms.push_back(roomId.toString());
|
.then([this](const auto job) {
|
||||||
}
|
const auto &replyData = job->jsonData();
|
||||||
endResetModel();
|
beginResetModel();
|
||||||
Q_EMIT countChanged();
|
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"
|
#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(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
|
||||||
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
|
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
|
||||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||||
|
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Roles {
|
enum Roles {
|
||||||
@@ -43,10 +44,13 @@ public:
|
|||||||
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
bool loading() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void connectionChanged();
|
void connectionChanged();
|
||||||
void userIdChanged();
|
void userIdChanged();
|
||||||
void countChanged();
|
void countChanged();
|
||||||
|
void loadingChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void reload();
|
void reload();
|
||||||
@@ -54,4 +58,5 @@ private:
|
|||||||
QPointer<NeoChatConnection> m_connection;
|
QPointer<NeoChatConnection> m_connection;
|
||||||
QString m_userId;
|
QString m_userId;
|
||||||
QList<QString> m_commonRooms;
|
QList<QString> m_commonRooms;
|
||||||
|
bool m_loading = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -207,10 +207,6 @@
|
|||||||
</entry>
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
<group name="FeatureFlags">
|
<group name="FeatureFlags">
|
||||||
<entry name="Threads" type="bool">
|
|
||||||
<label>Enable threads</label>
|
|
||||||
<default>false</default>
|
|
||||||
</entry>
|
|
||||||
<entry name="Phone3PId" type="bool">
|
<entry name="Phone3PId" type="bool">
|
||||||
<label>Enable add phone numbers as 3PIDs</label>
|
<label>Enable add phone numbers as 3PIDs</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Delegates.RoundedItemDelegate {
|
|||||||
signal contextMenuRequested
|
signal contextMenuRequested
|
||||||
signal selected
|
signal selected
|
||||||
|
|
||||||
|
activeFocusOnTab: true
|
||||||
padding: Kirigami.Units.largeSpacing
|
padding: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
|
|||||||
@@ -8,45 +8,26 @@ import org.kde.kirigami as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
|
||||||
import Quotient
|
|
||||||
|
|
||||||
Kirigami.PromptDialog {
|
Kirigami.PromptDialog {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property NeoChatRoom room
|
required property NeoChatRoom room
|
||||||
|
|
||||||
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
|
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
|
||||||
subtitle: {
|
subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
|
||||||
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 "";
|
|
||||||
}
|
|
||||||
dialogType: Kirigami.PromptDialog.Warning
|
dialogType: Kirigami.PromptDialog.Warning
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
onRejected: {
|
onAccepted: root.room.forget()
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
footer: QQC2.DialogButtonBox {
|
footer: QQC2.DialogButtonBox {
|
||||||
standardButtons: QQC2.Dialog.Cancel
|
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
text: i18nc("@action:button", "Leave Room")
|
text: i18nc("@action:button Leave this room/space", "Leave")
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
|
||||||
icon.name: "arrow-left-symbolic"
|
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")
|
title: i18nc("@title:dialog", "Sign out")
|
||||||
subtitle: i18n("Are you sure you want to sign out?")
|
subtitle: i18n("Are you sure you want to sign out?")
|
||||||
dialogType: Kirigami.PromptDialog.Warning
|
dialogType: Kirigami.PromptDialog.Warning
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
onRejected: {
|
onAccepted: root.connection.logout(true)
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
footer: QQC2.DialogButtonBox {
|
footer: QQC2.DialogButtonBox {
|
||||||
standardButtons: QQC2.Dialog.Cancel
|
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
text: i18nc("@action:button", "Sign out")
|
text: i18nc("@action:button", "Sign out")
|
||||||
|
onClicked: root.accept()
|
||||||
|
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
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
|
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: Qt.openUrlExternally(root.link)
|
||||||
Qt.openUrlExternally(root.link);
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRejected: {
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
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)
|
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||||
|
|
||||||
title: i18nc("@title", "Manually Enter a Room")
|
title: i18nc("@title", "Manually Enter a Room")
|
||||||
|
showCloseButton: false
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
|
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -31,35 +34,26 @@ Kirigami.Dialog {
|
|||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
|
||||||
standardButtons: Kirigami.Dialog.Cancel
|
onAccepted: {
|
||||||
customFooterActions: [
|
// We don't necessarily have all the info so fill out the best we can.
|
||||||
Kirigami.Action {
|
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
||||||
enabled: roomIdAliasText.isValidText
|
let displayName = "";
|
||||||
text: i18n("OK")
|
let avatarUrl = "";
|
||||||
icon.name: "dialog-ok"
|
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
||||||
onTriggered: {
|
let topic = "";
|
||||||
// We don't necessarily have all the info so fill out the best we can.
|
let memberCount = -1;
|
||||||
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
let isJoined = false;
|
||||||
let displayName = "";
|
if (roomIdAliasText.room) {
|
||||||
let avatarUrl = "";
|
roomId = roomIdAliasText.room.id;
|
||||||
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
displayName = roomIdAliasText.room.displayName;
|
||||||
let topic = "";
|
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
||||||
let memberCount = -1;
|
alias = roomIdAliasText.room.canonicalAlias;
|
||||||
let isJoined = false;
|
topic = roomIdAliasText.room.topic;
|
||||||
if (roomIdAliasText.room) {
|
memberCount = roomIdAliasText.room.joinedCount;
|
||||||
roomId = roomIdAliasText.room.id;
|
isJoined = true;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||||
|
}
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -110,4 +104,16 @@ Kirigami.Dialog {
|
|||||||
roomIdAliasText.forceActiveFocus();
|
roomIdAliasText.forceActiveFocus();
|
||||||
timer.restart();
|
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)
|
signal userSelected(string userId)
|
||||||
|
|
||||||
title: i18nc("@title", "User ID")
|
title: i18nc("@title", "User ID")
|
||||||
|
showCloseButton: false
|
||||||
|
|
||||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
rightPadding: 0
|
rightPadding: 0
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
standardButtons: Kirigami.Dialog.Cancel
|
onAccepted: root.userSelected(userIdText.text)
|
||||||
customFooterActions: [
|
|
||||||
Kirigami.Action {
|
|
||||||
enabled: userIdText.isValidText
|
|
||||||
text: i18n("OK")
|
|
||||||
icon.name: "dialog-ok"
|
|
||||||
onTriggered: {
|
|
||||||
root.userSelected(userIdText.text)
|
|
||||||
root.accept();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -79,4 +70,16 @@ Kirigami.Dialog {
|
|||||||
userIdText.forceActiveFocus();
|
userIdText.forceActiveFocus();
|
||||||
timer.restart();
|
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
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
@@ -14,11 +15,16 @@ Kirigami.PromptDialog {
|
|||||||
|
|
||||||
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
|
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.")
|
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 {
|
footer: QQC2.DialogButtonBox {
|
||||||
icon.name: "camera-video-symbolic"
|
QQC2.Button {
|
||||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
icon.name: "camera-video-symbolic"
|
||||||
onTriggered: root.accept()
|
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
|
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 {
|
QQC2.Control {
|
||||||
id: pinControl
|
id: pinControl
|
||||||
@@ -361,7 +361,6 @@ Kirigami.Page {
|
|||||||
id: chatBar
|
id: chatBar
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentRoom: root.currentRoom
|
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.
|
// 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()
|
onHeightChanged: root.resetViewSettling()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ FormCard.FormCardPage {
|
|||||||
|
|
||||||
property bool processing: false
|
property bool processing: false
|
||||||
|
|
||||||
title: i18nc("@title:window", "Manage Secret Backup")
|
title: i18nc("@title:window", "Manage Key Storage")
|
||||||
|
|
||||||
topPadding: Kirigami.Units.gridUnit
|
topPadding: Kirigami.Units.gridUnit
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -32,7 +32,7 @@ FormCard.FormCardPage {
|
|||||||
function onKeyBackupError(): void {
|
function onKeyBackupError(): void {
|
||||||
securityKeyField.clear()
|
securityKeyField.clear()
|
||||||
root.processing = false
|
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
|
banner.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +45,20 @@ FormCard.FormCardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
|
title: i18nc("@title", "Unlock using Recovery Key")
|
||||||
}
|
}
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
FormCard.FormTextDelegate {
|
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 {
|
FormCard.FormTextFieldDelegate {
|
||||||
id: securityKeyField
|
id: securityKeyField
|
||||||
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
|
label: i18nc("@label:textbox", "Recovery Key:")
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
}
|
}
|
||||||
FormCard.FormButtonDelegate {
|
FormCard.FormButtonDelegate {
|
||||||
id: uploadSecurityKeyButton
|
id: uploadSecurityKeyButton
|
||||||
text: i18nc("@action:button", "Upload from File")
|
text: i18nc("@action:button", "Upload From File")
|
||||||
icon.name: "cloud-upload"
|
icon.name: "cloud-upload"
|
||||||
enabled: !root.processing
|
enabled: !root.processing
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -83,12 +83,12 @@ FormCard.FormCardPage {
|
|||||||
}
|
}
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
FormCard.FormTextDelegate {
|
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 {
|
FormCard.FormButtonDelegate {
|
||||||
id: unlockCrossSigningButton
|
id: unlockCrossSigningButton
|
||||||
icon.name: "emblem-shared-symbolic"
|
icon.name: "emblem-shared-symbolic"
|
||||||
text: i18nc("@action:button", "Request from other Devices")
|
text: i18nc("@action:button", "Request From Other Devices")
|
||||||
enabled: !root.processing
|
enabled: !root.processing
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.processing = true
|
root.processing = true
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Kirigami.Dialog {
|
|||||||
text: root.shareUrl,
|
text: root.shareUrl,
|
||||||
title: root.displayName,
|
title: root.displayName,
|
||||||
subtitle: root.user.id,
|
subtitle: root.user.id,
|
||||||
avatarColor: root.room?.member(root.user.id).color,
|
avatarColor: root.room?.member(root.user.id).color ?? null,
|
||||||
avatarSource: avatar.source,
|
avatarSource: avatar.source,
|
||||||
}) as QrCodeMaximizeComponent;
|
}) as QrCodeMaximizeComponent;
|
||||||
root.close();
|
root.close();
|
||||||
@@ -401,18 +401,36 @@ Kirigami.Dialog {
|
|||||||
Kirigami.Heading {
|
Kirigami.Heading {
|
||||||
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
|
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
|
||||||
level: 4
|
level: 4
|
||||||
visible: !root.isSelf && root.hasMutualRooms
|
visible: !root.isSelf && root.connection.canCheckMutualRooms
|
||||||
|
|
||||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
visible: !root.isSelf && root.hasMutualRooms
|
visible: !root.isSelf && root.connection.canCheckMutualRooms
|
||||||
|
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
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 {
|
Repeater {
|
||||||
|
id: roomRepeater
|
||||||
|
|
||||||
model: root.limiterModel
|
model: root.limiterModel
|
||||||
|
|
||||||
delegate: KirigamiComponents.AvatarButton {
|
delegate: KirigamiComponents.AvatarButton {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ KirigamiComponents.ConvergentContextMenu {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Kirigami.ApplicationWindow window
|
required property Kirigami.ApplicationWindow window
|
||||||
required property var author
|
required property NeochatRoomMember author
|
||||||
|
|
||||||
headerContentItem: RowLayout {
|
headerContentItem: RowLayout {
|
||||||
id: detailRow
|
id: detailRow
|
||||||
@@ -68,7 +68,7 @@ KirigamiComponents.ConvergentContextMenu {
|
|||||||
text: i18nc("@action:button", "Mention")
|
text: i18nc("@action:button", "Mention")
|
||||||
icon.name: "username-copy-symbolic"
|
icon.name: "username-copy-symbolic"
|
||||||
onTriggered: {
|
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();
|
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]() {
|
connect(NeoChatConfig::self(), &NeoChatConfig::SortOrderChanged, this, [this]() {
|
||||||
m_sortFilterRoomTreeModel->invalidate();
|
m_sortFilterRoomTreeModel->invalidate();
|
||||||
});
|
});
|
||||||
@@ -546,13 +537,14 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
|||||||
LastRoomBlocker blocker(this);
|
LastRoomBlocker blocker(this);
|
||||||
|
|
||||||
// We can't have empty keys in KConfig, so it's stored as "Home":
|
// 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);
|
resolveResource(lastRoom, "no_join"_L1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no last room was opened, go to the space home:
|
// 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);
|
resolveResource(spaceId, "no_join"_L1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ public:
|
|||||||
*
|
*
|
||||||
* @sa Quotient::UriResolverBase::visitResource()
|
* @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;
|
bool hasOpenRoom() const;
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ public:
|
|||||||
* @brief Show a context menu for the given event.
|
* @brief Show a context menu for the given event.
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void
|
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.
|
* @brief Set a URL to be loaded as the initial room.
|
||||||
@@ -316,7 +316,7 @@ Q_SIGNALS:
|
|||||||
const QString &plainText,
|
const QString &plainText,
|
||||||
const QString &richtText,
|
const QString &richtText,
|
||||||
const QString &mimeType,
|
const QString &mimeType,
|
||||||
const FileTransferInfo &progressInfo,
|
const Quotient::FileTransferInfo &progressInfo,
|
||||||
const QString &selectedText,
|
const QString &selectedText,
|
||||||
const QString &hoveredLink);
|
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;
|
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();
|
Q_EMIT windowChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +56,11 @@ void WindowController::showAndRaiseWindow(const QString &startupId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!m_window->isVisible()) {
|
if (!m_window->isVisible()) {
|
||||||
m_window->show();
|
if (m_wasMaximized) {
|
||||||
|
m_window->showMaximized();
|
||||||
|
} else {
|
||||||
|
m_window->show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_WINDOWSYSTEM
|
#ifdef HAVE_WINDOWSYSTEM
|
||||||
|
|||||||
@@ -67,5 +67,6 @@ Q_SIGNALS:
|
|||||||
private:
|
private:
|
||||||
WindowController() = default;
|
WindowController() = default;
|
||||||
|
|
||||||
|
bool m_wasMaximized;
|
||||||
QWindow *m_window = nullptr;
|
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
|
URI org.kde.neochat.chatbar
|
||||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
|
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
|
||||||
QML_FILES
|
QML_FILES
|
||||||
AttachDialog.qml
|
|
||||||
ChatBar.qml
|
ChatBar.qml
|
||||||
|
ChatBarCore.qml
|
||||||
|
RichEditBar.qml
|
||||||
|
SendBar.qml
|
||||||
CompletionMenu.qml
|
CompletionMenu.qml
|
||||||
EmojiDelegate.qml
|
EmojiDelegate.qml
|
||||||
EmojiGrid.qml
|
EmojiGrid.qml
|
||||||
@@ -15,6 +17,25 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
|||||||
EmojiPicker.qml
|
EmojiPicker.qml
|
||||||
EmojiDialog.qml
|
EmojiDialog.qml
|
||||||
EmojiTonesPicker.qml
|
EmojiTonesPicker.qml
|
||||||
|
StylePicker.qml
|
||||||
|
StyleDelegate.qml
|
||||||
ImageEditorPage.qml
|
ImageEditorPage.qml
|
||||||
VoiceMessageDialog.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 Carl Schwan <carl@carlschwan.de>
|
||||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
// 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
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
@@ -25,27 +26,26 @@ import org.kde.neochat.libneochat as LibNeoChat
|
|||||||
*
|
*
|
||||||
* @sa ChatBar
|
* @sa ChatBar
|
||||||
*/
|
*/
|
||||||
QQC2.Control {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
required property NeoChatRoom currentRoom
|
required property LibNeoChat.NeoChatRoom currentRoom
|
||||||
|
|
||||||
required property NeoChatConnection connection
|
|
||||||
|
|
||||||
onActiveFocusChanged: textField.forceActiveFocus()
|
|
||||||
|
|
||||||
onCurrentRoomChanged: {
|
onCurrentRoomChanged: {
|
||||||
_private.chatBarCache = currentRoom.mainCache
|
|
||||||
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
|
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
|
||||||
|
contentModel.focusedTextItem.
|
||||||
textField.text = ShareHandler.text;
|
textField.text = ShareHandler.text;
|
||||||
ShareHandler.text = "";
|
ShareHandler.text = "";
|
||||||
ShareHandler.room = "";
|
ShareHandler.room = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onActiveFocusChanged: if (activeFocus) {
|
||||||
|
core.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ShareHandler
|
target: ShareHandler
|
||||||
function onRoomChanged(): void {
|
function onRoomChanged(): void {
|
||||||
@@ -60,356 +60,31 @@ QQC2.Control {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root.currentRoom.mainCache
|
target: root.currentRoom.mainCache
|
||||||
|
|
||||||
function onMentionAdded(mention: string): void {
|
function onMentionAdded(text: string, hRef: string): void {
|
||||||
// add mention text
|
core.completionModel.insertCompletion(text, hRef);
|
||||||
textField.append(mention + " ");
|
|
||||||
// move cursor to the end
|
|
||||||
textField.cursorPosition = textField.text.length;
|
|
||||||
// move the focus back to the chat bar
|
// move the focus back to the chat bar
|
||||||
textField.forceActiveFocus(Qt.OtherFocusReason);
|
core.model.refocusCurrentComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
implicitHeight: column.implicitHeight + Kirigami.Units.largeSpacing
|
||||||
* @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
|
|
||||||
|
|
||||||
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
ColumnLayout {
|
||||||
|
id: column
|
||||||
// Matrix does not allow sending attachments in replies
|
anchors.top: root.top
|
||||||
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
anchors.horizontalCenter: root.horizontalCenter
|
||||||
icon.name: "mail-attachment"
|
ChatBarCore {
|
||||||
text: i18nc("@action:button", "Attach an image or file")
|
id: core
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
Message.room: root.currentRoom
|
||||||
|
room: root.currentRoom
|
||||||
onTriggered: {
|
maxAvailableWidth: chatBarSizeHelper.availableWidth
|
||||||
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
|
|
||||||
}
|
}
|
||||||
]
|
QQC2.Label {
|
||||||
|
|
||||||
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
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
visible: !Kirigami.Setting.isMobile
|
||||||
Loader {
|
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")
|
||||||
id: replyLoader
|
horizontalAlignment: Text.AlignRight
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale * 0.75
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibNeoChat.DelegateSizeHelper {
|
LibNeoChat.DelegateSizeHelper {
|
||||||
@@ -419,195 +94,8 @@ QQC2.Control {
|
|||||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||||
startPercentWidth: 100
|
startPercentWidth: 100
|
||||||
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
|
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
|
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.delegates as Delegates
|
||||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat.libneochat as LibNeoChat
|
||||||
|
|
||||||
QQC2.Popup {
|
QQC2.Popup {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property NeoChatConnection connection
|
property alias model: completions.model
|
||||||
required property var chatDocumentHandler
|
|
||||||
|
|
||||||
visible: completions.count > 0
|
required property LibNeoChat.ChatKeyHelper keyHelper
|
||||||
|
|
||||||
onVisibleChanged: if (visible) {
|
Connections {
|
||||||
root.open();
|
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: {
|
x: model.textItem.textItem.cursorRectangle.x - Kirigami.Units.largeSpacing
|
||||||
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
y: model.textItem.textItem.cursorRectangle.y - implicitHeight - Kirigami.Units.smallSpacing
|
||||||
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementIndex() {
|
function incrementIndex() {
|
||||||
completions.incrementCurrentIndex();
|
completions.incrementCurrentIndex();
|
||||||
@@ -38,8 +68,8 @@ QQC2.Popup {
|
|||||||
completions.decrementCurrentIndex();
|
completions.decrementCurrentIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
function complete() {
|
function completeCurrent() {
|
||||||
root.chatDocumentHandler.complete(completions.currentIndex);
|
model.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -49,61 +79,57 @@ QQC2.Popup {
|
|||||||
|
|
||||||
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: QQC2.ScrollView {
|
||||||
spacing: 0
|
contentWidth: Kirigami.Units.gridUnit * 20
|
||||||
Kirigami.Separator {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
QQC2.ScrollView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: contentHeight
|
|
||||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
|
|
||||||
|
|
||||||
background: Rectangle {
|
ListView {
|
||||||
color: Kirigami.Theme.backgroundColor
|
id: completions
|
||||||
}
|
currentIndex: 0
|
||||||
|
keyNavigationWraps: true
|
||||||
|
highlightMoveDuration: 100
|
||||||
|
onCountChanged: currentIndex = 0
|
||||||
|
delegate: Delegates.RoundedItemDelegate {
|
||||||
|
id: completionDelegate
|
||||||
|
|
||||||
ListView {
|
required property int index
|
||||||
id: completions
|
required property string displayName
|
||||||
|
required property string subtitle
|
||||||
|
required property string iconName
|
||||||
|
required property string replacedText
|
||||||
|
required property url hRef
|
||||||
|
|
||||||
model: root.chatDocumentHandler.completionModel
|
text: displayName
|
||||||
currentIndex: 0
|
|
||||||
keyNavigationWraps: true
|
|
||||||
highlightMoveDuration: 100
|
|
||||||
onCountChanged: currentIndex = 0
|
|
||||||
delegate: Delegates.RoundedItemDelegate {
|
|
||||||
id: completionDelegate
|
|
||||||
|
|
||||||
required property int index
|
contentItem: RowLayout {
|
||||||
required property string displayName
|
KirigamiComponents.Avatar {
|
||||||
required property string subtitle
|
visible: completionDelegate.iconName !== "invalid"
|
||||||
required property string iconName
|
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||||
text: displayName
|
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
||||||
|
name: completionDelegate.text
|
||||||
contentItem: RowLayout {
|
}
|
||||||
KirigamiComponents.Avatar {
|
Delegates.SubtitleContentItem {
|
||||||
visible: completionDelegate.iconName !== "invalid"
|
itemDelegate: completionDelegate
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
labelItem.textFormat: Text.PlainText
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
labelItem.clip: true // Intentional to limit insane Unicode in display names
|
||||||
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
subtitle: completionDelegate.subtitle ?? ""
|
||||||
name: completionDelegate.text
|
subtitleItem.textFormat: Text.PlainText
|
||||||
}
|
|
||||||
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
|
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
|
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)
|
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||||
title: i18nc("@title: create new poll in the room", "Create Poll")
|
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 {
|
contentItem: ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -148,4 +138,16 @@ Kirigami.Dialog {
|
|||||||
onClicked: optionModel.append({optionText: ""})
|
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