Compare commits
74 Commits
work/tobia
...
work/carl/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab2a81aee | ||
|
|
4daff26747 | ||
|
|
fe09dc23a6 | ||
|
|
9cd4a7416e | ||
|
|
f6e3210b0d | ||
|
|
16d33eb02c | ||
|
|
326512697c | ||
|
|
f25de891bf | ||
|
|
e785533858 | ||
|
|
0699bc4147 | ||
|
|
d07a8258e9 | ||
|
|
d490e65315 | ||
|
|
7a632c9561 | ||
|
|
ec4b35fa5f | ||
|
|
b263755629 | ||
|
|
828585a260 | ||
|
|
3b33121dbc | ||
|
|
21484c8184 | ||
|
|
020385c850 | ||
|
|
c585f3d8ae | ||
|
|
3356e6c6cf | ||
|
|
2fa6ad22a3 | ||
|
|
b887519f26 | ||
|
|
b1e54a834c | ||
|
|
171e62a272 | ||
|
|
053770c117 | ||
|
|
911f3e1f54 | ||
|
|
1494ba95b3 | ||
|
|
c0b00ce146 | ||
|
|
47a08b829e | ||
|
|
953bb60aac | ||
|
|
fc4cb31277 | ||
|
|
9876636dbc | ||
|
|
8314ab03bd | ||
|
|
9d887ba3e7 | ||
|
|
1612a8a960 | ||
|
|
99af210e62 | ||
|
|
8fd108cde1 | ||
|
|
ca81d35936 | ||
|
|
d65aacac6f | ||
|
|
43f052a363 | ||
|
|
0e0a38ffa2 | ||
|
|
819586fc4e | ||
|
|
6b8a331428 | ||
|
|
57e7004e05 | ||
|
|
25c95cafe3 | ||
|
|
54be1a8918 | ||
|
|
d4a0573051 | ||
|
|
4a96eac67b | ||
|
|
2f4134a6d2 | ||
|
|
4f87dcc0c0 | ||
|
|
47eba6b720 | ||
|
|
cba537d561 | ||
|
|
c9d03cb042 | ||
|
|
e42c002fbd | ||
|
|
7c7b073a47 | ||
|
|
20090d21eb | ||
|
|
b0e69ff4b8 | ||
|
|
552bb0a98b | ||
|
|
19510858af | ||
|
|
44b2f6ee63 | ||
|
|
c5d3002f31 | ||
|
|
2e3659d4ee | ||
|
|
f8a5509a91 | ||
|
|
421f436871 | ||
|
|
a37c9d6cea | ||
|
|
d14d576d99 | ||
|
|
9391e44e4b | ||
|
|
a7aebe3a61 | ||
|
|
1fca9021a4 | ||
|
|
a39194b2ad | ||
|
|
bff93d9352 | ||
|
|
d76c9cd16d | ||
|
|
14774fe235 |
55
.reuse/dep5
55
.reuse/dep5
@@ -1,55 +0,0 @@
|
||||
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
|
||||
84
REUSE.toml
Normal file
84
REUSE.toml
Normal file
@@ -0,0 +1,84 @@
|
||||
# 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"
|
||||
BIN
android/neochat-playstore.png
Normal file
BIN
android/neochat-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -83,6 +83,15 @@ 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__":
|
||||
|
||||
@@ -32,8 +32,6 @@ private:
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void eventId();
|
||||
void nullEventId();
|
||||
void authorDisplayName();
|
||||
void nullAuthorDisplayName();
|
||||
void singleLineSidplayName();
|
||||
@@ -56,14 +54,8 @@ 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();
|
||||
};
|
||||
@@ -74,17 +66,6 @@ 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"));
|
||||
@@ -118,8 +99,9 @@ void EventHandlerTest::time()
|
||||
{
|
||||
const auto event = room->messageEvents().at(0).get();
|
||||
|
||||
QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
|
||||
QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
|
||||
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)));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullTime()
|
||||
@@ -138,19 +120,19 @@ void EventHandlerTest::timeString()
|
||||
KFormat format;
|
||||
|
||||
QCOMPARE(EventHandler::timeString(event, false),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(EventHandler::timeString(event, true),
|
||||
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));
|
||||
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));
|
||||
QCOMPARE(EventHandler::timeString(event, QStringLiteral("hh:mm")),
|
||||
QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
|
||||
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(QStringLiteral("hh:mm")));
|
||||
}
|
||||
|
||||
void EventHandlerTest::highlighted()
|
||||
@@ -295,30 +277,6 @@ 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();
|
||||
@@ -344,29 +302,6 @@ 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());
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <qnamespace.h>
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
#include "testutils.h"
|
||||
@@ -76,6 +77,7 @@ 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"));
|
||||
}
|
||||
|
||||
@@ -54,19 +54,24 @@
|
||||
<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>
|
||||
@@ -288,7 +293,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</value>
|
||||
<value key="KDE::supporters">Tanguy Fardet;[dabe](https://freeradical.zone/@dabe);[lengau](https://mastodon.world/@lengau);Joshua Strobl;Stuart Turton</value>
|
||||
</custom>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
@@ -443,6 +448,7 @@
|
||||
<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"/>
|
||||
|
||||
@@ -88,24 +88,31 @@ 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
|
||||
|
||||
933
po/ar/neochat.po
933
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
917
po/az/neochat.po
917
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
843
po/ca/neochat.po
843
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
985
po/cs/neochat.po
985
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
907
po/da/neochat.po
907
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1345
po/de/neochat.po
1345
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
943
po/el/neochat.po
943
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1039
po/en_GB/neochat.po
1039
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
991
po/eo/neochat.po
991
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
891
po/es/neochat.po
891
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
929
po/eu/neochat.po
929
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
2325
po/fi/neochat.po
2325
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
907
po/fr/neochat.po
907
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
903
po/gl/neochat.po
903
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
995
po/hu/neochat.po
995
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1037
po/ia/neochat.po
1037
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
942
po/id/neochat.po
942
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
904
po/ie/neochat.po
904
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1035
po/it/neochat.po
1035
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
805
po/ja/neochat.po
805
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
844
po/ka/neochat.po
844
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
967
po/ko/neochat.po
967
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
805
po/lt/neochat.po
805
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1062
po/lv/neochat.po
1062
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
847
po/nl/neochat.po
847
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
994
po/nn/neochat.po
994
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
894
po/pa/neochat.po
894
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
993
po/pl/neochat.po
993
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
942
po/pt/neochat.po
942
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
965
po/ru/neochat.po
965
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
901
po/sk/neochat.po
901
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
847
po/sl/neochat.po
847
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
928
po/sv/neochat.po
928
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
893
po/ta/neochat.po
893
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
903
po/tr/neochat.po
903
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
853
po/uk/neochat.po
853
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1060
po/zh_TW/neochat.po
1060
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ parts:
|
||||
- olm
|
||||
- qtkeychain
|
||||
source: https://github.com/quotient-im/libQuotient.git
|
||||
source-tag: 0.9.0
|
||||
source-tag: 0.9.1
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
build-packages:
|
||||
|
||||
@@ -10,6 +10,12 @@ 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
|
||||
@@ -20,6 +26,8 @@ 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
|
||||
@@ -42,6 +50,8 @@ 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
|
||||
@@ -91,6 +101,10 @@ 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
|
||||
@@ -180,12 +194,20 @@ 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
|
||||
@@ -268,9 +290,6 @@ 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
|
||||
@@ -300,13 +319,9 @@ 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)
|
||||
@@ -520,6 +535,7 @@ if(ANDROID)
|
||||
"kde"
|
||||
"list-remove-symbolic"
|
||||
"edit-delete"
|
||||
"user-home-symbolic"
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
|
||||
@@ -519,6 +519,7 @@ QQC2.Control {
|
||||
y: -implicitHeight
|
||||
|
||||
modal: false
|
||||
includeCustom: true
|
||||
closeOnChosen: false
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
@@ -5,30 +5,59 @@ import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
QQC2.Button {
|
||||
QQC2.ItemDelegate {
|
||||
id: root
|
||||
|
||||
required property string toolTip
|
||||
property string name
|
||||
property string emoji
|
||||
property bool showTones: false
|
||||
property bool isImage: false
|
||||
|
||||
QQC2.ToolTip.text: toolTip
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: root.name
|
||||
QQC2.ToolTip.visible: hovered && root.name !== ""
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
leftInset: Kirigami.Units.smallSpacing
|
||||
topInset: Kirigami.Units.smallSpacing
|
||||
rightInset: Kirigami.Units.smallSpacing
|
||||
bottomInset: Kirigami.Units.smallSpacing
|
||||
|
||||
flat: true
|
||||
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"
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
text: root.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ QQC2.Popup {
|
||||
*/
|
||||
property NeoChatRoom currentRoom
|
||||
|
||||
property bool includeCustom: false
|
||||
property bool closeOnChosen: true
|
||||
property bool showQuickReaction: false
|
||||
|
||||
signal chosen(string emoji)
|
||||
|
||||
@@ -62,15 +64,15 @@ QQC2.Popup {
|
||||
padding: 2
|
||||
|
||||
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
|
||||
width: Math.min(contentItem.implicitWidth + 2 * padding, applicationWindow().width)
|
||||
|
||||
width: Math.min(contentItem.categoryIconSize * 11 + 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();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella
|
||||
// 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
|
||||
|
||||
readonly property int emojisPerRow: emojis.width / Kirigami.Units.iconSizes.large
|
||||
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
|
||||
required property QtObject header
|
||||
property bool stickers: false
|
||||
|
||||
@@ -22,8 +25,6 @@ QQC2.ScrollView {
|
||||
emojis.forceActiveFocus();
|
||||
}
|
||||
|
||||
width: Kirigami.Units.gridUnit * 24
|
||||
|
||||
GridView {
|
||||
id: emojis
|
||||
|
||||
@@ -40,9 +41,7 @@ QQC2.ScrollView {
|
||||
onModelChanged: currentIndex = -1
|
||||
|
||||
cellWidth: emojis.width / root.emojisPerRow
|
||||
cellHeight: Kirigami.Units.iconSizes.large
|
||||
|
||||
model: EmojiModelManager.emojiModel
|
||||
cellHeight: root.targetIconSize
|
||||
|
||||
KeyNavigation.up: root.header
|
||||
|
||||
@@ -50,49 +49,50 @@ QQC2.ScrollView {
|
||||
|
||||
delegate: EmojiDelegate {
|
||||
id: emojiDelegate
|
||||
|
||||
required property string unicode
|
||||
required property string identifier
|
||||
required property int index
|
||||
|
||||
text: emojiDelegate.unicode
|
||||
toolTip: emojiDelegate.identifier
|
||||
checked: emojis.currentIndex === emojiDelegate.index
|
||||
checked: emojis.currentIndex === model.index
|
||||
emoji: !!modelData ? modelData.unicode : model.url
|
||||
name: !!modelData ? modelData.shortName : model.body
|
||||
|
||||
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 (!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
|
||||
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
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
|
||||
text: root.stickers ? i18nc("@info", "No stickers") : i18nc("@info", "No emojis")
|
||||
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
|
||||
visible: emojis.count === 0
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: tonesPopupComponent
|
||||
EmojiTonesPicker {
|
||||
onChosen: root.chosen(emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -14,29 +13,87 @@ ColumnLayout {
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom currentRoom
|
||||
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
|
||||
|
||||
signal chosen(string emoji)
|
||||
|
||||
spacing: 0
|
||||
|
||||
onActiveFocusChanged: if (activeFocus) {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
EmojiPickerTypeHeader {
|
||||
id: emoticonPickerTypeHeader
|
||||
spacing: 0
|
||||
|
||||
Kirigami.NavigationTabBar {
|
||||
id: types
|
||||
Layout.fillWidth: true
|
||||
onSelectedTypeChanged: emoticonPickerCategoryHeader.currentIndex = 0
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
EmojiPickerPackHeader {
|
||||
id: emoticonPickerCategoryHeader
|
||||
|
||||
QQC2.ScrollView {
|
||||
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
|
||||
|
||||
model: UnicodeEmoticonManager.categories
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
@@ -48,34 +105,119 @@ 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: emoticonPickerCategoryHeader
|
||||
header: categories
|
||||
Keys.forwardTo: searchField
|
||||
stickers: emoticonPickerTypeHeader.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
|
||||
stickers: root.selectedType === 1
|
||||
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
visible: showQuickReaction
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
}
|
||||
|
||||
QuickReaction {
|
||||
id: quickReaction
|
||||
onChosen: root.chosen(text)
|
||||
QQC2.ScrollView {
|
||||
visible: showQuickReaction
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2022 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
|
||||
text: modelData.unicode
|
||||
toolTip: modelData.shortName
|
||||
emoji: modelData.unicode
|
||||
name: modelData.shortName
|
||||
|
||||
width: root.categoryIconSize
|
||||
height: width
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
ChatBarCache::ChatBarCache(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -319,7 +321,25 @@ void ChatBarCache::postMessage()
|
||||
return;
|
||||
}
|
||||
|
||||
room->postMessage(text(), sendText, *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), replyId(), editId(), threadId());
|
||||
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);
|
||||
clearCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
ColorSchemer::ColorSchemer(QObject *parent)
|
||||
: QObject(parent)
|
||||
, c(new KColorSchemeManager(this))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -18,17 +17,17 @@ ColorSchemer::~ColorSchemer()
|
||||
|
||||
QAbstractItemModel *ColorSchemer::model() const
|
||||
{
|
||||
return c->model();
|
||||
return KColorSchemeManager::instance()->model();
|
||||
}
|
||||
|
||||
void ColorSchemer::apply(int idx)
|
||||
{
|
||||
c->activateScheme(c->model()->index(idx, 0));
|
||||
KColorSchemeManager::instance()->activateScheme(KColorSchemeManager::instance()->model()->index(idx, 0));
|
||||
}
|
||||
|
||||
int ColorSchemer::indexForCurrentScheme()
|
||||
{
|
||||
return c->indexForSchemeId(c->activeSchemeId()).row();
|
||||
return KColorSchemeManager::instance()->indexForSchemeId(KColorSchemeManager::instance()->activeSchemeId()).row();
|
||||
}
|
||||
|
||||
#include "moc_colorschemer.cpp"
|
||||
|
||||
@@ -49,7 +49,4 @@ public:
|
||||
* @sa KColorScheme
|
||||
*/
|
||||
Q_INVOKABLE int indexForCurrentScheme();
|
||||
|
||||
private:
|
||||
KColorSchemeManager *c;
|
||||
};
|
||||
|
||||
@@ -423,10 +423,14 @@ void Controller::setTestMode(bool test)
|
||||
|
||||
void Controller::removeConnection(const QString &userId)
|
||||
{
|
||||
if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) {
|
||||
auto connection = m_connectionsLoading[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];
|
||||
SettingsGroup("Accounts"_ls).remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,28 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,22 @@ ColumnLayout {
|
||||
title: i18nc("@title", "Choose Room")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
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.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.FormTextDelegate {
|
||||
visible: root.room
|
||||
text: i18n("Room Id: %1", root.room.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
function openEventSource(stateKey: string): void {
|
||||
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||
model: stateKeysModel,
|
||||
allowEdit: true,
|
||||
room: root.room,
|
||||
|
||||
1857
src/emojis.h
Normal file
1857
src/emojis.h
Normal file
File diff suppressed because it is too large
Load Diff
9
src/emojitones.cpp
Normal file
9
src/emojitones.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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"
|
||||
};
|
||||
21
src/emojitones.h
Normal file
21
src/emojitones.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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;
|
||||
};
|
||||
1784
src/emojitones_data.h
Normal file
1784
src/emojitones_data.h
Normal file
File diff suppressed because it is too large
Load Diff
85
src/enums/roomsortparameter.cpp
Normal file
85
src/enums/roomsortparameter.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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());
|
||||
}
|
||||
122
src/enums/roomsortparameter.h
Normal file
122
src/enums/roomsortparameter.h
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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);
|
||||
@@ -49,16 +49,6 @@ 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) {
|
||||
@@ -70,7 +60,7 @@ QString EventHandler::authorDisplayName(const NeoChatRoom *room, const Quotient:
|
||||
return {};
|
||||
}
|
||||
|
||||
if (is<RoomMemberEvent>(*event) && !event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
|
||||
if (is<RoomMemberEvent>(*event) && event->unsignedJson()[QStringLiteral("prev_content")].toObject().contains("displayname"_L1)
|
||||
&& event->stateKey() == event->senderId()) {
|
||||
auto previousDisplayName = event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
|
||||
if (previousDisplayName.isEmpty()) {
|
||||
@@ -291,7 +281,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()) {
|
||||
if (event->isRedacted() && !event->isStateEvent()) {
|
||||
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());
|
||||
}
|
||||
@@ -508,7 +498,7 @@ QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomE
|
||||
qCWarning(EventHandling) << "genericBody called with event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (event->isRedacted()) {
|
||||
if (event->isRedacted() && !event->isStateEvent()) {
|
||||
return i18n("<i>[This message was deleted]</i>");
|
||||
}
|
||||
|
||||
@@ -834,31 +824,6 @@ 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) {
|
||||
@@ -877,38 +842,6 @@ 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) {
|
||||
|
||||
@@ -37,14 +37,6 @@ 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.
|
||||
*
|
||||
@@ -220,20 +212,6 @@ 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.
|
||||
*
|
||||
@@ -249,20 +227,6 @@ 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.
|
||||
*
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
#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;
|
||||
@@ -61,7 +62,13 @@ void LinkPreviewer::loadUrlPreview()
|
||||
if (conn == nullptr) {
|
||||
return;
|
||||
}
|
||||
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url);
|
||||
|
||||
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);)
|
||||
}
|
||||
|
||||
connect(job, &BaseJob::success, this, [this, job, conn]() {
|
||||
const auto json = job->jsonData();
|
||||
|
||||
@@ -25,8 +25,7 @@ void LoginHelper::init()
|
||||
m_connection = new NeoChatConnection();
|
||||
m_matrixId = QString();
|
||||
m_password = QString();
|
||||
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
|
||||
.arg(QSysInfo::machineHostName(), QSysInfo::productType(), QSysInfo::productVersion(), QSysInfo::currentCpuArchitecture());
|
||||
m_deviceName = QStringLiteral("NeoChat");
|
||||
m_supportsSso = false;
|
||||
m_supportsPassword = false;
|
||||
m_ssoUrl = QUrl();
|
||||
|
||||
@@ -271,7 +271,6 @@ 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)) {
|
||||
|
||||
222
src/models/accountemoticonmodel.cpp
Normal file
222
src/models/accountemoticonmodel.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
// 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"
|
||||
104
src/models/accountemoticonmodel.h
Normal file
104
src/models/accountemoticonmodel.h
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -109,11 +109,10 @@ 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.
|
||||
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||
rainbowText,
|
||||
RoomMessageEvent::MsgType::Text,
|
||||
chatBarCache->replyId(),
|
||||
chatBarCache->editId());
|
||||
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);
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -129,11 +128,10 @@ 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.
|
||||
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||
rainbowText,
|
||||
RoomMessageEvent::MsgType::Emote,
|
||||
chatBarCache->replyId(),
|
||||
chatBarCache->editId());
|
||||
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);
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -144,7 +142,7 @@ QList<ActionsModel::Action> actions{
|
||||
Action{
|
||||
QStringLiteral("plain"),
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {});
|
||||
room->postPlainText(text.toHtmlEscaped());
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -156,11 +154,10 @@ 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.
|
||||
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
|
||||
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
|
||||
RoomMessageEvent::MsgType::Text,
|
||||
chatBarCache->replyId(),
|
||||
chatBarCache->editId());
|
||||
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);
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -605,15 +602,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) {
|
||||
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, replaceId);
|
||||
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
|
||||
} else {
|
||||
room->postHtmlMessage(messageText,
|
||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||
event->msgtype(),
|
||||
{},
|
||||
replaceId);
|
||||
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
|
||||
}
|
||||
Quotient::EventRelation relatesTo = Quotient::EventRelation::replace(replaceId);
|
||||
room->post<Quotient::RoomMessageEvent>(messageText, event->msgtype(), std::move(content), relatesTo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
#include "actionsmodel.h"
|
||||
#include "completionproxymodel.h"
|
||||
// #include "emojimodel.h"
|
||||
#include "customemojimodel.h"
|
||||
#include "emojimodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "userlistmodel.h"
|
||||
@@ -15,13 +16,11 @@ 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);
|
||||
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
||||
m_userListModel->setRoom(m_room);
|
||||
});
|
||||
// TODO m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
||||
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||
}
|
||||
|
||||
QString CompletionModel::text() const
|
||||
@@ -89,20 +88,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) {
|
||||
// // TODO 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) {
|
||||
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -148,8 +147,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);
|
||||
// TODO m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
||||
m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
||||
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
||||
m_filterModel->setFullText(m_fullText);
|
||||
m_filterModel->setFilterText(m_text);
|
||||
m_filterModel->invalidate();
|
||||
|
||||
213
src/models/customemojimodel.cpp
Normal file
213
src/models/customemojimodel.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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"
|
||||
116
src/models/customemojimodel.h
Normal file
116
src/models/customemojimodel.h
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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();
|
||||
};
|
||||
242
src/models/emojimodel.cpp
Normal file
242
src/models/emojimodel.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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"
|
||||
184
src/models/emojimodel.h
Normal file
184
src/models/emojimodel.h
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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);
|
||||
};
|
||||
57
src/models/emoticonfiltermodel.cpp
Normal file
57
src/models/emoticonfiltermodel.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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"
|
||||
55
src/models/emoticonfiltermodel.h
Normal file
55
src/models/emoticonfiltermodel.h
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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;
|
||||
};
|
||||
170
src/models/imagepacksmodel.cpp
Normal file
170
src/models/imagepacksmodel.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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"
|
||||
103
src/models/imagepacksmodel.h
Normal file
103
src/models/imagepacksmodel.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -305,7 +305,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return component.attributes;
|
||||
}
|
||||
if (role == EventIdRole) {
|
||||
return EventHandler::id(event.first);
|
||||
return event.first->displayId();
|
||||
}
|
||||
if (role == TimeRole) {
|
||||
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
|
||||
@@ -348,7 +348,9 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
|
||||
}
|
||||
if (role == ReplyEventIdRole) {
|
||||
return EventHandler::replyId(event.first);
|
||||
if (const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first)) {
|
||||
return roomMessageEvent->replyEventId();
|
||||
}
|
||||
}
|
||||
if (role == ReplyAuthorRole) {
|
||||
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
|
||||
@@ -456,8 +458,8 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
||||
|
||||
QList<MessageComponent> newComponents;
|
||||
|
||||
if (eventCast<const Quotient::RoomMessageEvent>(event.first)
|
||||
&& eventCast<const Quotient::RoomMessageEvent>(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
return newComponents;
|
||||
}
|
||||
@@ -482,7 +484,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
||||
}
|
||||
|
||||
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
|
||||
if (isThreading && !EventHandler::isThreaded(event.first)) {
|
||||
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||
}
|
||||
|
||||
@@ -496,7 +498,11 @@ void MessageContentModel::updateReplyModel()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EventHandler::hasReply(event.first) || (EventHandler::isThreaded(event.first) && NeoChatConfig::self()->threads())) {
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
if (roomMessageEvent == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!roomMessageEvent->isReply() || (roomMessageEvent->isThreaded() && NeoChatConfig::self()->threads())) {
|
||||
if (m_replyModel) {
|
||||
delete m_replyModel;
|
||||
}
|
||||
@@ -507,7 +513,7 @@ void MessageContentModel::updateReplyModel()
|
||||
return;
|
||||
}
|
||||
|
||||
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event.first), true, false, this);
|
||||
m_replyModel = new MessageContentModel(m_room, roomMessageEvent->replyEventId(), true, false, this);
|
||||
|
||||
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
|
||||
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
|
||||
@@ -606,6 +612,7 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
}
|
||||
}
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
return {MessageComponent{type, QString(), {}}};
|
||||
}
|
||||
|
||||
@@ -501,7 +501,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (EventHandler::isThreaded(&evt) && EventHandler::threadRoot(&evt) != EventHandler::id(&evt) && NeoChatConfig::threads()) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt);
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->threadRootEventId() != evt.id() && NeoChatConfig::threads()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
@@ -509,7 +510,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == EventIdRole) {
|
||||
return EventHandler::id(&evt);
|
||||
return evt.displayId();
|
||||
}
|
||||
|
||||
if (role == ProgressInfoRole) {
|
||||
@@ -534,11 +535,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == IsThreadedRole) {
|
||||
return EventHandler::isThreaded(&evt);
|
||||
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
return roomMessageEvent->isThreaded();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == ThreadRootRole) {
|
||||
return EventHandler::threadRoot(&evt);
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&evt);
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
return roomMessageEvent->threadRootEventId();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == ShowSectionRole) {
|
||||
@@ -654,8 +662,10 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, boo
|
||||
}
|
||||
}
|
||||
|
||||
if (EventHandler::isThreaded(event) && !m_threadModels.contains(EventHandler::threadRoot(event))) {
|
||||
m_threadModels[EventHandler::threadRoot(event)] = QSharedPointer<ThreadModel>(new ThreadModel(EventHandler::threadRoot(event), m_currentRoom));
|
||||
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));
|
||||
}
|
||||
|
||||
// ReadMarkerModel handles updates to add and remove markers, we only need to
|
||||
|
||||
@@ -8,6 +8,23 @@
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -153,6 +170,8 @@ 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";
|
||||
@@ -163,7 +182,7 @@ void PublicRoomListModel::next(int limit)
|
||||
if (m_showOnlySpaces) {
|
||||
roomTypes += QLatin1String("m.space");
|
||||
}
|
||||
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
|
||||
job = m_connection->callApi<NeoChatQueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
|
||||
Q_EMIT searchingChanged();
|
||||
|
||||
connect(job, &BaseJob::finished, this, [this] {
|
||||
@@ -181,6 +200,9 @@ 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;
|
||||
@@ -302,4 +324,9 @@ bool PublicRoomListModel::searching() const
|
||||
return job != nullptr;
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::redirectedText() const
|
||||
{
|
||||
return m_redirectedText;
|
||||
}
|
||||
|
||||
#include "moc_publicroomlistmodel.cpp"
|
||||
|
||||
@@ -52,6 +52,11 @@ 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.
|
||||
@@ -113,6 +118,8 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void search(int limit = 50);
|
||||
|
||||
QString redirectedText() const;
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection = nullptr;
|
||||
QString m_server;
|
||||
@@ -135,6 +142,7 @@ private:
|
||||
QList<Quotient::PublicRoomsChunk> rooms;
|
||||
|
||||
Quotient::QueryPublicRoomsJob *job = nullptr;
|
||||
QString m_redirectedText;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
@@ -142,4 +150,5 @@ Q_SIGNALS:
|
||||
void searchTextChanged();
|
||||
void showOnlySpacesChanged();
|
||||
void searchingChanged();
|
||||
void redirectedChanged();
|
||||
};
|
||||
|
||||
@@ -129,7 +129,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refresh(room, {SubtitleTextRole, LastActiveTimeRole});
|
||||
refresh(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refresh(room, {SubtitleTextRole});
|
||||
@@ -229,9 +229,6 @@ 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");
|
||||
@@ -291,7 +288,6 @@ 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";
|
||||
|
||||
@@ -43,7 +43,6 @@ 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. */
|
||||
|
||||
@@ -176,7 +176,7 @@ void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refreshRoomRoles(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole, LastActiveTimeRole});
|
||||
refreshRoomRoles(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole});
|
||||
@@ -274,7 +274,6 @@ 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";
|
||||
@@ -284,8 +283,6 @@ 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;
|
||||
}
|
||||
@@ -341,9 +338,6 @@ 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");
|
||||
@@ -380,12 +374,6 @@ 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);
|
||||
|
||||
@@ -36,7 +36,6 @@ 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. */
|
||||
@@ -48,8 +47,6 @@ 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)
|
||||
|
||||
@@ -108,11 +108,17 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
case HighlightRole:
|
||||
return EventHandler::isHighlighted(m_room, &event);
|
||||
case EventIdRole:
|
||||
return EventHandler::id(&event);
|
||||
return event.displayId();
|
||||
case IsThreadedRole:
|
||||
return EventHandler::isThreaded(&event);
|
||||
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event)) {
|
||||
return roomMessageEvent->isThreaded();
|
||||
}
|
||||
return {};
|
||||
case ThreadRootRole:
|
||||
return EventHandler::threadRoot(&event);
|
||||
if (auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event); roomMessageEvent->isThreaded()) {
|
||||
return roomMessageEvent->threadRootEventId();
|
||||
}
|
||||
return {};
|
||||
case ContentModelRole: {
|
||||
if (!event.isStateEvent()) {
|
||||
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, event.id()));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatroomtype.h"
|
||||
#include "roommanager.h"
|
||||
#include "roomtreemodel.h"
|
||||
@@ -44,58 +45,49 @@ 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<RoomTreeModel::EventRoles> alphabeticalSortPriorities{
|
||||
static const QVector<RoomSortParameter::Parameter> alphabeticalSortPriorities{
|
||||
// Does exactly what it says on the tin.
|
||||
RoomTreeModel::DisplayNameRole,
|
||||
RoomSortParameter::AlphabeticalAscending,
|
||||
};
|
||||
|
||||
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> activitySortPriorities{
|
||||
RoomSortParameter::HasHighlight,
|
||||
RoomSortParameter::MostHighlights,
|
||||
RoomSortParameter::HasUnread,
|
||||
RoomSortParameter::MostUnread,
|
||||
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;
|
||||
}
|
||||
}
|
||||
static const QVector<RoomSortParameter::Parameter> lastMessageSortPriorities{
|
||||
RoomSortParameter::LastActive,
|
||||
};
|
||||
|
||||
bool SortFilterRoomTreeModel::prioritiesCmp(const QVector<RoomTreeModel::EventRoles> &priorities,
|
||||
bool SortFilterRoomTreeModel::prioritiesCmp(const QVector<RoomSortParameter::Parameter> &priorities,
|
||||
const QModelIndex &source_left,
|
||||
const QModelIndex &source_right) const
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||
@@ -110,8 +102,9 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "enums/roomsortparameter.h"
|
||||
#include "models/roomtreemodel.h"
|
||||
|
||||
/**
|
||||
@@ -53,6 +54,7 @@ public:
|
||||
enum RoomSortOrder {
|
||||
Alphabetical,
|
||||
Activity,
|
||||
LastMessage,
|
||||
};
|
||||
Q_ENUM(RoomSortOrder)
|
||||
|
||||
@@ -104,6 +106,5 @@ private:
|
||||
QString m_filterText;
|
||||
QString m_activeSpaceId;
|
||||
|
||||
bool roleCmp(const QVariant &left, const QVariant &right) const;
|
||||
bool prioritiesCmp(const QVector<RoomTreeModel::EventRoles> &priorities, const QModelIndex &left, const QModelIndex &right) const;
|
||||
bool prioritiesCmp(const QVector<RoomSortParameter::Parameter> &priorities, const QModelIndex &left, const QModelIndex &right) const;
|
||||
};
|
||||
|
||||
@@ -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 (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
|
||||
if (roomEvent->isThreaded() && roomEvent->threadRootEventId() == 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 (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
|
||||
if (roomEvent->isThreaded() && roomEvent->threadRootEventId() == m_threadRootId) {
|
||||
addNewEvent(roomEvent);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user