Compare commits

..

1 Commits

Author SHA1 Message Date
Tobias Fella
68a917e385 Comletely redo emoticon handling 2024-11-22 14:52:12 +01:00
145 changed files with 15561 additions and 32166 deletions

55
.reuse/dep5 Normal file
View File

@@ -0,0 +1,55 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: NeoChat
Upstream-Contact: Carl Schwan <carlschwan@kde.org>
Files: 128-logo.png icons/* logo.png org.kde.neochat.svg org.kde.neochat.tray.svg android/res/drawable/neochat.png
Copyright: 2020 Carson Black <uhhadd@gmail.com>
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
Files: android/res/drawable/splash.xml
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: .gitignore
Copyright: None
License: CC0-1.0
Files: .gitlab/issue_templates/bug.md
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: CC0-1.0
Files: src/res.qrc src/res_android.qrc src/res_desktop.qrc
Copyright: None
License: CC0-1.0
Files: cmake/Flatpak/99-noto-mono-color-emoji.conf
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: BSD-2-Clause
Files: src/neochatconfig.kcfg
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: src/qml/confetti.png src/qml/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: autotests/data/*
Copyright: none
License: CC0-1.0
Files: appiumtests/data/*
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: CC0-1.0
Files: src/purpose/purposeplugin.json
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause

View File

@@ -1,84 +0,0 @@
# SPDX-FileCopyrightText: none
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "NeoChat"
SPDX-PackageSupplier = "Carl Schwan <carlschwan@kde.org>"
[[annotations]]
path = ["128-logo.png", "icons/**", "logo.png", "org.kde.neochat.svg", "org.kde.neochat.tray.svg", "android/res/drawable/neochat.png", "android/neochat-playstore.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Carson Black <uhhadd@gmail.com>"
SPDX-License-Identifier = "LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL"
[[annotations]]
path = "android/res/drawable/splash.xml"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".gitignore"
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ".gitlab/issue_templates/bug.md"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Carl Schwan <carlschwan@kde.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ["src/res.qrc", "src/res_android.qrc", "src/res_desktop.qrc"]
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "cmake/Flatpak/99-noto-mono-color-emoji.conf"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Carl Schwan <carlschwan@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "src/neochatconfig.kcfg"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "src/neochat.notifyrc"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ["src/qml/confetti.png", "src/qml/glowdot.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Alexey Andreyev <aa13q@ya.ru>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ".flatpak-manifest.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020-2022 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "autotests/data/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "none"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "appiumtests/data/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "src/purpose/purposeplugin.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -83,15 +83,6 @@ def create_room():
next_sync_payload = "sync_response_new_room"
return response
@app.route("/_matrix/client/v3/publicRooms", methods=["POST"])
def public_rooms():
if request.get_json()["filter"]["generic_search_term"] == "forbidden":
data = dict()
data["errcode"] = "M_FORBIDDEN"
data["error"] = "You are not allowed to search for this. Go to https://wikipedia.org for more information"
return data, 403
return dict()
if __name__ == "__main__":

View File

@@ -32,6 +32,8 @@ private:
private Q_SLOTS:
void initTestCase();
void eventId();
void nullEventId();
void authorDisplayName();
void nullAuthorDisplayName();
void singleLineSidplayName();
@@ -54,8 +56,14 @@ private Q_SLOTS:
void nullSubtitle();
void mediaInfo();
void nullMediaInfo();
void hasReply();
void nullHasReply();
void replyId();
void nullReplyId();
void replyAuthor();
void nullReplyAuthor();
void thread();
void nullThread();
void location();
void nullLocation();
};
@@ -66,6 +74,17 @@ void EventHandlerTest::initTestCase()
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-eventhandler-sync.json"));
}
void EventHandlerTest::eventId()
{
QCOMPARE(EventHandler::id(room->messageEvents().at(0).get()), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
QTest::ignoreMessage(QtWarningMsg, "id called with event set to nullptr.");
QCOMPARE(EventHandler::id(nullptr), QString());
}
void EventHandlerTest::authorDisplayName()
{
QCOMPARE(EventHandler::authorDisplayName(room, room->messageEvents().at(1).get()), QStringLiteral("before"));
@@ -99,9 +118,8 @@ void EventHandlerTest::time()
{
const auto event = room->messageEvents().at(0).get();
QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, QTimeZone(QTimeZone::UTC))),
QDateTime::fromMSecsSinceEpoch(1234, QTimeZone(QTimeZone::UTC)));
QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::nullTime()
@@ -120,19 +138,19 @@ void EventHandlerTest::timeString()
KFormat format;
QCOMPARE(EventHandler::timeString(event, false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::LongFormat));
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(event, QStringLiteral("hh:mm")),
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(QStringLiteral("hh:mm")));
QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
}
void EventHandlerTest::highlighted()
@@ -277,6 +295,30 @@ void EventHandlerTest::nullMediaInfo()
QCOMPARE(EventHandler::mediaInfo(room, nullptr), QVariantMap());
}
void EventHandlerTest::hasReply()
{
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(5).get()), true);
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHasReply()
{
QTest::ignoreMessage(QtWarningMsg, "hasReply called with event set to nullptr.");
QCOMPARE(EventHandler::hasReply(nullptr), false);
}
void EventHandlerTest::replyId()
{
QCOMPARE(EventHandler::replyId(room->messageEvents().at(5).get()), QStringLiteral("$153456789:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(0).get()), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
QTest::ignoreMessage(QtWarningMsg, "replyId called with event set to nullptr.");
QCOMPARE(EventHandler::replyId(nullptr), QString());
}
void EventHandlerTest::replyAuthor()
{
auto replyEvent = room->messageEvents().at(0).get();
@@ -302,6 +344,29 @@ void EventHandlerTest::nullReplyAuthor()
QCOMPARE(EventHandler::replyAuthor(room, nullptr), RoomMember());
}
void EventHandlerTest::thread()
{
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(0).get()), false);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(0).get()), QString());
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(9).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(10).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(10).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(10).get()), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::nullThread()
{
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with event set to nullptr.");
QCOMPARE(EventHandler::isThreaded(nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with event set to nullptr.");
QCOMPARE(EventHandler::threadRoot(nullptr), QString());
}
void EventHandlerTest::location()
{
QCOMPARE(EventHandler::latitude(room->messageEvents().at(7).get()), QStringLiteral("51.7035").toFloat());

View File

@@ -11,7 +11,6 @@
#include <qnamespace.h>
#include "enums/messagecomponenttype.h"
#include "models/customemojimodel.h"
#include "neochatconnection.h"
#include "testutils.h"
@@ -77,7 +76,6 @@ void TextHandlerTest::initTestCase()
QJsonObject{{"body"_ls, "Test custom emoji"_ls},
{"url"_ls, "mxc://example.org/test"_ls},
{"usage"_ls, QJsonArray{"emoticon"_ls}}}}}}});
CustomEmojiModel::instance().setConnection(static_cast<NeoChatConnection *>(connection));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-texthandler-sync.json"));
}

View File

@@ -54,24 +54,19 @@
<summary xml:lang="ar">دردش على ماتركس</summary>
<summary xml:lang="ca">Xat a Matrix</summary>
<summary xml:lang="ca-valencia">Xat a Matrix</summary>
<summary xml:lang="de">Über Matrix unterhalten</summary>
<summary xml:lang="en-GB">Chat on Matrix</summary>
<summary xml:lang="es">Charle en Matrix</summary>
<summary xml:lang="eu">Berriketa Matrix-en</summary>
<summary xml:lang="fi">Keskustelu Matrixissä</summary>
<summary xml:lang="fr">Discuter sur Matrix</summary>
<summary xml:lang="gl">Charlar en Matrix</summary>
<summary xml:lang="he">התכתבות דרך Matrix</summary>
<summary xml:lang="hu">Csevegés Matrixon</summary>
<summary xml:lang="ia">Conversation en ditecto sur Matrix</summary>
<summary xml:lang="it">Chat su Matrix</summary>
<summary xml:lang="ka">ისაუბრეთ Matrix-ზე</summary>
<summary xml:lang="lv">Tērzējiet „Matrix“ tīklā</summary>
<summary xml:lang="nl">Chat op Matrix</summary>
<summary xml:lang="nn">Prat med via Matrix</summary>
<summary xml:lang="pl">Rozmawiaj na Matriksie</summary>
<summary xml:lang="sl">Klepet na Matrixu</summary>
<summary xml:lang="sv">Chatta på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸுக்கான உரையாடல் செயலி</summary>
<summary xml:lang="tr">Matrix Üzerinde Sohbet</summary>
<summary xml:lang="uk">Спілкування у Matrix</summary>
@@ -293,7 +288,7 @@
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::supporters">Tanguy Fardet;[dabe](https://freeradical.zone/@dabe);[lengau](https://mastodon.world/@lengau);Joshua Strobl;Stuart Turton</value>
<value key="KDE::supporters">Tanguy Fardet</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -448,7 +443,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.12.0" date="2024-12-12"/>
<release version="24.08.3" date="2024-11-07"/>
<release version="24.08.2" date="2024-10-10"/>
<release version="24.08.1" date="2024-09-12"/>

View File

@@ -88,31 +88,24 @@ GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix
Comment[ar]=دردش على ماتركس
Comment[ca]=Xat a Matrix
Comment[ca@valencia]=Xat a Matrix
Comment[de]=Über Matrix unterhalten
Comment[en_GB]=Chat on Matrix
Comment[es]=Chat en Matrix
Comment[eu]=Berriketa Matrix-en
Comment[fi]=Keskustele Matrixissä
Comment[fr]=Clavarder sur Matrix
Comment[gl]=Charle en Matrix
Comment[he]=התכתבות דרך Matrix
Comment[hu]=Csevegés Matrixon
Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[sl]=Klepet na Matrixu
Comment[sv]=Chatta på Matrix
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix Üzerinde Sohbet Et
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment[zh_TW]=在 Matrix 上聊天
MimeType=x-scheme-handler/matrix;
Exec=neochat %u
Terminal=false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@ parts:
- olm
- qtkeychain
source: https://github.com/quotient-im/libQuotient.git
source-tag: 0.9.1
source-tag: 0.9.0
source-depth: 1
plugin: cmake
build-packages:

View File

@@ -10,12 +10,6 @@ endif()
add_library(neochat STATIC
controller.cpp
controller.h
models/emojimodel.cpp
models/emojimodel.h
emojitones.cpp
emojitones.h
models/customemojimodel.cpp
models/customemojimodel.h
clipboard.cpp
clipboard.h
models/messageeventmodel.cpp
@@ -26,8 +20,6 @@ add_library(neochat STATIC
models/roomlistmodel.h
models/sortfilterspacelistmodel.cpp
models/sortfilterspacelistmodel.h
models/accountemoticonmodel.cpp
models/accountemoticonmodel.h
spacehierarchycache.cpp
spacehierarchycache.h
roommanager.cpp
@@ -50,8 +42,6 @@ add_library(neochat STATIC
models/userdirectorylistmodel.h
models/pushrulemodel.cpp
models/pushrulemodel.h
models/emoticonfiltermodel.cpp
models/emoticonfiltermodel.h
notificationsmanager.cpp
notificationsmanager.h
models/sortfilterroomlistmodel.cpp
@@ -101,10 +91,6 @@ add_library(neochat STATIC
texthandler.h
logger.cpp
logger.h
models/stickermodel.cpp
models/stickermodel.h
models/imagepacksmodel.cpp
models/imagepacksmodel.h
events/imagepackevent.cpp
events/imagepackevent.h
events/joinrulesevent.cpp
@@ -194,20 +180,12 @@ add_library(neochat STATIC
models/threadmodel.h
enums/messagetype.h
messagecomponent.h
enums/roomsortparameter.cpp
enums/roomsortparameter.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
if(ANDROID OR WIN32)
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
QT_QML_SOURCE_TYPENAME ShareAction
)
endif()
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
QML_FILES
@@ -290,6 +268,9 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConfirmLeaveDialog.qml
qml/CodeMaximizeComponent.qml
qml/EditStateDialog.qml
qml/EmojiPickerTypeHeader.qml
qml/EmojiPickerPackHeader.qml
qml/QuickReaction.qml
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
@@ -319,9 +300,13 @@ if(NOT ANDROID AND NOT WIN32)
qml/EditMenu.qml
)
else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
QT_RESOURCE_ALIAS qml/ShareAction.qml
)
qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml)
endif()
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
if(WIN32)
@@ -535,7 +520,6 @@ if(ANDROID)
"kde"
"list-remove-symbolic"
"edit-delete"
"user-home-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()

View File

@@ -519,7 +519,6 @@ QQC2.Control {
y: -implicitHeight
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom

View File

@@ -5,59 +5,30 @@ import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
QQC2.ItemDelegate {
QQC2.Button {
id: root
property string name
property string emoji
required property string toolTip
property bool showTones: false
property bool isImage: false
QQC2.ToolTip.text: root.name
QQC2.ToolTip.visible: hovered && root.name !== ""
QQC2.ToolTip.text: toolTip
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
leftInset: Kirigami.Units.smallSpacing
topInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
bottomInset: Kirigami.Units.smallSpacing
contentItem: Item {
Kirigami.Heading {
anchors.fill: parent
visible: !root.emoji.startsWith("mxc") && !root.isImage
text: root.emoji
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
flat: true
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5
source: "arrow-down-symbolic"
anchors.bottom: parent.bottom
anchors.right: parent.right
visible: root.showTones
}
}
Image {
anchors.fill: parent
visible: root.emoji.startsWith("mxc") || root.isImage
source: visible ? root.emoji : ""
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
}
contentItem: Kirigami.Heading {
text: root.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
background: Rectangle {
color: root.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
Rectangle {
radius: Kirigami.Units.cornerRadius
anchors.fill: parent
color: Kirigami.Theme.highlightColor
opacity: root.hovered && !root.pressed ? 0.2 : 0
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5
source: "arrow-down-symbolic"
anchors.bottom: parent.bottom
anchors.right: parent.right
visible: root.showTones
}
}
}

View File

@@ -16,9 +16,7 @@ QQC2.Popup {
*/
property NeoChatRoom currentRoom
property bool includeCustom: false
property bool closeOnChosen: true
property bool showQuickReaction: false
signal chosen(string emoji)
@@ -64,15 +62,15 @@ QQC2.Popup {
padding: 2
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, applicationWindow().width)
width: Math.min(contentItem.implicitWidth + 2 * padding, applicationWindow().width)
contentItem: EmojiPicker {
id: emojiPicker
height: 400
currentRoom: root.currentRoom
includeCustom: root.includeCustom
showQuickReaction: root.showQuickReaction
onChosen: emoji => {
root.chosen(emoji);
ImageContentManager.emojiUsed(emoji)
if (root.closeOnChosen) {
root.close();
}

View File

@@ -1,20 +1,17 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.textaddons.emoticons
QQC2.ScrollView {
id: root
property alias model: emojis.model
property alias count: emojis.count
required property int targetIconSize
readonly property int emojisPerRow: emojis.width / targetIconSize
required property bool withCustom
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
readonly property int emojisPerRow: emojis.width / Kirigami.Units.iconSizes.large
required property QtObject header
property bool stickers: false
@@ -25,6 +22,8 @@ QQC2.ScrollView {
emojis.forceActiveFocus();
}
width: Kirigami.Units.gridUnit * 24
GridView {
id: emojis
@@ -41,7 +40,9 @@ QQC2.ScrollView {
onModelChanged: currentIndex = -1
cellWidth: emojis.width / root.emojisPerRow
cellHeight: root.targetIconSize
cellHeight: Kirigami.Units.iconSizes.large
model: EmojiModelManager.emojiModel
KeyNavigation.up: root.header
@@ -49,50 +50,49 @@ QQC2.ScrollView {
delegate: EmojiDelegate {
id: emojiDelegate
checked: emojis.currentIndex === model.index
emoji: !!modelData ? modelData.unicode : model.url
name: !!modelData ? modelData.shortName : model.body
required property string unicode
required property string identifier
required property int index
text: emojiDelegate.unicode
toolTip: emojiDelegate.identifier
checked: emojis.currentIndex === emojiDelegate.index
width: emojis.cellWidth
height: emojis.cellHeight
isImage: root.stickers
Keys.onEnterPressed: clicked()
Keys.onReturnPressed: clicked()
onClicked: {
if (root.stickers) {
root.stickerChosen(model.index);
}
root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
EmojiModel.emojiUsed(modelData);
}
Keys.onSpacePressed: pressAndHold()
onPressAndHold: {
if (EmojiModel.tones(modelData.shortName).length === 0) {
return;
}
let tones = tonesPopupComponent.createObject(emojiDelegate, {
shortName: modelData.shortName,
unicode: modelData.unicode,
categoryIconSize: root.targetIconSize
});
tones.open();
tones.forceActiveFocus();
}
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
// onClicked: {
// if (root.stickers) {
// root.stickerChosen(model.index);
// }
// root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
// EmojiModel.emojiUsed(modelData);
// }
// Keys.onSpacePressed: pressAndHold()
// onPressAndHold: {
// if (!showTones) {
// return;
// }
// let tones = Qt.createComponent("org.kde.neochat", "EmojiTonesPicker").createObject(emojiDelegate, {
// shortName: modelData.shortName,
// unicode: modelData.unicode,
// categoryIconSize: root.targetIconSize,
// onChosen: root.chosen(emoji => root.chosen(emoji))
// });
// tones.open();
// tones.forceActiveFocus();
// }
// showTones: model.hasTones
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
text: root.stickers ? i18nc("@info", "No stickers") : i18nc("@info", "No emojis")
visible: emojis.count === 0
}
}
Component {
id: tonesPopupComponent
EmojiTonesPicker {
onChosen: root.chosen(emoji)
}
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022-2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.textaddons.emoticons
ColumnLayout {
id: root
@@ -13,87 +14,29 @@ ColumnLayout {
/**
* @brief The current room that user is viewing.
*/
property NeoChatRoom currentRoom
property bool includeCustom: false
property bool showQuickReaction: false
readonly property var currentEmojiModel: {
if (includeCustom) {
EmojiModel.categoriesWithCustom;
} else {
EmojiModel.categories;
}
}
readonly property int categoryIconSize: Math.round(Kirigami.Units.gridUnit * 2.5)
readonly property var currentCategory: currentEmojiModel[categories.currentIndex].category
readonly property alias categoryCount: categories.count
property int selectedType: 0
required property NeoChatRoom currentRoom
signal chosen(string emoji)
spacing: 0
onActiveFocusChanged: if (activeFocus) {
searchField.forceActiveFocus();
}
spacing: 0
EmojiPickerTypeHeader {
id: emoticonPickerTypeHeader
Kirigami.NavigationTabBar {
id: types
Layout.fillWidth: true
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: null
actions: [
Kirigami.Action {
id: emojis
icon.name: "smiley"
text: i18n("Emojis")
checked: true
onTriggered: root.selectedType = 0
},
Kirigami.Action {
id: stickers
icon.name: "stickers"
text: i18n("Stickers")
onTriggered: root.selectedType = 1
}
]
onSelectedTypeChanged: emoticonPickerCategoryHeader.currentIndex = 0
}
QQC2.ScrollView {
EmojiPickerPackHeader {
id: emoticonPickerCategoryHeader
Layout.fillWidth: true
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
visible: categories.count !== 0
ListView {
id: categories
clip: true
focus: true
orientation: ListView.Horizontal
Keys.onReturnPressed: if (emojiGrid.count > 0) {
emojiGrid.focus = true;
}
Keys.onEnterPressed: if (emojiGrid.count > 0) {
emojiGrid.focus = true;
}
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
keyNavigationEnabled: true
keyNavigationWraps: true
Keys.forwardTo: searchField
interactive: width !== contentWidth
model: root.selectedType === 0 ? root.currentEmojiModel : stickerPackModel
Component.onCompleted: categories.forceActiveFocus()
delegate: root.selectedType === 0 ? emojiDelegate : stickerDelegate
}
model: UnicodeEmoticonManager.categories
}
Kirigami.Separator {
@@ -105,119 +48,34 @@ ColumnLayout {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillWidth: true
visible: selectedType === 0
/**
* The focus is manged by the parent and we don't want to use the standard
* shortcut as it could block other SearchFields from using it.
*/
focusSequence: ""
}
EmojiGrid {
id: emojiGrid
targetIconSize: root.currentCategory === EmojiModel.Custom ? Kirigami.Units.gridUnit * 3 : root.categoryIconSize // Custom emojis are bigger
model: root.selectedType === 1 ? emoticonFilterModel : searchField.text.length === 0 ? EmojiModel.emojis(root.currentCategory) : (root.includeCustom ? EmojiModel.filterModel(searchField.text, false) : EmojiModel.filterModelNoCustom(searchField.text, false))
Layout.fillWidth: true
Layout.fillHeight: true
withCustom: root.includeCustom
onChosen: unicode => root.chosen(unicode)
header: categories
header: emoticonPickerCategoryHeader
Keys.forwardTo: searchField
stickers: root.selectedType === 1
stickers: emoticonPickerTypeHeader.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
}
Kirigami.Separator {
visible: showQuickReaction
Layout.fillWidth: true
Layout.preferredHeight: 1
}
QQC2.ScrollView {
visible: showQuickReaction
QuickReaction {
id: quickReaction
onChosen: root.chosen(text)
Layout.fillWidth: true
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
ListView {
id: quickReactions
Layout.fillWidth: true
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
delegate: EmojiDelegate {
emoji: modelData
height: root.categoryIconSize
width: height
onClicked: root.chosen(modelData)
}
orientation: Qt.Horizontal
}
}
ImagePacksModel {
id: stickerPackModel
room: root.currentRoom
showStickers: true
showEmoticons: false
}
StickerModel {
id: stickerModel
model: stickerPackModel
packIndex: 0
room: root.currentRoom
}
EmoticonFilterModel {
id: emoticonFilterModel
sourceModel: stickerModel
showStickers: true
}
Component {
id: emojiDelegate
Kirigami.NavigationTabButton {
width: root.categoryIconSize
height: width
checked: categories.currentIndex === model.index
text: modelData ? modelData.emoji : ""
QQC2.ToolTip.text: modelData ? modelData.name : ""
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
onClicked: {
categories.currentIndex = index;
categories.focus = true;
}
}
}
Component {
id: stickerDelegate
Kirigami.NavigationTabButton {
width: root.categoryIconSize
height: width
checked: stickerModel.packIndex === model.index
padding: Kirigami.Units.largeSpacing
contentItem: Image {
source: model.avatarUrl
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
QQC2.ToolTip.text: model.name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered && !!model.name
onClicked: stickerModel.packIndex = model.index
}
}
function clearSearchField() {
searchField.text = "";
searchField.text = ""
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@@ -54,8 +54,8 @@ QQC2.Popup {
delegate: EmojiDelegate {
id: emojiDelegate
checked: tonesList.currentIndex === model.index
emoji: modelData.unicode
name: modelData.shortName
text: modelData.unicode
toolTip: modelData.shortName
width: root.categoryIconSize
height: width

View File

@@ -11,8 +11,6 @@
#include "neochatroom.h"
#include "texthandler.h"
using namespace Qt::StringLiterals;
ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent)
{
@@ -321,25 +319,7 @@ void ChatBarCache::postMessage()
return;
}
bool isReply = !replyId().isEmpty();
const auto replyIt = room->findInTimeline(replyId());
if (replyIt == room->historyEdge()) {
isReply = false;
}
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
std::optional<Quotient::EventRelation> relatesTo = std::nullopt;
if (!threadId().isEmpty()) {
relatesTo = Quotient::EventRelation::replyInThread(threadId(), !isReply, isReply ? replyId() : threadId());
} else if (!editId().isEmpty()) {
relatesTo = Quotient::EventRelation::replace(editId());
} else if (isReply) {
relatesTo = Quotient::EventRelation::replyTo(replyId());
}
const auto type = std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result);
room->post<Quotient::RoomMessageEvent>(text(), type ? *type : Quotient::RoomMessageEvent::MsgType::Text, std::move(content), relatesTo);
room->postMessage(text(), sendText, *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), replyId(), editId(), threadId());
clearCache();
}

View File

@@ -8,6 +8,7 @@
ColorSchemer::ColorSchemer(QObject *parent)
: QObject(parent)
, c(new KColorSchemeManager(this))
{
}
@@ -17,17 +18,17 @@ ColorSchemer::~ColorSchemer()
QAbstractItemModel *ColorSchemer::model() const
{
return KColorSchemeManager::instance()->model();
return c->model();
}
void ColorSchemer::apply(int idx)
{
KColorSchemeManager::instance()->activateScheme(KColorSchemeManager::instance()->model()->index(idx, 0));
c->activateScheme(c->model()->index(idx, 0));
}
int ColorSchemer::indexForCurrentScheme()
{
return KColorSchemeManager::instance()->indexForSchemeId(KColorSchemeManager::instance()->activeSchemeId()).row();
return c->indexForSchemeId(c->activeSchemeId()).row();
}
#include "moc_colorschemer.cpp"

View File

@@ -49,4 +49,7 @@ public:
* @sa KColorScheme
*/
Q_INVOKABLE int indexForCurrentScheme();
private:
KColorSchemeManager *c;
};

View File

@@ -423,14 +423,10 @@ void Controller::setTestMode(bool test)
void Controller::removeConnection(const QString &userId)
{
// When loadAccessTokenFromKeyChain() fails m_connectionsLoading won't have an
// entry for it so we need to check both separately.
if (m_accountsLoading.contains(userId)) {
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
}
if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) {
auto connection = m_connectionsLoading[userId];
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
SettingsGroup("Accounts"_ls).remove(userId);
}
}

View File

@@ -33,28 +33,4 @@ ColumnLayout {
}
}
}
FormCard.FormCard {
FormCard.FormSwitchDelegate {
id: showAccessTokenCheckbox
text: i18nc("@info", "Show Access Token")
description: i18n("This should not be shared with anyone, even other users. This token gives full access to your account.")
}
FormCard.FormTextDelegate {
text: i18nc("@info", "Access Token")
description: root.connection.accessToken
visible: showAccessTokenCheckbox.checked
contentItem.children: QQC2.Button {
text: i18nc("@action:button", "Copy access token to clipboard")
icon.name: "edit-copy"
display: QQC2.AbstractButton.IconOnly
onClicked: Clipboard.saveText(root.connection.accessToken)
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
}
}

View File

@@ -21,22 +21,19 @@ ColumnLayout {
title: i18nc("@title", "Choose Room")
}
FormCard.FormCard {
FormCard.FormButtonDelegate {
text: root.room?.displayNameForHtml ?? i18nc("@info", "No room selected")
description: i18nc("@info", "Click to choose a room");
onClicked: {
let dialog = root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Choose Room"),
width: Kirigami.Units.gridUnit * 24
});
dialog.chosen.connect(id => root.room = root.connection.room(id))
}
FormCard.FormComboBoxDelegate {
id: roomComboBox
text: i18n("Room")
textRole: "escapedDisplayName"
valueRole: "roomId"
displayText: RoomManager.roomListModel.data(RoomManager.roomListModel.index(currentIndex, 0), RoomListModel.EscapedDisplayNameRole)
model: RoomManager.roomListModel
currentIndex: 0
displayMode: FormCard.FormComboBoxDelegate.Page
Component.onCompleted: currentIndex = RoomManager.roomListModel.rowForRoom(root.room)
onCurrentValueChanged: root.room = RoomManager.roomListModel.roomByAliasOrId(roomComboBox.currentValue)
}
FormCard.FormTextDelegate {
visible: root.room
text: i18n("Room Id: %1", root.room.id)
}
}

View File

@@ -37,7 +37,7 @@ FormCard.FormCardPage {
}
function openEventSource(stateKey: string): void {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateKeysModel,
allowEdit: true,
room: root.room,

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: None
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "emojitones.h"
#include "models/emojimodel.h"
QMultiHash<QString, QVariant> EmojiTones::_tones = {
#include "emojitones_data.h"
};

View File

@@ -1,21 +0,0 @@
// SPDX-FileCopyrightText: None
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QVariant>
/**
* @class EmojiTones
*
* This class provides a _tones variable with the available emoji tones to EmojiModel.
*
* @sa EmojiModel
*/
class EmojiTones
{
private:
static QMultiHash<QString, QVariant> _tones;
friend class EmojiModel;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: 2024 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 "roomsortparameter.h"
namespace
{
template<typename T>
int typeCompare(T left, T right)
{
return left == right ? 0 : left > right ? 1 : -1;
}
template<>
int typeCompare<QString>(QString left, QString right)
{
return left.localeAwareCompare(right);
}
}
int RoomSortParameter::compareParameter(Parameter parameter, NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
switch (parameter) {
case AlphabeticalAscending:
return compareParameter<AlphabeticalAscending>(leftRoom, rightRoom);
case AlphabeticalDescending:
return compareParameter<AlphabeticalDescending>(leftRoom, rightRoom);
case HasUnread:
return compareParameter<HasUnread>(leftRoom, rightRoom);
case MostUnread:
return compareParameter<MostUnread>(leftRoom, rightRoom);
case HasHighlight:
return compareParameter<HasHighlight>(leftRoom, rightRoom);
case MostHighlights:
return compareParameter<MostHighlights>(leftRoom, rightRoom);
case LastActive:
return compareParameter<LastActive>(leftRoom, rightRoom);
default:
return false;
}
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::AlphabeticalAscending>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return -typeCompare(leftRoom->displayName(), rightRoom->displayName());
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::AlphabeticalDescending>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return typeCompare(leftRoom->displayName(), rightRoom->displayName());
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::HasUnread>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return typeCompare(leftRoom->contextAwareNotificationCount() > 0, rightRoom->contextAwareNotificationCount() > 0);
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::MostUnread>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return typeCompare(leftRoom->contextAwareNotificationCount(), rightRoom->contextAwareNotificationCount());
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::HasHighlight>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
const auto leftHighlight = leftRoom->highlightCount() > 0 && leftRoom->contextAwareNotificationCount() > 0;
const auto rightHighlight = rightRoom->highlightCount() > 0 && rightRoom->contextAwareNotificationCount() > 0;
return typeCompare(leftHighlight, rightHighlight);
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::MostHighlights>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return typeCompare(int(leftRoom->highlightCount()), int(rightRoom->highlightCount()));
}
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::LastActive>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom)
{
return typeCompare(leftRoom->lastActiveTime(), rightRoom->lastActiveTime());
}

View File

@@ -1,122 +0,0 @@
// SPDX-FileCopyrightText: 2024 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 "neochatroom.h"
#include <QObject>
#include <QQmlEngine>
#include <KLocalizedString>
/**
* @class RoomSortParameter
*
* A class with the Parameter enum for room sort parameters.
*/
class RoomSortParameter : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the available sort parameters.
*/
enum Parameter {
AlphabeticalAscending,
AlphabeticalDescending,
HasUnread,
MostUnread,
HasHighlight,
MostHighlights,
LastActive,
};
Q_ENUM(Parameter)
/**
* @brief Translate the Parameter enum value to a human readable name string.
*
* @sa Parameter
*/
static QString parameterName(Parameter parameter)
{
switch (parameter) {
case Parameter::AlphabeticalAscending:
return i18nc("As in sorting alphabetically with A first and Z last", "Alphabetical Ascending");
case Parameter::AlphabeticalDescending:
return i18nc("As in sorting alphabetically with Z first and A last", "Alphabetical Descending");
case Parameter::HasUnread:
return i18nc("As in sorting rooms with unread message above those without", "Has Unread Messages");
case Parameter::MostUnread:
return i18nc("As in sorting rooms with the most unread messages higher", "Most Unread Messages");
case Parameter::HasHighlight:
return i18nc("As in sorting rooms with highlighted message above those without", "Has Highlighted Messages");
case Parameter::MostHighlights:
return i18nc("As in sorting rooms with the most highlighted messages higher", "Most Highlighted Messages");
case Parameter::LastActive:
return i18nc("As in sorting the chat room with the newest meassage first", "Last Active");
default:
return {};
}
};
/**
* @brief Translate the Parameter enum value to a human readable description string.
*
* @sa Parameter
*/
static QString parameterDescription(Parameter parameter)
{
switch (parameter) {
case Parameter::AlphabeticalAscending:
return i18nc("@info", "Room names closer to A alphabetically are higher");
case Parameter::AlphabeticalDescending:
return i18nc("@info", "Room names closer to Z alphabetically are higher");
case Parameter::HasUnread:
return i18nc("@info", "Rooms with unread messages are higher");
case Parameter::MostUnread:
return i18nc("@info", "Rooms with the most unread message are higher");
case Parameter::HasHighlight:
return i18nc("@info", "Rooms with highlighted messages are higher");
case Parameter::MostHighlights:
return i18nc("@info", "Rooms with the most highlighted messages are higher");
case Parameter::LastActive:
return i18nc("@info", "Rooms with the newer messages are higher");
default:
return {};
}
};
/**
* @brief Compare the given parameter of the two given rooms.
*
* @return 0 if they are equal, 1 if the left is greater and -1 if the right is greater.
*
* @sa Parameter
*/
static int compareParameter(Parameter parameter, NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
private:
template<Parameter parameter>
static int compareParameter(NeoChatRoom *, NeoChatRoom *)
{
return false;
}
};
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::AlphabeticalAscending>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::AlphabeticalDescending>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::HasUnread>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::MostUnread>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::HasHighlight>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::MostHighlights>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);
template<>
int RoomSortParameter::compareParameter<RoomSortParameter::LastActive>(NeoChatRoom *leftRoom, NeoChatRoom *rightRoom);

View File

@@ -49,6 +49,16 @@ Q_DECLARE_FLAGS(MemberChanges, MemberChange)
Q_DECLARE_OPERATORS_FOR_FLAGS(MemberChanges)
};
QString EventHandler::id(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "id called with event set to nullptr.";
return {};
}
return !event->id().isEmpty() ? event->id() : event->transactionId();
}
QString EventHandler::authorDisplayName(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
{
if (room == nullptr) {
@@ -60,7 +70,7 @@ QString EventHandler::authorDisplayName(const NeoChatRoom *room, const Quotient:
return {};
}
if (is<RoomMemberEvent>(*event) && event->unsignedJson()[QStringLiteral("prev_content")].toObject().contains("displayname"_L1)
if (is<RoomMemberEvent>(*event) && !event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& event->stateKey() == event->senderId()) {
auto previousDisplayName = event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
@@ -281,7 +291,7 @@ QString EventHandler::markdownBody(const Quotient::RoomEvent *event)
QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines)
{
if (event->isRedacted() && !event->isStateEvent()) {
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason.toHtmlEscaped());
}
@@ -498,7 +508,7 @@ QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomE
qCWarning(EventHandling) << "genericBody called with event set to nullptr.";
return {};
}
if (event->isRedacted() && !event->isStateEvent()) {
if (event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
@@ -824,6 +834,31 @@ QVariantMap EventHandler::getMediaInfoFromTumbnail(const NeoChatRoom *room, cons
return thumbnailInfo;
}
bool EventHandler::hasReply(const Quotient::RoomEvent *event, bool showFallbacks)
{
if (event == nullptr) {
qCWarning(EventHandling) << "hasReply called with event set to nullptr.";
return false;
}
const auto relations = event->contentPart<QJsonObject>("m.relates_to"_ls);
if (!relations.isEmpty()) {
const bool hasReplyRelation = relations.contains("m.in_reply_to"_ls);
bool isFallingBack = relations["is_falling_back"_ls].toBool();
return hasReplyRelation && (showFallbacks ? true : !isFallingBack);
}
return false;
}
QString EventHandler::replyId(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "replyId called with event set to nullptr.";
return {};
}
return event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
Quotient::RoomMember EventHandler::replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (room == nullptr) {
@@ -842,6 +877,38 @@ Quotient::RoomMember EventHandler::replyAuthor(const NeoChatRoom *room, const Qu
}
}
bool EventHandler::isThreaded(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with event set to nullptr.";
return false;
}
return (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists.
if (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return id(event);
}
return {};
}
float EventHandler::latitude(const Quotient::RoomEvent *event)
{
if (event == nullptr) {

View File

@@ -37,6 +37,14 @@ class NeoChatRoom;
class EventHandler
{
public:
/**
* @brief Return the ID of the event.
*
* Returns the transaction ID if the Matrix ID is empty, which may be the case
* for a pending event.
*/
static QString id(const Quotient::RoomEvent *event);
/**
* @brief Get the display name of the event author.
*
@@ -212,6 +220,20 @@ public:
*/
static QVariantMap mediaInfo(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Whether the event is a reply to another in the timeline.
*
* @param showFallbacks whether message that have is_falling_back set true should
* show the fallback reply. Leave true for non-threaded
* timelines.
*/
static bool hasReply(const Quotient::RoomEvent *event, bool showFallbacks = true);
/**
* @brief Return the Matrix ID of the event replied to.
*/
static QString replyId(const Quotient::RoomEvent *event);
/**
* @brief Get the author of the event replied to in context of the room.
*
@@ -227,6 +249,20 @@ public:
*/
static Quotient::RoomMember replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
static bool isThreaded(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
static QString threadRoot(const Quotient::RoomEvent *event);
/**
* @brief Return the latitude for the event.
*

View File

@@ -4,12 +4,11 @@
#include "linkpreviewer.h"
#include <Quotient/connection.h>
#include <Quotient/csapi/authed-content-repo.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/events/roommessageevent.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "utils.h"
using namespace Quotient;
@@ -62,13 +61,7 @@ void LinkPreviewer::loadUrlPreview()
if (conn == nullptr) {
return;
}
BaseJob *job = nullptr;
if (conn->supportedMatrixSpecVersions().contains("v1.11"_L1)) {
job = conn->callApi<GetUrlPreviewAuthedJob>(m_url);
} else {
QT_IGNORE_DEPRECATIONS(job = conn->callApi<GetUrlPreviewJob>(m_url);)
}
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url);
connect(job, &BaseJob::success, this, [this, job, conn]() {
const auto json = job->jsonData();

View File

@@ -25,7 +25,8 @@ void LoginHelper::init()
m_connection = new NeoChatConnection();
m_matrixId = QString();
m_password = QString();
m_deviceName = QStringLiteral("NeoChat");
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
.arg(QSysInfo::machineHostName(), QSysInfo::productType(), QSysInfo::productVersion(), QSysInfo::currentCpuArchitecture());
m_supportsSso = false;
m_supportsPassword = false;
m_ssoUrl = QUrl();

View File

@@ -271,6 +271,7 @@ int main(int argc, char *argv[])
#endif
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
if (parser.isSet("ignore-ssl-errors"_ls)) {

View File

@@ -1,222 +0,0 @@
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "accountemoticonmodel.h"
#include <QImage>
#include <QMimeDatabase>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/events/eventcontent.h>
#include <qcoro/qcorosignal.h>
#include "neochatconnection.h"
using namespace Quotient;
AccountEmoticonModel::AccountEmoticonModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int AccountEmoticonModel::rowCount(const QModelIndex &index) const
{
Q_UNUSED(index);
if (!m_images) {
return 0;
}
return m_images->images.size();
}
QVariant AccountEmoticonModel::data(const QModelIndex &index, int role) const
{
if (m_connection == nullptr) {
return {};
}
const auto &row = index.row();
const auto &image = m_images->images[row];
if (role == UrlRole) {
return m_connection->makeMediaUrl(image.url).toString();
}
if (role == BodyRole) {
if (image.body) {
return *image.body;
}
}
if (role == ShortCodeRole) {
return image.shortcode;
}
if (role == IsStickerRole) {
if (image.usage) {
return image.usage->isEmpty() || image.usage->contains("sticker"_ls);
}
if (m_images->pack && m_images->pack->usage) {
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_ls);
}
return true;
}
if (role == IsEmojiRole) {
if (image.usage) {
return image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
}
if (m_images->pack && m_images->pack->usage) {
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_ls);
}
return true;
}
return {};
}
QHash<int, QByteArray> AccountEmoticonModel::roleNames() const
{
return {
{AccountEmoticonModel::UrlRole, "url"},
{AccountEmoticonModel::BodyRole, "body"},
{AccountEmoticonModel::ShortCodeRole, "shortcode"},
{AccountEmoticonModel::IsStickerRole, "isSticker"},
{AccountEmoticonModel::IsEmojiRole, "isEmoji"},
};
}
NeoChatConnection *AccountEmoticonModel::connection() const
{
return m_connection;
}
void AccountEmoticonModel::setConnection(NeoChatConnection *connection)
{
if (m_connection) {
disconnect(m_connection, nullptr, this, nullptr);
}
m_connection = connection;
Q_EMIT connectionChanged();
connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) {
if (type == QStringLiteral("im.ponies.user_emotes")) {
reloadEmoticons();
}
});
reloadEmoticons();
}
void AccountEmoticonModel::reloadEmoticons()
{
if (m_connection == nullptr) {
return;
}
QJsonObject json;
if (m_connection->hasAccountData("im.ponies.user_emotes"_ls)) {
json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson();
}
const auto &content = ImagePackEventContent(json);
beginResetModel();
m_images = content;
endResetModel();
}
void AccountEmoticonModel::deleteEmoticon(int index)
{
if (m_connection == nullptr) {
return;
}
QJsonObject data;
m_images->images.removeAt(index);
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
}
void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
{
if (m_connection == nullptr) {
return;
}
m_images->images[index].body = text;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
}
void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortcode)
{
if (m_connection == nullptr) {
return;
}
m_images->images[index].shortcode = shortcode;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
}
void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
{
if (m_connection == nullptr) {
return;
}
doSetEmoticonImage(index, source);
}
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
co_await qCoro(job.get(), &BaseJob::finished);
if (job->error() != BaseJob::NoError) {
co_return;
}
m_images->images[index].url = job->contentUri();
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_ls);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_ls)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images[index].info = info;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
}
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
co_await qCoro(job.get(), &BaseJob::finished);
if (job->error() != BaseJob::NoError) {
co_return;
}
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_ls);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_ls)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images.append(ImagePackEventContent::ImagePackImage{
shortcode,
job->contentUri(),
description,
info,
QStringList{type},
});
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
}
void AccountEmoticonModel::addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type)
{
if (m_connection == nullptr) {
return;
}
doAddEmoticon(source, shortcode, description, type);
}
#include "moc_accountemoticonmodel.cpp"

View File

@@ -1,104 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QCoroTask>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
class NeoChatConnection;
/**
* @class AccountEmoticonModel
*
* This class defines the model for visualising the account stickers and emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class AccountEmoticonModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The connection to get emoticons from.
*/
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
enum Roles {
UrlRole = Qt::UserRole + 1, /**< The URL for the emoticon. */
ShortCodeRole, /**< The shortcode for the emoticon. */
BodyRole, //**< A textual description of the emoticon */
IsStickerRole, //**< Whether this emoticon is a sticker */
IsEmojiRole, //**< Whether this emoticon is an emoji */
};
explicit AccountEmoticonModel(QObject *parent = nullptr);
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
/**
* @brief Deletes the emoticon at the given index.
*/
Q_INVOKABLE void deleteEmoticon(int index);
/**
* @brief Changes the description for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonBody(int index, const QString &text);
/**
* @brief Changes the shortcode for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonShortcode(int index, const QString &shortCode);
/**
* @brief Changes the image for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonImage(int index, const QUrl &source);
/**
* @brief Add an emoticon with the given parameters.
*/
Q_INVOKABLE void addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type);
Q_SIGNALS:
void connectionChanged();
private:
std::optional<Quotient::ImagePackEventContent> m_images;
QPointer<NeoChatConnection> m_connection;
QCoro::Task<void> doSetEmoticonImage(int index, QUrl source);
QCoro::Task<void> doAddEmoticon(QUrl source, QString shortcode, QString description, QString type);
void reloadEmoticons();
};

View File

@@ -109,10 +109,11 @@ QList<ActionsModel::Action> actions{
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
}
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(rainbowText, u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>("/rainbow %1"_L1.arg(text), MessageEventType::Text, std::move(content), relatesTo);
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Text,
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -128,10 +129,11 @@ QList<ActionsModel::Action> actions{
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
}
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(rainbowText, u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>(u"/rainbow %1"_s.arg(text), MessageEventType::Text, std::move(content), relatesTo);
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Emote,
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -142,7 +144,7 @@ QList<ActionsModel::Action> actions{
Action{
QStringLiteral("plain"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
room->postPlainText(text.toHtmlEscaped());
room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {});
return QString();
},
false,
@@ -154,10 +156,11 @@ QList<ActionsModel::Action> actions{
QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(u"<span data-mx-spoiler>%1</span>"_s.arg(text), u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>(u"/spoiler %1"_s.arg(text), MessageEventType::Text, std::move(content), relatesTo);
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -602,15 +605,15 @@ bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messa
if (eventRelation && eventRelation->type == "m.replace"_L1) {
replaceId = eventRelation->eventId;
}
std::unique_ptr<EventContent::TextContent> content = nullptr;
if (flags == "/g"_L1) {
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, replaceId);
} else {
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
room->postHtmlMessage(messageText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
replaceId);
}
Quotient::EventRelation relatesTo = Quotient::EventRelation::replace(replaceId);
room->post<Quotient::RoomMessageEvent>(messageText, event->msgtype(), std::move(content), relatesTo);
return true;
}
}

View File

@@ -6,8 +6,7 @@
#include "actionsmodel.h"
#include "completionproxymodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"
// #include "emojimodel.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "userlistmodel.h"
@@ -16,11 +15,13 @@ CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_userListModel(RoomManager::instance().userListModel())
, m_emojiModel(new QConcatenateTablesProxyModel(this))
//, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance());
connect(this, &CompletionModel::roomChanged, this, [this]() {
m_userListModel->setRoom(m_room);
});
// TODO m_emojiModel->addSourceModel(&EmojiModel::instance());
}
QString CompletionModel::text() const
@@ -88,20 +89,20 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
}
}
if (m_autoCompletionType == Emoji) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
}
}
// if (m_autoCompletionType == Emoji) {
// if (role == DisplayNameRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
// }
// if (role == IconNameRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
// }
// if (role == ReplacedTextRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
// }
// if (role == SubtitleRole) {
// // TODO return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
// }
// }
return {};
}
@@ -147,8 +148,8 @@ void CompletionModel::updateCompletion()
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);
m_autoCompletionType = Emoji;
m_filterModel->setFilterRole(CustomEmojiModel::Name);
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
// m_filterModel->setFilterRole(CustomEmojiModel::Name);
// TODO m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();

View File

@@ -1,213 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "customemojimodel.h"
#include <QImage>
#include <QMimeDatabase>
#include "emojimodel.h"
#include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/content-repo.h>
using namespace Quotient;
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchEmojis();
}
NeoChatConnection *CustomEmojiModel::connection() const
{
return m_connection;
}
void CustomEmojiModel::fetchEmojis()
{
if (!m_connection) {
return;
}
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
if (data == nullptr) {
return;
}
QJsonObject emojis = data->contentJson()["images"_ls].toObject();
// TODO: Remove with stable migration
const auto legacyEmojis = data->contentJson()["emoticons"_ls].toObject();
for (const auto &emoji : legacyEmojis.keys()) {
if (!emojis.contains(emoji)) {
emojis[emoji] = legacyEmojis[emoji];
}
}
beginResetModel();
m_emojis.clear();
for (const auto &emoji : emojis.keys()) {
const auto &data = emojis[emoji];
const auto e = emoji.startsWith(":"_ls) ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
m_emojis << CustomEmoji{e, data.toObject()["url"_ls].toString(), QRegularExpression(e)};
}
endResetModel();
}
void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
using namespace Quotient;
auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_ls].toObject();
QString url;
url = job->contentUri().toString();
QImage image(location.toLocalFile());
QJsonObject imageInfo;
imageInfo["w"_ls] = image.width();
imageInfo["h"_ls] = image.height();
imageInfo["mimetype"_ls] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
imageInfo["size"_ls] = image.sizeInBytes();
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
{QStringLiteral("url"), url},
{QStringLiteral("info"), imageInfo},
{QStringLiteral("body"), location.fileName()},
{"usage"_ls, "emoticon"_ls},
});
json["images"_ls] = emojiData;
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
});
}
void CustomEmojiModel::removeEmoji(const QString &name)
{
using namespace Quotient;
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
Q_ASSERT(data);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
auto emojiData = json["images"_ls].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["images"_ls] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["images"_ls] = emojiData;
}
emojiData = json["emoticons"_ls].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["emoticons"_ls] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["emoticons"_ls] = emojiData;
}
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
}
CustomEmojiModel::CustomEmojiModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!m_connection) {
return;
}
CustomEmojiModel::fetchEmojis();
connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
if (id != QStringLiteral("im.ponies.user_emotes")) {
return;
}
fetchEmojis();
});
});
CustomEmojiModel::fetchEmojis();
}
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
{
const auto row = idx.row();
if (row >= m_emojis.length()) {
return QVariant();
}
const auto &data = m_emojis[row];
switch (Roles(role)) {
case Roles::ModelData:
return QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(data.url)).toString(), data.name, true));
case Roles::Name:
case Roles::DisplayRole:
case Roles::ReplacedTextRole:
return data.name;
case Roles::ImageURL:
return m_connection->makeMediaUrl(QUrl(data.url));
case Roles::MxcUrl:
return m_connection->makeMediaUrl(QUrl(data.url));
default:
return {};
}
return QVariant();
}
int CustomEmojiModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_emojis.length();
}
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
{
return {
{Name, "name"},
{ImageURL, "imageURL"},
{ModelData, "modelData"},
{MxcUrl, "mxcUrl"},
};
}
QString CustomEmojiModel::preprocessText(QString text)
{
for (const auto &emoji : std::as_const(m_emojis)) {
text.replace(
emoji.regexp,
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
}
return text;
}
QVariantList CustomEmojiModel::filterModel(const QString &filter)
{
QVariantList results;
for (const auto &emoji : std::as_const(m_emojis)) {
if (results.length() >= 10)
break;
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
continue;
results << QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(emoji.url)).toString(), emoji.name, true));
}
return results;
}
#include "moc_customemojimodel.cpp"

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QRegularExpression>
#include "neochatconnection.h"
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
Q_PROPERTY(QString name MEMBER name)
};
/**
* @class CustomEmojiModel
*
* This class defines the model for custom user emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Name = Qt::DisplayRole, /**< The name of the emoji. */
ImageURL, /**< The URL for the custom emoji. */
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
};
Q_ENUM(Roles)
static CustomEmojiModel &instance()
{
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Substitute any custom emojis for an image in the input text.
*/
Q_INVOKABLE QString preprocessText(QString text);
/**
* @brief Return a list of custom emojis where the name contains the filter text.
*/
Q_INVOKABLE QVariantList filterModel(const QString &filter);
/**
* @brief Add a new emoji to the model.
*/
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
/**
* @brief Remove an emoji from the model.
*/
Q_INVOKABLE void removeEmoji(const QString &name);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
QPointer<NeoChatConnection> m_connection;
void fetchEmojis();
};

