Compare commits
1 Commits
master
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f3c542d04 |
@@ -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,6 +28,7 @@ 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)
|
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
|
||||||
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.5)
|
find_package(QuotientQt6 0.9.3)
|
||||||
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 QuickTest)
|
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
|
||||||
add_subdirectory(autotests)
|
add_subdirectory(autotests)
|
||||||
# add_subdirectory(appiumtests)
|
# add_subdirectory(appiumtests)
|
||||||
if (NOT ANDROID)
|
if (NOT ANDROID)
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ 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
|
||||||
@@ -104,45 +110,3 @@ 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,9 +7,7 @@
|
|||||||
#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"
|
||||||
@@ -90,8 +88,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(room);
|
auto cache = new ChatBarCache(this);
|
||||||
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)};
|
cache->setText(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));
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
// 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,7 +14,6 @@
|
|||||||
#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"
|
||||||
|
|
||||||
@@ -37,6 +36,8 @@ 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();
|
||||||
@@ -76,7 +77,7 @@ void ChatBarCacheTest::empty()
|
|||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->cache().toString(), QString());
|
QCOMPARE(chatBarCache->text(), 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);
|
||||||
@@ -86,14 +87,47 @@ 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->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
chatBarCache->setText(u"some text"_s);
|
||||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||||
chatBarCache->setReplyId(eventId);
|
chatBarCache->setReplyId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
QCOMPARE(chatBarCache->text(), 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);
|
||||||
@@ -107,11 +141,11 @@ void ChatBarCacheTest::reply()
|
|||||||
void ChatBarCacheTest::replyMissingUser()
|
void ChatBarCacheTest::replyMissingUser()
|
||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
chatBarCache->setText(u"some text"_s);
|
||||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||||
chatBarCache->setReplyId(eventId);
|
chatBarCache->setReplyId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
QCOMPARE(chatBarCache->text(), 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);
|
||||||
@@ -138,7 +172,7 @@ void ChatBarCacheTest::edit()
|
|||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||||
|
|
||||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
chatBarCache->setText(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());
|
||||||
@@ -146,7 +180,7 @@ void ChatBarCacheTest::edit()
|
|||||||
});
|
});
|
||||||
chatBarCache->setEditId(eventId);
|
chatBarCache->setEditId(eventId);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
QCOMPARE(chatBarCache->text(), 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);
|
||||||
@@ -159,11 +193,11 @@ void ChatBarCacheTest::edit()
|
|||||||
void ChatBarCacheTest::attachment()
|
void ChatBarCacheTest::attachment()
|
||||||
{
|
{
|
||||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
chatBarCache->setText(u"some text"_s);
|
||||||
chatBarCache->setEditId(eventId);
|
chatBarCache->setEditId(eventId);
|
||||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||||
|
|
||||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
QCOMPARE(chatBarCache->text(), 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);
|
||||||
|
|||||||
36
autotests/chatdocumenthandlertest.cpp
Normal file
36
autotests/chatdocumenthandlertest.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// 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"
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
// 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,7 +29,6 @@
|
|||||||
#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"
|
||||||
@@ -180,7 +179,7 @@ void ModelTest::testRoomTreeModel()
|
|||||||
|
|
||||||
void ModelTest::testMessageContentModel()
|
void ModelTest::testMessageContentModel()
|
||||||
{
|
{
|
||||||
auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
|
auto contentModel = std::make_unique<MessageContentModel>(room, nullptr, eventId);
|
||||||
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
|
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
@@ -194,23 +193,21 @@ void ModelTest::testEventMessageContentModel()
|
|||||||
|
|
||||||
void ModelTest::testThreadModel()
|
void ModelTest::testThreadModel()
|
||||||
{
|
{
|
||||||
auto model = std::make_unique<ThreadModel>(eventId, room);
|
auto model = new ThreadModel(eventId, room);
|
||||||
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
auto tester = new QAbstractItemModelTester(model, model);
|
||||||
tester->setUseFetchMore(true);
|
tester->setUseFetchMore(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModelTest::testThreadFetchModel()
|
void ModelTest::testThreadFetchModel()
|
||||||
{
|
{
|
||||||
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
|
auto model = new ThreadFetchModel(new 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 threadModel = std::make_unique<ThreadModel>(eventId, room);
|
auto model = new ThreadChatBarModel(new ThreadModel(eventId, room), 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);
|
||||||
}
|
}
|
||||||
@@ -400,7 +397,9 @@ 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);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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).constFirst());
|
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
|
||||||
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,8 +227,7 @@ 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();
|
||||||
|
|
||||||
const auto changes = m_state.mid(token);
|
for (const auto &change : 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{
|
||||||
@@ -273,7 +272,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &change : changes) {
|
for (const auto &change : m_state.mid(token)) {
|
||||||
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();
|
||||||
@@ -300,7 +299,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &change : changes) {
|
for (const auto &change : m_state.mid(token)) {
|
||||||
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();
|
||||||
@@ -327,7 +326,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &change : changes) {
|
for (const auto &change : m_state.mid(token)) {
|
||||||
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();
|
||||||
@@ -354,7 +353,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &change : changes) {
|
for (const auto &change : m_state.mid(token)) {
|
||||||
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.
|
||||||
@@ -366,7 +365,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &change : changes) {
|
for (const auto &change : m_state.mid(token)) {
|
||||||
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,16 +73,6 @@ 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
1419
po/ar/neochat.po
1419
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1275
po/ast/neochat.po
1275
po/ast/neochat.po
File diff suppressed because it is too large
Load Diff
1430
po/az/neochat.po
1430
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
1350
po/cs/neochat.po
1350
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1379
po/da/neochat.po
1379
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
2010
po/de/neochat.po
2010
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1460
po/el/neochat.po
1460
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1510
po/en_GB/neochat.po
1510
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
1340
po/es/neochat.po
1340
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
1492
po/fi/neochat.po
1492
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
1280
po/ga/neochat.po
1280
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
1400
po/he/neochat.po
1400
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
1427
po/ia/neochat.po
1427
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1433
po/id/neochat.po
1433
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1412
po/ie/neochat.po
1412
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1485
po/it/neochat.po
1485
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1275
po/ja/neochat.po
1275
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
1430
po/pa/neochat.po
1430
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1531
po/pl/neochat.po
1531
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1419
po/pt/neochat.po
1419
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
1399
po/ro/neochat.po
1399
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
2739
po/ru/neochat.po
2739
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
1425
po/sk/neochat.po
1425
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,6 +70,7 @@ 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
|
||||||
@@ -78,6 +79,7 @@ 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
|
||||||
@@ -103,6 +105,7 @@ 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
|
||||||
@@ -171,7 +174,12 @@ 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)
|
||||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
if (NOT WIN32 AND NOT APPLE)
|
||||||
|
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
|
||||||
|
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
|
||||||
|
else()
|
||||||
|
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||||
|
endif()
|
||||||
target_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,8 +29,10 @@
|
|||||||
#include "proxycontroller.h"
|
#include "proxycontroller.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID)
|
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||||
#include "trayicon.h"
|
#include "trayicon.h"
|
||||||
|
#elif !defined(Q_OS_ANDROID)
|
||||||
|
#include "trayicon_sni.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_KUNIFIEDPUSH
|
#ifdef HAVE_KUNIFIEDPUSH
|
||||||
|
|||||||
@@ -74,11 +74,6 @@ 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()) {
|
||||||
@@ -94,26 +89,15 @@ void CommonRoomsModel::reload()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_loading = true;
|
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
|
||||||
Q_EMIT loadingChanged();
|
const auto &replyData = job->jsonData();
|
||||||
|
beginResetModel();
|
||||||
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId)
|
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
||||||
.then([this](const auto job) {
|
m_commonRooms.push_back(roomId.toString());
|
||||||
const auto &replyData = job->jsonData();
|
}
|
||||||
beginResetModel();
|
endResetModel();
|
||||||
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
Q_EMIT countChanged();
|
||||||
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,7 +21,6 @@ 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 {
|
||||||
@@ -44,13 +43,10 @@ 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();
|
||||||
@@ -58,5 +54,4 @@ 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,6 +207,10 @@
|
|||||||
</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,7 +21,6 @@ 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,26 +8,45 @@ 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: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
|
subtitle: {
|
||||||
dialogType: Kirigami.PromptDialog.Warning
|
if (root.room) {
|
||||||
standardButtons: QQC2.Dialog.Cancel
|
let message = xi18nc("@info Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml)
|
||||||
|
|
||||||
onAccepted: root.room.forget()
|
// 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
|
||||||
|
|
||||||
|
onRejected: {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
|
||||||
footer: QQC2.DialogButtonBox {
|
footer: QQC2.DialogButtonBox {
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
text: i18nc("@action:button Leave this room/space", "Leave")
|
text: i18nc("@action:button", "Leave Room")
|
||||||
icon.name: "arrow-left-symbolic"
|
|
||||||
|
|
||||||
onClicked: root.accept()
|
|
||||||
|
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||||
|
icon.name: "arrow-left-symbolic"
|
||||||
|
//onClicked: root.room.forget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,22 @@ 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
|
|
||||||
|
|
||||||
onAccepted: root.connection.logout(true)
|
onRejected: {
|
||||||
|
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,5 +17,12 @@ Kirigami.PromptDialog {
|
|||||||
|
|
||||||
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
|
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
|
||||||
|
|
||||||
onAccepted: Qt.openUrlExternally(root.link)
|
onAccepted: {
|
||||||
|
Qt.openUrlExternally(root.link);
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onRejected: {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
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
|
||||||
@@ -25,8 +24,6 @@ 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
|
||||||
@@ -34,26 +31,35 @@ Kirigami.Dialog {
|
|||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
|
||||||
onAccepted: {
|
standardButtons: Kirigami.Dialog.Cancel
|
||||||
// We don't necessarily have all the info so fill out the best we can.
|
customFooterActions: [
|
||||||
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
Kirigami.Action {
|
||||||
let displayName = "";
|
enabled: roomIdAliasText.isValidText
|
||||||
let avatarUrl = "";
|
text: i18n("OK")
|
||||||
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
icon.name: "dialog-ok"
|
||||||
let topic = "";
|
onTriggered: {
|
||||||
let memberCount = -1;
|
// We don't necessarily have all the info so fill out the best we can.
|
||||||
let isJoined = false;
|
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
||||||
if (roomIdAliasText.room) {
|
let displayName = "";
|
||||||
roomId = roomIdAliasText.room.id;
|
let avatarUrl = "";
|
||||||
displayName = roomIdAliasText.room.displayName;
|
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
||||||
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
let topic = "";
|
||||||
alias = roomIdAliasText.room.canonicalAlias;
|
let memberCount = -1;
|
||||||
topic = roomIdAliasText.room.topic;
|
let isJoined = false;
|
||||||
memberCount = roomIdAliasText.room.joinedCount;
|
if (roomIdAliasText.room) {
|
||||||
isJoined = true;
|
roomId = roomIdAliasText.room.id;
|
||||||
|
displayName = roomIdAliasText.room.displayName;
|
||||||
|
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
||||||
|
alias = roomIdAliasText.room.canonicalAlias;
|
||||||
|
topic = roomIdAliasText.room.topic;
|
||||||
|
memberCount = roomIdAliasText.room.joinedCount;
|
||||||
|
isJoined = true;
|
||||||
|
}
|
||||||
|
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
]
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -104,16 +110,4 @@ 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,16 +24,25 @@ 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
|
|
||||||
|
|
||||||
onAccepted: root.userSelected(userIdText.text)
|
standardButtons: Kirigami.Dialog.Cancel
|
||||||
|
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
|
||||||
@@ -70,16 +79,4 @@ 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,7 +4,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -15,16 +14,11 @@ 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: QQC2.Dialog.Cancel
|
standardButtons: Kirigami.Dialog.Cancel
|
||||||
|
|
||||||
footer: QQC2.DialogButtonBox {
|
customFooterActions: Kirigami.Action {
|
||||||
QQC2.Button {
|
icon.name: "camera-video-symbolic"
|
||||||
icon.name: "camera-video-symbolic"
|
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
onTriggered: root.accept()
|
||||||
|
|
||||||
onClicked: root.accept()
|
|
||||||
|
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,22 @@ 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
|
||||||
@@ -138,16 +148,4 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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,6 +361,7 @@ 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 Key Storage")
|
title: i18nc("@title:window", "Manage Secret Backup")
|
||||||
|
|
||||||
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 recovery key was not correct.")
|
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
|
||||||
banner.visible = true
|
banner.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +45,20 @@ FormCard.FormCardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
title: i18nc("@title", "Unlock using Recovery Key")
|
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
|
||||||
}
|
}
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
FormCard.FormTextDelegate {
|
FormCard.FormTextDelegate {
|
||||||
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.")
|
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
|
||||||
}
|
}
|
||||||
FormCard.FormTextFieldDelegate {
|
FormCard.FormTextFieldDelegate {
|
||||||
id: securityKeyField
|
id: securityKeyField
|
||||||
label: i18nc("@label:textbox", "Recovery Key:")
|
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
|
||||||
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 request encryption keys from other verified devices.")
|
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.")
|
||||||
}
|
}
|
||||||
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 ?? null,
|
avatarColor: root.room?.member(root.user.id).color,
|
||||||
avatarSource: avatar.source,
|
avatarSource: avatar.source,
|
||||||
}) as QrCodeMaximizeComponent;
|
}) as QrCodeMaximizeComponent;
|
||||||
root.close();
|
root.close();
|
||||||
@@ -401,36 +401,18 @@ 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.connection.canCheckMutualRooms
|
visible: !root.isSelf && root.hasMutualRooms
|
||||||
|
|
||||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
visible: !root.isSelf && root.connection.canCheckMutualRooms
|
visible: !root.isSelf && root.hasMutualRooms
|
||||||
|
|
||||||
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 NeochatRoomMember author
|
required property var 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.disambiguatedName, "https://matrix.to/#/" + root.author.id);
|
RoomManager.currentRoom.mainCache.mentionAdded(root.author.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ 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();
|
||||||
});
|
});
|
||||||
@@ -537,14 +546,13 @@ 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());
|
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||||
!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 && m_connection->room(spaceId)) {
|
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||||
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(Quotient::Uri uri, const QString &action = {});
|
Q_INVOKABLE void resolveResource(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 Quotient::RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
viewEventMenu(QObject *parent, const 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 Quotient::FileTransferInfo &progressInfo,
|
const FileTransferInfo &progressInfo,
|
||||||
const QString &selectedText,
|
const QString &selectedText,
|
||||||
const QString &hoveredLink);
|
const QString &hoveredLink);
|
||||||
|
|
||||||
|
|||||||
32
src/app/trayicon_sni.cpp
Normal file
32
src/app/trayicon_sni.cpp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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"
|
||||||
30
src/app/trayicon_sni.h
Normal file
30
src/app/trayicon_sni.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// 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,16 +32,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +46,7 @@ void WindowController::showAndRaiseWindow(const QString &startupId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!m_window->isVisible()) {
|
if (!m_window->isVisible()) {
|
||||||
if (m_wasMaximized) {
|
m_window->show();
|
||||||
m_window->showMaximized();
|
|
||||||
} else {
|
|
||||||
m_window->show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_WINDOWSYSTEM
|
#ifdef HAVE_WINDOWSYSTEM
|
||||||
|
|||||||
@@ -67,6 +67,5 @@ Q_SIGNALS:
|
|||||||
private:
|
private:
|
||||||
WindowController() = default;
|
WindowController() = default;
|
||||||
|
|
||||||
bool m_wasMaximized;
|
|
||||||
QWindow *m_window = nullptr;
|
QWindow *m_window = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
64
src/chatbar/AttachDialog.qml
Normal file
64
src/chatbar/AttachDialog.qml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 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,10 +6,8 @@ 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
|
||||||
@@ -17,25 +15,6 @@ 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,6 +1,5 @@
|
|||||||
// 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
|
||||||
@@ -26,26 +25,27 @@ import org.kde.neochat.libneochat as LibNeoChat
|
|||||||
*
|
*
|
||||||
* @sa ChatBar
|
* @sa ChatBar
|
||||||
*/
|
*/
|
||||||
Item {
|
QQC2.Control {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
required property LibNeoChat.NeoChatRoom currentRoom
|
required property 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,31 +60,356 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root.currentRoom.mainCache
|
target: root.currentRoom.mainCache
|
||||||
|
|
||||||
function onMentionAdded(text: string, hRef: string): void {
|
function onMentionAdded(mention: string): void {
|
||||||
core.completionModel.insertCompletion(text, hRef);
|
// add mention text
|
||||||
|
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
|
||||||
core.model.refocusCurrentComponent();
|
textField.forceActiveFocus(Qt.OtherFocusReason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
ColumnLayout {
|
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||||
id: column
|
|
||||||
anchors.top: root.top
|
// Matrix does not allow sending attachments in replies
|
||||||
anchors.horizontalCenter: root.horizontalCenter
|
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
||||||
ChatBarCore {
|
icon.name: "mail-attachment"
|
||||||
id: core
|
text: i18nc("@action:button", "Attach an image or file")
|
||||||
Message.room: root.currentRoom
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
room: root.currentRoom
|
|
||||||
maxAvailableWidth: chatBarSizeHelper.availableWidth
|
onTriggered: {
|
||||||
|
if (Clipboard.hasImage) {
|
||||||
|
let dialog = attachDialog.createObject(root.QQC2.Overlay.overlay) as AttachDialog;
|
||||||
|
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||||
|
dialog.open();
|
||||||
|
} else {
|
||||||
|
let dialog = openFileDialog.createObject(root.QQC2.Overlay.overlay) as OpenFileDialog;
|
||||||
|
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip: text
|
||||||
|
},
|
||||||
|
BusyAction {
|
||||||
|
id: emojiAction
|
||||||
|
|
||||||
|
isBusy: false
|
||||||
|
|
||||||
|
visible: !Kirigami.Settings.isMobile
|
||||||
|
icon.name: "smiley"
|
||||||
|
text: i18nc("@action:button", "Emojis & Stickers")
|
||||||
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
|
checkable: true
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (emojiDialog.visible) {
|
||||||
|
emojiDialog.close();
|
||||||
|
} else {
|
||||||
|
emojiDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tooltip: text
|
||||||
|
},
|
||||||
|
BusyAction {
|
||||||
|
id: mapButton
|
||||||
|
icon.name: "mark-location-symbolic"
|
||||||
|
isBusy: false
|
||||||
|
text: i18nc("@action:button", "Send a Location")
|
||||||
|
displayHint: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
(locationChooser.createObject(QQC2.Overlay.overlay, {
|
||||||
|
room: root.currentRoom
|
||||||
|
}) as LocationChooser).open();
|
||||||
|
}
|
||||||
|
tooltip: text
|
||||||
|
},
|
||||||
|
BusyAction {
|
||||||
|
id: pollButton
|
||||||
|
icon.name: "amarok_playcount"
|
||||||
|
isBusy: false
|
||||||
|
text: i18nc("@action:button", "Create a Poll")
|
||||||
|
displayHint: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
(newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
|
room: root.currentRoom
|
||||||
|
}) as NewPollDialog).open();
|
||||||
|
}
|
||||||
|
tooltip: text
|
||||||
|
},
|
||||||
|
BusyAction {
|
||||||
|
icon.name: "microphone"
|
||||||
|
isBusy: false
|
||||||
|
text: i18nc("@action:button", "Send a Voice Message")
|
||||||
|
displayHint: QQC2.AbstractButton.IconOnly
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = voiceMessageDialog.createObject(root, {
|
||||||
|
room: root.currentRoom
|
||||||
|
}) as VoiceMessageDialog;
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
tooltip: text
|
||||||
|
},
|
||||||
|
BusyAction {
|
||||||
|
id: sendAction
|
||||||
|
|
||||||
|
isBusy: false
|
||||||
|
|
||||||
|
icon.name: "document-send"
|
||||||
|
text: i18nc("@action:button", "Send message")
|
||||||
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
|
checkable: true
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
_private.postMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip: text
|
||||||
}
|
}
|
||||||
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
|
}
|
||||||
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")
|
Loader {
|
||||||
horizontalAlignment: Text.AlignRight
|
id: replyLoader
|
||||||
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 {
|
||||||
@@ -94,8 +419,195 @@ Item {
|
|||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
// 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,54 +11,24 @@ 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.libneochat as LibNeoChat
|
import org.kde.neochat
|
||||||
|
|
||||||
QQC2.Popup {
|
QQC2.Popup {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias model: completions.model
|
required property NeoChatConnection connection
|
||||||
|
required property var chatDocumentHandler
|
||||||
|
|
||||||
required property LibNeoChat.ChatKeyHelper keyHelper
|
visible: completions.count > 0
|
||||||
|
|
||||||
Connections {
|
onVisibleChanged: if (visible) {
|
||||||
target: keyHelper
|
root.open();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x: model.textItem.textItem.cursorRectangle.x - Kirigami.Units.largeSpacing
|
Component.onCompleted: {
|
||||||
y: model.textItem.textItem.cursorRectangle.y - implicitHeight - Kirigami.Units.smallSpacing
|
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
||||||
|
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
||||||
|
}
|
||||||
|
|
||||||
function incrementIndex() {
|
function incrementIndex() {
|
||||||
completions.incrementCurrentIndex();
|
completions.incrementCurrentIndex();
|
||||||
@@ -68,8 +38,8 @@ QQC2.Popup {
|
|||||||
completions.decrementCurrentIndex();
|
completions.decrementCurrentIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeCurrent() {
|
function complete() {
|
||||||
model.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
|
root.chatDocumentHandler.complete(completions.currentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -79,57 +49,61 @@ QQC2.Popup {
|
|||||||
|
|
||||||
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||||
|
|
||||||
contentItem: QQC2.ScrollView {
|
contentItem: ColumnLayout {
|
||||||
contentWidth: Kirigami.Units.gridUnit * 20
|
spacing: 0
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
QQC2.ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: contentHeight
|
||||||
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
|
||||||
|
|
||||||
ListView {
|
background: Rectangle {
|
||||||
id: completions
|
color: Kirigami.Theme.backgroundColor
|
||||||
currentIndex: 0
|
}
|
||||||
keyNavigationWraps: true
|
|
||||||
highlightMoveDuration: 100
|
|
||||||
onCountChanged: currentIndex = 0
|
|
||||||
delegate: Delegates.RoundedItemDelegate {
|
|
||||||
id: completionDelegate
|
|
||||||
|
|
||||||
required property int index
|
ListView {
|
||||||
required property string displayName
|
id: completions
|
||||||
required property string subtitle
|
|
||||||
required property string iconName
|
|
||||||
required property string replacedText
|
|
||||||
required property url hRef
|
|
||||||
|
|
||||||
text: displayName
|
model: root.chatDocumentHandler.completionModel
|
||||||
|
currentIndex: 0
|
||||||
|
keyNavigationWraps: true
|
||||||
|
highlightMoveDuration: 100
|
||||||
|
onCountChanged: currentIndex = 0
|
||||||
|
delegate: Delegates.RoundedItemDelegate {
|
||||||
|
id: completionDelegate
|
||||||
|
|
||||||
contentItem: RowLayout {
|
required property int index
|
||||||
KirigamiComponents.Avatar {
|
required property string displayName
|
||||||
visible: completionDelegate.iconName !== "invalid"
|
required property string subtitle
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
required property string iconName
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
|
||||||
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
text: displayName
|
||||||
name: completionDelegate.text
|
|
||||||
}
|
contentItem: RowLayout {
|
||||||
Delegates.SubtitleContentItem {
|
KirigamiComponents.Avatar {
|
||||||
itemDelegate: completionDelegate
|
visible: completionDelegate.iconName !== "invalid"
|
||||||
labelItem.textFormat: Text.PlainText
|
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||||
labelItem.clip: true // Intentional to limit insane Unicode in display names
|
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||||
subtitle: completionDelegate.subtitle ?? ""
|
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
||||||
subtitleItem.textFormat: Text.PlainText
|
name: completionDelegate.text
|
||||||
|
}
|
||||||
|
Delegates.SubtitleContentItem {
|
||||||
|
itemDelegate: completionDelegate
|
||||||
|
labelItem.textFormat: Text.PlainText
|
||||||
|
labelItem.clip: true // Intentional to limit insane Unicode in display names
|
||||||
|
subtitle: completionDelegate.subtitle ?? ""
|
||||||
|
subtitleItem.textFormat: Text.PlainText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
|
||||||
}
|
}
|
||||||
onClicked: root.model.insertCompletion(replacedText, hRef)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Kirigami.ShadowedRectangle {
|
background: Rectangle {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
// 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:")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
// 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 {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user