View File

@@ -1,242 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QVariant>
#include "emojimodel.h"
#include "emojitones.h"
#include <QDebug>
#include <algorithm>
#include "customemojimodel.h"
#include <KLocalizedString>
EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent)
, m_config(KSharedConfig::openStateConfig())
, m_configGroup(KConfigGroup(m_config, QStringLiteral("Editor")))
{
if (_emojis.isEmpty()) {
#include "emojis.h"
}
}
int EmojiModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
int total = 0;
for (const auto &category : std::as_const(_emojis)) {
total += category.count();
}
return total;
}
QVariant EmojiModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
for (const auto &category : std::as_const(_emojis)) {
if (row >= category.count()) {
row -= category.count();
continue;
}
auto emoji = category[row].value<Emoji>();
switch (role) {
case ShortNameRole:
return QStringLiteral(":%1:").arg(emoji.shortName);
case UnicodeRole:
case ReplacedTextRole:
return emoji.unicode;
case InvalidRole:
return QStringLiteral("invalid");
case DisplayRole:
return QStringLiteral("%2 :%1:").arg(emoji.shortName, emoji.unicode);
case DescriptionRole:
return emoji.description;
}
}
return {};
}
QHash<int, QByteArray> EmojiModel::roleNames() const
{
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
}
QStringList EmojiModel::lastUsedEmojis() const
{
return m_configGroup.readEntry(QStringLiteral("lastUsedEmojis"), QStringList());
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
{
auto emojis = CustomEmojiModel::instance().filterModel(filter);
emojis += filterModelNoCustom(filter, limit);
return emojis;
}
QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
{
QVariantList result;
const auto &values = _emojis.values();
for (const auto &e : values) {
for (const auto &variant : e) {
const auto &emoji = qvariant_cast<Emoji>(variant);
if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) {
result.append(variant);
if (result.length() > 10 && limit) {
return result;
}
}
}
}
return result;
}
void EmojiModel::emojiUsed(const QVariant &modelData)
{
auto list = lastUsedEmojis();
const auto emoji = modelData.value<Emoji>();
auto it = list.begin();
while (it != list.end()) {
if (*it == emoji.shortName) {
it = list.erase(it);
} else {
it++;
}
}
list.push_front(emoji.shortName);
m_configGroup.writeEntry(QStringLiteral("lastUsedEmojis"), list);
Q_EMIT historyChanged();
}
QVariantList EmojiModel::emojis(Category category) const
{
if (category == History) {
return emojiHistory();
}
if (category == HistoryNoCustom) {
QVariantList list;
const auto &history = emojiHistory();
for (const auto &e : history) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
}
}
return list;
}
if (category == Custom) {
return CustomEmojiModel::instance().filterModel({});
}
return _emojis[category];
}
QVariantList EmojiModel::tones(const QString &baseEmoji) const
{
if (baseEmoji.endsWith(QStringLiteral("tone"))) {
return EmojiTones::_tones.values(baseEmoji.split(QStringLiteral(":"))[0]);
}
return EmojiTones::_tones.values(baseEmoji);
}
QHash<EmojiModel::Category, QVariantList> EmojiModel::_emojis;
QVariantList EmojiModel::categories() const
{
return QVariantList{
{QVariantMap{
{QStringLiteral("category"), EmojiModel::HistoryNoCustom},
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Smileys},
{QStringLiteral("name"), i18nc("'Smileys' is a category of emoji", "Smileys")},
{QStringLiteral("emoji"), QStringLiteral("😏")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::People},
{QStringLiteral("name"), i18nc("'People' is a category of emoji", "People")},
{QStringLiteral("emoji"), QStringLiteral("🙋‍♂️")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Nature},
{QStringLiteral("name"), i18nc("'Nature' is a category of emoji", "Nature")},
{QStringLiteral("emoji"), QStringLiteral("🌲")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Food},
{QStringLiteral("name"), i18nc("'Food' is a category of emoji", "Food")},
{QStringLiteral("emoji"), QStringLiteral("🍛")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Activities},
{QStringLiteral("name"), i18nc("'Activities' is a category of emoji", "Activities")},
{QStringLiteral("emoji"), QStringLiteral("🚁")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Travel},
{QStringLiteral("name"), i18nc("'Travel' is a category of emoji", "Travel")},
{QStringLiteral("emoji"), QStringLiteral("🚅")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Objects},
{QStringLiteral("name"), i18nc("'Objects' is a category of emoji", "Objects")},
{QStringLiteral("emoji"), QStringLiteral("💡")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Symbols},
{QStringLiteral("name"), i18nc("'Symbols' is a category of emoji", "Symbols")},
{QStringLiteral("emoji"), QStringLiteral("🔣")},
}},
{QVariantMap{
{QStringLiteral("category"), EmojiModel::Flags},
{QStringLiteral("name"), i18nc("'Flags' is a category of emoji", "Flags")},
{QStringLiteral("emoji"), QStringLiteral("🏁")},
}},
};
}
QVariantList EmojiModel::categoriesWithCustom() const
{
auto cats = categories();
cats.removeAt(0);
cats.insert(0,
QVariantMap{
{QStringLiteral("category"), EmojiModel::History},
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
});
cats.insert(1,
QVariantMap{
{QStringLiteral("category"), EmojiModel::Custom},
{QStringLiteral("name"), i18nc("'Custom' is a category of emoji", "Custom")},
{QStringLiteral("emoji"), QStringLiteral("🖼️")},
});
;
return cats;
}
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
const auto &lastUsed = lastUsedEmojis();
for (const auto &historicEmoji : lastUsed) {
for (const auto &emojiCategory : std::as_const(_emojis)) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);
}
}
}
}
return list;
}
#include "moc_emojimodel.cpp"

View File

@@ -1,184 +0,0 @@
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
struct Emoji {
Emoji(QString unicode, QString shortname, bool isCustom = false)
: unicode(std::move(unicode))
, shortName(std::move(shortname))
, isCustom(isCustom)
{
}
Emoji(QString unicode, QString shortname, QString description)
: unicode(std::move(unicode))
, shortName(std::move(shortname))
, description(std::move(description))
{
}
Emoji() = default;
QString unicode;
QString shortName;
QString description;
bool isCustom = false;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER unicode)
Q_PROPERTY(QString shortName MEMBER shortName)
Q_PROPERTY(QString description MEMBER description)
Q_PROPERTY(bool isCustom MEMBER isCustom)
};
Q_DECLARE_METATYPE(Emoji)
/**
* @class EmojiModel
*
* This class defines the model for visualising a list of emojis.
*/
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of emoji categories.
*
* @note No custom emoji categories will be included.
*/
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
/**
* @brief Return a list of emoji categories with custom emojis.
*/
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
public:
static EmojiModel &instance()
{
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
UnicodeRole, /**< The unicode character of the emoji. */
InvalidRole = 50, /**< Invalid, reserved. */
DisplayRole = 51, /**< The display text for an emoji. */
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
DescriptionRole = 53, /**< The long description of an emoji. */
};
Q_ENUM(RoleNames)
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
Custom, /**< A custom user emoji. */
Search, /**< The results of a filter. */
SearchNoCustom, /**< The results of a filter with no custom emojis. */
History, /**< Recently used emojis. */
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
Smileys, /**< Smileys & emotion emojis. */
People, /**< People & Body emojis. */
Nature, /**< Animals & Nature emojis. */
Food, /**< Food & Drink emojis. */
Activities, /**< Activities emojis. */
Travel, /**< Travel & Places emojis. */
Objects, /**< Objects emojis. */
Symbols, /**< Symbols emojis. */
Flags, /**< Flags emojis. */
Component, /**< ??? */
};
Q_ENUM(Category)
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa RoleNames, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return a filtered list of emojis.
*
* @note This includes custom emojis, use filterModelNoCustom to return a result
* without custom emojis.
*
* @sa filterModelNoCustom
*/
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
/**
* @brief Return a filtered list of emojis without custom emojis.
*
* @note Use filterModel to return a result with custom emojis.
*
* @sa filterModel
*/
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
/**
* @brief Return a list of emojis for the given category.
*/
Q_INVOKABLE QVariantList emojis(Category category) const;
/**
* @brief Return a list of emoji tones for the given base emoji.
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
/**
* @brief Return a list of the last used emoji shortnames
*/
QStringList lastUsedEmojis() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;
Q_SIGNALS:
void historyChanged();
public Q_SLOTS:
void emojiUsed(const QVariant &modelData);
private:
static QHash<Category, QVariantList> _emojis;
/// Returns QVariants containing the last used Emojis
QVariantList emojiHistory() const;
KSharedConfig::Ptr m_config;
KConfigGroup m_configGroup;
EmojiModel(QObject *parent = nullptr);
};

View File

@@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "emoticonfiltermodel.h"
#include "accountemoticonmodel.h"
#include "stickermodel.h"
EmoticonFilterModel::EmoticonFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &EmoticonFilterModel::sourceModelChanged, this, [this]() {
if (dynamic_cast<StickerModel *>(sourceModel())) {
m_stickerRole = StickerModel::IsStickerRole;
m_emojiRole = StickerModel::IsEmojiRole;
} else {
m_stickerRole = AccountEmoticonModel::IsStickerRole;
m_emojiRole = AccountEmoticonModel::IsEmojiRole;
}
});
}
bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_stickerRole).toBool();
auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_emojiRole).toBool();
return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis);
}
bool EmoticonFilterModel::showStickers() const
{
return m_showStickers;
}
void EmoticonFilterModel::setShowStickers(bool showStickers)
{
beginResetModel();
m_showStickers = showStickers;
endResetModel();
Q_EMIT showStickersChanged();
}
bool EmoticonFilterModel::showEmojis() const
{
return m_showEmojis;
}
void EmoticonFilterModel::setShowEmojis(bool showEmojis)
{
beginResetModel();
m_showEmojis = showEmojis;
endResetModel();
Q_EMIT showEmojisChanged();
}
#include "moc_emoticonfiltermodel.cpp"

View File

@@ -1,55 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class EmoticonFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering a emoticon by type
* (Sticker or Emoji).
*/
class EmoticonFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether stickers should be shown
*/
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
/**
* @brief Whether emojis show be shown
*/
Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged)
public:
explicit EmoticonFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function checking the type of emoticon
*
* @note The filter cannot be modified and will always use the same filter properties.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
[[nodiscard]] bool showStickers() const;
void setShowStickers(bool showStickers);
[[nodiscard]] bool showEmojis() const;
void setShowEmojis(bool showEmojis);
Q_SIGNALS:
void showStickersChanged();
void showEmojisChanged();
private:
bool m_showStickers = false;
bool m_showEmojis = false;
int m_stickerRole = 0;
int m_emojiRole = 0;
};

View File

@@ -1,170 +0,0 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "imagepacksmodel.h"
#include "neochatroom.h"
#include <KLocalizedString>
using namespace Quotient;
ImagePacksModel::ImagePacksModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int ImagePacksModel::rowCount(const QModelIndex &index) const
{
Q_UNUSED(index);
return m_events.count();
}
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
{
const auto row = index.row();
if (row < 0 || row >= m_events.size()) {
return {};
}
const auto &event = m_events[row];
if (role == DisplayNameRole) {
if (event.pack->displayName) {
return *event.pack->displayName;
}
}
if (role == AvatarUrlRole) {
if (event.pack->avatarUrl) {
return m_room->connection()->makeMediaUrl(*event.pack->avatarUrl);
} else if (!event.images.empty()) {
return m_room->connection()->makeMediaUrl(event.images[0].url);
}
}
return {};
}
QHash<int, QByteArray> ImagePacksModel::roleNames() const
{
return {
{DisplayNameRole, "displayName"},
{AvatarUrlRole, "avatarUrl"},
{AttributionRole, "attribution"},
{IdRole, "id"},
};
}
NeoChatRoom *ImagePacksModel::room() const
{
return m_room;
}
void ImagePacksModel::setRoom(NeoChatRoom *room)
{
if (m_room) {
disconnect(m_room, nullptr, this, nullptr);
disconnect(m_room->connection(), nullptr, this, nullptr);
}
m_room = room;
if (m_room) {
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == "im.ponies.user_emotes"_ls) {
reloadImages();
}
});
}
// TODO listen to packs changing
reloadImages();
Q_EMIT roomChanged();
}
void ImagePacksModel::reloadImages()
{
if (!m_room) {
return;
}
beginResetModel();
m_events.clear();
// Load emoticons from the account data
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
json["pack"_ls] = QJsonObject{
{"display_name"_ls,
m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
};
const auto &content = ImagePackEventContent(json);
if (!content.images.isEmpty()) {
m_events += ImagePackEventContent(json);
}
}
// Load emoticons from the saved rooms
const auto &accountData = m_room->connection()->accountData("im.ponies.emote_rooms"_ls);
if (accountData) {
const auto &rooms = accountData->contentJson()["rooms"_ls].toObject();
for (const auto &roomId : rooms.keys()) {
if (roomId == m_room->id()) {
continue;
}
auto packs = rooms[roomId].toObject();
const auto &stickerRoom = m_room->connection()->room(roomId);
if (!stickerRoom) {
continue;
}
for (const auto &packKey : packs.keys()) {
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
const auto packContent = pack->content();
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers()))
&& !packContent.images.isEmpty()) {
m_events += packContent;
}
}
}
}
}
// Load emoticons from the current room
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
for (const auto &event : events) {
auto packContent = eventCast<const ImagePackEvent>(event)->content();
if (packContent.pack.has_value()) {
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers())) {
m_events += packContent;
}
}
}
Q_EMIT imagesLoaded();
endResetModel();
}
bool ImagePacksModel::showStickers() const
{
return m_showStickers;
}
void ImagePacksModel::setShowStickers(bool showStickers)
{
m_showStickers = showStickers;
Q_EMIT showStickersChanged();
}
bool ImagePacksModel::showEmoticons() const
{
return m_showEmoticons;
}
void ImagePacksModel::setShowEmoticons(bool showEmoticons)
{
m_showEmoticons = showEmoticons;
Q_EMIT showEmoticonsChanged();
}
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
{
if (index < 0 || index >= m_events.size()) {
return {};
}
return m_events[index].images;
}
#include "moc_imagepacksmodel.cpp"

View File

@@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QList>
#include <QPointer>
#include <QQmlEngine>
class NeoChatRoom;
/**
* @class ImagePacksModel
*
* Defines the model for visualising image packs.
*
* See Matrix MSC2545 for more details on image packs.
* https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
*/
class ImagePacksModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current room that the model is being used in.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief Whether sticker image packs should be shown.
*/
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
/**
* @brief Whether emoticon image packs should be shown.
*/
Q_PROPERTY(bool showEmoticons READ showEmoticons WRITE setShowEmoticons NOTIFY showEmoticonsChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the image pack. */
AvatarUrlRole, /**< The source mxc URL for the pack avatar. */
AttributionRole, /**< The attribution for the pack author(s). */
IdRole, /**< The ID of the image pack. */
};
Q_ENUM(Roles)
explicit ImagePacksModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] bool showStickers() const;
void setShowStickers(bool showStickers);
[[nodiscard]] bool showEmoticons() const;
void setShowEmoticons(bool showEmoticons);
/**
* @brief Return a vector of the images in the pack at the given index.
*/
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
Q_SIGNALS:
void roomChanged();
void showStickersChanged();
void showEmoticonsChanged();
void imagesLoaded();
private:
QPointer<NeoChatRoom> m_room;
QList<Quotient::ImagePackEventContent> m_events;
bool m_showStickers = true;
bool m_showEmoticons = true;
void reloadImages();
};

View File

@@ -305,7 +305,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes;
}
if (role == EventIdRole) {
return event.first->displayId();
return EventHandler::id(event.first);
}
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
@@ -348,9 +348,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == ReplyEventIdRole) {
if (const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first)) {
return roomMessageEvent->replyEventId();
}
return EventHandler::replyId(event.first);
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
@@ -458,8 +456,8 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
QList<MessageComponent> newComponents;
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
if (eventCast<const Quotient::RoomMessageEvent>(event.first)
&& eventCast<const Quotient::RoomMessageEvent>(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
@@ -484,7 +482,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {
if (isThreading && !EventHandler::isThreaded(event.first)) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
@@ -498,11 +496,7 @@ void MessageContentModel::updateReplyModel()
return;
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (roomMessageEvent == nullptr) {
return;
}
if (!roomMessageEvent->isReply() || (roomMessageEvent->isThreaded() && NeoChatConfig::self()->threads())) {
if (!EventHandler::hasReply(event.first) || (EventHandler::isThreaded(event.first) && NeoChatConfig::self()->threads())) {
if (m_replyModel) {
delete m_replyModel;
}
@@ -513,7 +507,7 @@ void MessageContentModel::updateReplyModel()
return;
}
m_replyModel = new MessageContentModel(m_room, roomMessageEvent->replyEventId(), true, false, this);
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event.first), true, false, this);
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
@@ -612,7 +606,6 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
}
}
}
[[fallthrough]];
default:
return {MessageComponent{type, QString(), {}}};
}

View File

@@ -501,8 +501,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return EventStatus::Hidden;
}
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt);
if (roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->threadRootEventId() != evt.id() && NeoChatConfig::threads()) {
if (EventHandler::isThreaded(&evt) && EventHandler::threadRoot(&evt) != EventHandler::id(&evt) && NeoChatConfig::threads()) {
return EventStatus::Hidden;
}
@@ -510,7 +509,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == EventIdRole) {
return evt.displayId();
return EventHandler::id(&evt);
}
if (role == ProgressInfoRole) {
@@ -535,18 +534,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == IsThreadedRole) {
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt)) {
return roomMessageEvent->isThreaded();
}
return {};
return EventHandler::isThreaded(&evt);
}
if (role == ThreadRootRole) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt);
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
return roomMessageEvent->threadRootEventId();
}
return {};
return EventHandler::threadRoot(&evt);
}
if (role == ShowSectionRole) {
@@ -662,10 +654,8 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, boo
}
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) {
m_threadModels[roomMessageEvent->threadRootEventId()] =
QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_currentRoom));
if (EventHandler::isThreaded(event) && !m_threadModels.contains(EventHandler::threadRoot(event))) {
m_threadModels[EventHandler::threadRoot(event)] = QSharedPointer<ThreadModel>(new ThreadModel(EventHandler::threadRoot(event), m_currentRoom));
}
// ReadMarkerModel handles updates to add and remove markers, we only need to

View File

@@ -8,23 +8,6 @@
using namespace Quotient;
class NeoChatQueryPublicRoomsJob : public QueryPublicRoomsJob
{
public:
explicit NeoChatQueryPublicRoomsJob(const QString &server = {},
std::optional<int> limit = std::nullopt,
const QString &since = {},
const std::optional<Filter> &filter = std::nullopt,
std::optional<bool> includeAllNetworks = std::nullopt,
const QString &thirdPartyInstanceId = {})
: QueryPublicRoomsJob(server, limit, since, filter, includeAllNetworks, thirdPartyInstanceId)
{
// TODO Remove once we can use libQuotient's job directly
// This is to make libQuotient happy about results not having the "chunk" field
setExpectedKeys({});
}
};
PublicRoomListModel::PublicRoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -170,8 +153,6 @@ void PublicRoomListModel::next(int limit)
if (m_connection == nullptr || limit < 1) {
return;
}
m_redirectedText.clear();
Q_EMIT redirectedChanged();
if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore";
@@ -182,7 +163,7 @@ void PublicRoomListModel::next(int limit)
if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space");
}
job = m_connection->callApi<NeoChatQueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] {
@@ -200,9 +181,6 @@ void PublicRoomListModel::next(int limit)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk());
this->endInsertRows();
} else if (job->error() == BaseJob::ContentAccessError) {
m_redirectedText = job->jsonData()[u"error"_s].toString();
Q_EMIT redirectedChanged();
}
this->job = nullptr;
@@ -324,9 +302,4 @@ bool PublicRoomListModel::searching() const
return job != nullptr;
}
QString PublicRoomListModel::redirectedText() const
{
return m_redirectedText;
}
#include "moc_publicroomlistmodel.cpp"

View File

@@ -52,11 +52,6 @@ class PublicRoomListModel : public QAbstractListModel
*/
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
/**
* @brief The text returned by the server after redirection
*/
Q_PROPERTY(QString redirectedText READ redirectedText NOTIFY redirectedChanged)
public:
/**
* @brief Defines the model roles.
@@ -118,8 +113,6 @@ public:
*/
Q_INVOKABLE void search(int limit = 50);
QString redirectedText() const;
private:
QPointer<NeoChatConnection> m_connection = nullptr;
QString m_server;
@@ -142,7 +135,6 @@ private:
QList<Quotient::PublicRoomsChunk> rooms;
Quotient::QueryPublicRoomsJob *job = nullptr;
QString m_redirectedText;
Q_SIGNALS:
void connectionChanged();
@@ -150,5 +142,4 @@ Q_SIGNALS:
void searchTextChanged();
void showOnlySpacesChanged();
void searchingChanged();
void redirectedChanged();
};

View File

@@ -129,7 +129,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
refresh(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refresh(room, {SubtitleTextRole});
refresh(room, {SubtitleTextRole, LastActiveTimeRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refresh(room, {SubtitleTextRole});
@@ -229,6 +229,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == HasHighlightNotificationsRole) {
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
}
if (role == LastActiveTimeRole) {
return room->lastActiveTime();
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
@@ -288,6 +291,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[CategoryRole] = "category";
roles[ContextNotificationCountRole] = "contextNotificationCount";
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[SubtitleTextRole] = "subtitleText";

View File

@@ -43,6 +43,7 @@ public:
CategoryRole, /**< The room category, e.g favourite. */
ContextNotificationCountRole, /**< The context aware notification count for the room. */
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
SubtitleTextRole, /**< The text to show as the room subtitle. */

View File

@@ -176,7 +176,7 @@ void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
refreshRoomRoles(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole});
refreshRoomRoles(room, {SubtitleTextRole, LastActiveTimeRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole});
@@ -274,6 +274,7 @@ QHash<int, QByteArray> RoomTreeModel::roleNames() const
roles[CategoryRole] = "category";
roles[ContextNotificationCountRole] = "contextNotificationCount";
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[SubtitleTextRole] = "subtitleText";
@@ -283,6 +284,8 @@ QHash<int, QByteArray> RoomTreeModel::roleNames() const
roles[IsDirectChat] = "isDirectChat";
roles[DelegateTypeRole] = "delegateType";
roles[IconRole] = "icon";
roles[AttentionRole] = "attention";
roles[FavouriteRole] = "favourite";
roles[RoomTypeRole] = "roomType";
return roles;
}
@@ -338,6 +341,9 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
if (role == HasHighlightNotificationsRole) {
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
}
if (role == LastActiveTimeRole) {
return room->lastActiveTime();
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
@@ -374,6 +380,12 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
if (role == DelegateTypeRole) {
return QStringLiteral("normal");
}
if (role == AttentionRole) {
return room->notificationCount() + room->highlightCount() > 0;
}
if (role == FavouriteRole) {
return room->isFavourite();
}
if (role == RoomTypeRole) {
if (room->creation()) {
return room->creation()->contentPart<QString>("type"_L1);

View File

@@ -36,6 +36,7 @@ public:
CategoryRole, /**< The room category, e.g favourite. */
ContextNotificationCountRole, /**< The context aware notification count for the room. */
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
@@ -47,6 +48,8 @@ public:
IsDirectChat, /**< Whether this room is a direct chat. */
DelegateTypeRole,
IconRole,
AttentionRole, /**< Whether there are any notifications. */
FavouriteRole, /**< Whether the room is favourited. */
RoomTypeRole, /**< The room's type. */
};
Q_ENUM(EventRoles)

View File

@@ -108,17 +108,11 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
case HighlightRole:
return EventHandler::isHighlighted(m_room, &event);
case EventIdRole:
return event.displayId();
return EventHandler::id(&event);
case IsThreadedRole:
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event)) {
return roomMessageEvent->isThreaded();
}
return {};
return EventHandler::isThreaded(&event);
case ThreadRootRole:
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event); roomMessageEvent->isThreaded()) {
return roomMessageEvent->threadRootEventId();
}
return {};
return EventHandler::threadRoot(&event);
case ContentModelRole: {
if (!event.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, event.id()));

View File

@@ -6,7 +6,6 @@
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "neochatroomtype.h"
#include "roommanager.h"
#include "roomtreemodel.h"
@@ -45,49 +44,58 @@ SortFilterRoomTreeModel::SortFilterRoomTreeModel(RoomTreeModel *sourceModel, QOb
void SortFilterRoomTreeModel::setRoomSortOrder(SortFilterRoomTreeModel::RoomSortOrder sortOrder)
{
m_sortOrder = sortOrder;
if (sortOrder == SortFilterRoomTreeModel::Alphabetical) {
setSortRole(RoomTreeModel::DisplayNameRole);
} else if (sortOrder == SortFilterRoomTreeModel::Activity) {
setSortRole(RoomTreeModel::LastActiveTimeRole);
}
invalidate();
}
static const QVector<RoomSortParameter::Parameter> alphabeticalSortPriorities{
static const QVector<RoomTreeModel::EventRoles> alphabeticalSortPriorities{
// Does exactly what it says on the tin.
RoomSortParameter::AlphabeticalAscending,
RoomTreeModel::DisplayNameRole,
};
static const QVector<RoomSortParameter::Parameter> activitySortPriorities{
RoomSortParameter::HasHighlight,
RoomSortParameter::MostHighlights,
RoomSortParameter::HasUnread,
RoomSortParameter::MostUnread,
RoomSortParameter::LastActive,
static const QVector<RoomTreeModel::EventRoles> activitySortPriorities{
// Anything useful at the top, quiet rooms at the bottom
RoomTreeModel::AttentionRole,
// Organize by highlights, notifications, unread favorites, all other unread, in that order
RoomTreeModel::HasHighlightNotificationsRole,
RoomTreeModel::ContextNotificationCountRole,
RoomTreeModel::FavouriteRole,
// Finally sort by last activity time
RoomTreeModel::LastActiveTimeRole,
};
static const QVector<RoomSortParameter::Parameter> lastMessageSortPriorities{
RoomSortParameter::LastActive,
};
bool SortFilterRoomTreeModel::roleCmp(const QVariant &sortLeft, const QVariant &sortRight) const
{
switch (sortLeft.typeId()) {
case QMetaType::Bool:
return (sortLeft == sortRight) ? false : sortLeft.toBool();
case QMetaType::QString:
return sortLeft.toString() < sortRight.toString();
case QMetaType::Int:
return sortLeft.toInt() > sortRight.toInt();
case QMetaType::QDateTime:
return sortLeft.toDateTime() > sortRight.toDateTime();
default:
return false;
}
}
bool SortFilterRoomTreeModel::prioritiesCmp(const QVector<RoomSortParameter::Parameter> &priorities,
bool SortFilterRoomTreeModel::prioritiesCmp(const QVector<RoomTreeModel::EventRoles> &priorities,
const QModelIndex &source_left,
const QModelIndex &source_right) const
{
const auto treeModel = dynamic_cast<RoomTreeModel *>(sourceModel());
if (treeModel == nullptr) {
return false;
}
const auto leftRoom = dynamic_cast<NeoChatRoom *>(treeModel->connection()->room(source_left.data(RoomTreeModel::RoomIdRole).toString()));
const auto rightRoom = dynamic_cast<NeoChatRoom *>(treeModel->connection()->room(source_right.data(RoomTreeModel::RoomIdRole).toString()));
if (leftRoom == nullptr || rightRoom == nullptr) {
return false;
}
for (auto sortRole : priorities) {
auto result = RoomSortParameter::compareParameter(sortRole, leftRoom, rightRoom);
if (result != 0) {
return result > 0;
for (RoomTreeModel::EventRoles sortRole : priorities) {
const auto sortLeft = sourceModel()->data(source_left, sortRole);
const auto sortRight = sourceModel()->data(source_right, sortRole);
if (sortLeft != sortRight) {
return roleCmp(sortLeft, sortRight);
}
}
return false;
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
@@ -102,9 +110,8 @@ bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QMo
return prioritiesCmp(alphabeticalSortPriorities, source_left, source_right);
case SortFilterRoomTreeModel::Activity:
return prioritiesCmp(activitySortPriorities, source_left, source_right);
case SortFilterRoomTreeModel::LastMessage:
return prioritiesCmp(lastMessageSortPriorities, source_left, source_right);
}
return QSortFilterProxyModel::lessThan(source_left, source_right);
}

View File

@@ -7,7 +7,6 @@
#include <QQmlEngine>
#include <QSortFilterProxyModel>
#include "enums/roomsortparameter.h"
#include "models/roomtreemodel.h"
/**
@@ -54,7 +53,6 @@ public:
enum RoomSortOrder {
Alphabetical,
Activity,
LastMessage,
};
Q_ENUM(RoomSortOrder)
@@ -106,5 +104,6 @@ private:
QString m_filterText;
QString m_activeSpaceId;
bool prioritiesCmp(const QVector<RoomSortParameter::Parameter> &priorities, const QModelIndex &left, const QModelIndex &right) const;
bool roleCmp(const QVariant &left, const QVariant &right) const;
bool prioritiesCmp(const QVector<RoomTreeModel::EventRoles> &priorities, const QModelIndex &left, const QModelIndex &right) const;
};

View File

@@ -25,7 +25,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
connect(room, &Quotient::Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (roomEvent->isThreaded() && roomEvent->threadRootEventId() == m_threadRootId) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(event);
addModels();
}
@@ -34,7 +34,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
connect(room, &Quotient::Room::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
for (const auto &event : events) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (roomEvent->isThreaded() && roomEvent->threadRootEventId() == m_threadRootId) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(roomEvent);
}
}

Some files were not shown because too many files have changed in this diff Show More