Compare commits

..

1 Commits

Author SHA1 Message Date
James Graham
3d738549ae Remove giant emoji tones variable 2024-10-20 11:07:46 +01:00
200 changed files with 26650 additions and 81181 deletions

View File

@@ -110,7 +110,7 @@
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "dev",
"branch": "0.8.x",
"disable-submodules": true
}
],

View File

@@ -7,8 +7,8 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
@@ -107,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.9)
find_package(QuotientQt6 0.8.2)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -115,6 +115,11 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server"
)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
find_package(cmark)
set_package_properties(cmark PROPERTIES
TYPE REQUIRED

View File

@@ -11,7 +11,7 @@ A Qt/QML based Matrix client.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://apps.kde.org/store_badges/snapstore/en.svg'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
## Introduction

View File

@@ -5,26 +5,6 @@
"!room_id_1234:localhost:1234": {
"state": {
"events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{
"type": "m.room.member",
"state_key": "@user:localhost:1234",
@@ -46,26 +26,6 @@
},
"timeline": {
"events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{
"type": "m.room.message",
"sender": "@user:localhost:1234",

View File

@@ -53,6 +53,12 @@ ecm_add_test(
TEST_NAME messageeventmodeltest
)
ecm_add_test(
actionshandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME actionshandlertest
)
ecm_add_test(
windowcontrollertest.cpp
LINK_LIBRARIES neochat Qt::Test

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QTest>
#include "actionshandler.h"
#include "chatbarcache.h"
#include "testutils.h"
class ActionsHandlerTest : public QObject
{
Q_OBJECT
private:
Quotient::Connection *connection = Quotient::Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
private Q_SLOTS:
void nullObject();
};
void ActionsHandlerTest::nullObject()
{
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
ActionsHandler::handleMessageEvent(nullptr, nullptr);
auto chatBarCache = new ChatBarCache(this);
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
ActionsHandler::handleMessageEvent(nullptr, chatBarCache);
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
ActionsHandler::handleMessageEvent(room, nullptr);
// The final one should throw no warning so we make sure.
QTest::failOnWarning("ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
ActionsHandler::handleMessageEvent(room, chatBarCache);
}
QTEST_GUILESS_MAIN(ActionsHandlerTest)
#include "actionshandlertest.moc"

View File

@@ -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"));
}
@@ -533,7 +535,7 @@ void TextHandlerTest::componentOutput_data()
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Quote, QStringLiteral("blockquote"), {}}};
MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};
QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};

View File

@@ -50,28 +50,38 @@
<name xml:lang="x-test">xxNeoChatxx</name>
<name xml:lang="zh-CN">NeoChat</name>
<name xml:lang="zh-TW">NeoChat</name>
<summary>Chat on Matrix</summary>
<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="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="fr">Discuter sur Matrix</summary>
<summary xml:lang="gl">Charlar en 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="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="ta">மேட்ரிக்ஸுக்கான உரையாடல் செயலி</summary>
<summary xml:lang="tr">Matrix Üzerinde Sohbet</summary>
<summary xml:lang="uk">Спілкування у Matrix</summary>
<summary xml:lang="x-test">xxChat on Matrixxx</summary>
<summary xml:lang="zh-TW">在 Matrix 上聊天</summary>
<summary>Chat with your friends on matrix</summary>
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="cs">Mluvte se svými přáteli na Matrixu</summary>
<summary xml:lang="de">Mit den Freunden auf Matrix unterhalten</summary>
<summary xml:lang="el">Συνομιλήστε με τους φίλους σας στο matrix</summary>
<summary xml:lang="en-GB">Chat with your friends on matrix</summary>
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="he">התכתבות עם החברים שלך ב־matrix</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="lv">Tērzējiet ar saviem draugiem „Matrix“ tīklā</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="tr">Matrixte arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
<summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary>
<description>
<p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="ar">نيوتشات هو تطبيق دردشة يتيح لك الاستفادة الكاملة من شبكة Matrix. فهو يوفر لك طريقة آمنة لإرسال الرسائل النصية ومقاطع الفيديو والملفات الصوتية إلى عائلتك وزملائك وأصدقائك.</p>
@@ -95,7 +105,6 @@
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="ru">NeoChat — приложение для общения, предоставляющее все преимущества сети Matrix. С его помощью можно безопасно отправлять текстовые сообщения, видеозаписи и звуковые файлы родственникам, коллегам и друзьям.</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
<p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p>
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
@@ -189,7 +198,6 @@
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="ru">Голосования — MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
@@ -288,7 +296,6 @@
<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>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -351,10 +358,8 @@
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="ru">Поиск новых сообществ с помощью Matrix Spaces</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
<caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption>
<caption xml:lang="ta">மேட்ரிக்ஸு இடங்களின் மூலம் புதிய சமூகங்களைக் கண்டுபிடிக்கலாம்</caption>
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
@@ -443,7 +448,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<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"/>
<release version="24.08.0" date="2024-08-22"/>

View File

@@ -87,25 +87,47 @@ GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix
Comment[ca]=Xat a Matrix
Comment[ca@valencia]=Xat a Matrix
Comment[en_GB]=Chat on Matrix
Comment[es]=Chat en Matrix
Comment[eu]=Berriketa Matrix-en
Comment[fr]=Clavarder sur Matrix
Comment[gl]=Charle en Matrix
Comment[hu]=Csevegés Matrixon
Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[sl]=Klepet na Matrixu
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix Üzerinde Sohbet Et
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment=Client for the Matrix protocol
Comment[ar]=عميل لميفاق ماتركس
Comment[az]=Matrix protokolu üçün müştəri
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
Comment[de]=Programm für das Matrix-Protokoll
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
Comment[en_GB]=Client for the Matrix protocol
Comment[eo]=Kliento por la Matrix-protokolo
Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[gl]=Cliente para o protocolo Matrix.
Comment[he]=לקוח לפרוטוקול Matrix
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix
Comment[ie]=Un cliente del protocol Matrix
Comment[it]=Client per il protocollo Matrix
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[lv]=Klients „Matrix“ protokolam
Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Klient for Matrix-protokollen
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[ru]=Клиент для протокола Matrix
Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix
Comment[ta]=Matrix நெறிமுறைக்கான வாங்கி
Comment[tr]=Matrix protokolü için istemci
Comment[uk]=Клієнт протоколу Matrix
Comment[x-test]=xxClient for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的客户端
Comment[zh_TW]=Matrix 通訊協定的用戶端
MimeType=x-scheme-handler/matrix;
Exec=neochat %u
Terminal=false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -10,6 +10,14 @@ endif()
add_library(neochat STATIC
controller.cpp
controller.h
actionshandler.cpp
actionshandler.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 +28,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 +52,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 +103,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,32 +196,6 @@ add_library(neochat STATIC
models/threadmodel.h
enums/messagetype.h
messagecomponent.h
imagecontentmanager.h
imagecontentmanager.cpp
models/imagecontentmodel.cpp
models/imagecontentmodel.h
models/emojipacksmodel.cpp
models/emojipacksmodel.h
models/accountimagepackmodel.cpp
models/accountimagepackmodel.h
models/historyimagepackmodel.cpp
models/historyimagepackmodel.h
models/imagepacksproxymodel.cpp
models/imagepacksproxymodel.h
models/imagepacksmodel.cpp
models/imagepacksmodel.h
models/recentimagecontentmodel.h
models/recentimagecontentmodel.cpp
models/recentimagecontentproxymodel.h
models/recentimagecontentproxymodel.cpp
models/allimagecontentmodel.h
models/allimagecontentmodel.cpp
models/roomimagepacksmodel.h
models/roomimagepacksmodel.cpp
models/imagepackroomsmodel.h
models/imagepackroomsmodel.cpp
models/imagecontentfiltermodel.h
models/imagecontentfiltermodel.cpp
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -294,9 +284,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
@@ -313,12 +300,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.chatbar
)
qt_add_resources(neochat "emoji"
PREFIX "/"
FILES
data/emojis.json
)
add_subdirectory(settings)
add_subdirectory(timeline)
add_subdirectory(devtools)

140
src/actionshandler.cpp Normal file
View File

@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "actionshandler.h"
#include "chatbarcache.h"
#include "models/actionsmodel.h"
#include "neochatconfig.h"
#include "texthandler.h"
using namespace Quotient;
using namespace Qt::StringLiterals;
void ActionsHandler::handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache)
{
if (room == nullptr || chatBarCache == nullptr) {
qWarning() << "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.";
return;
}
if (!chatBarCache->attachmentPath().isEmpty()) {
QUrl url(chatBarCache->attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text());
chatBarCache->setAttachmentPath({});
chatBarCache->setText({});
return;
}
const auto handledText = handleMentions(chatBarCache);
const auto result = handleQuickEdit(room, handledText);
if (!result) {
handleMessage(room, handledText, chatBarCache);
}
}
QString ActionsHandler::handleMentions(ChatBarCache *chatBarCache)
{
const auto mentions = chatBarCache->mentions();
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
auto handledText = chatBarCache->text();
for (const auto &mention : *mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
handledText = handledText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
mentions->clear();
return handledText;
}
bool ActionsHandler::handleQuickEdit(NeoChatRoom *room, const QString &handledText)
{
if (room == nullptr) {
return false;
}
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
auto match = sed.match(handledText);
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
const QString flags = match.captured(3);
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == room->localMember().id() && event->hasTextContent()) {
QString originalString;
if (event->content()) {
#if Quotient_VERSION_MINOR > 8
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content().get())->body;
#else
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
#endif
} else {
originalString = event->plainBody();
}
if (flags == "/g"_L1) {
room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
} else {
room->postHtmlMessage(handledText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
event->id());
}
return true;
}
}
}
}
}
return false;
}
void ActionsHandler::handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache)
{
if (room == nullptr) {
return;
}
auto messageType = RoomMessageEvent::MsgType::Text;
if (handledText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (handledText.indexOf(action.prefix) == 1
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
if (action.messageAction) {
break;
} else {
return;
}
}
}
}
TextHandler textHandler;
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
if (handledText.length() == 0) {
return;
}
room->postMessage(chatBarCache->text(), handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
#include "moc_actionshandler.cpp"

43
src/actionshandler.h Normal file
View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QString>
class ChatBarCache;
class NeoChatRoom;
/**
* @class ActionsHandler
*
* This class contains functions to handle chat messages ready for posting to a room.
*
* Everything that needs to be done to prepare the message for posting in a room
* including:
* - File handling
* - User mentions
* - Quick edits
* - Chat actions
* - Custom emojis
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*
* @sa ActionsModel, NeoChatRoom
*/
class ActionsHandler
{
public:
/**
* @brief Pre-process text and send message event.
*/
static void handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache);
private:
static QString handleMentions(ChatBarCache *chatBarCache);
static bool handleQuickEdit(NeoChatRoom *room, const QString &handledText);
static void handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache);
};

View File

@@ -176,14 +176,13 @@ QQC2.Control {
RowLayout {
QQC2.ScrollView {
id: chatBarScrollView
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
Layout.minimumHeight: Kirigami.Units.gridUnit * 3
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
@@ -252,22 +251,20 @@ QQC2.Control {
}
}
Keys.onEnterPressed: event => {
const controlIsPressed = event.modifiers & Qt.ControlModifier;
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n");
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
} else {
_private.postMessage();
}
}
Keys.onReturnPressed: event => {
const controlIsPressed = event.modifiers & Qt.ControlModifier;
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n");
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
} else {
_private.postMessage();
}
}
@@ -321,11 +318,12 @@ QQC2.Control {
id: actionsRow
spacing: 0
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
@@ -342,6 +340,7 @@ QQC2.Control {
}
}
}
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
@@ -406,6 +405,7 @@ QQC2.Control {
repeatTimer.stop();
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.clearRelations();
messageSent();
}
@@ -519,6 +519,7 @@ QQC2.Control {
y: -implicitHeight
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom

View File

@@ -5,30 +5,56 @@ 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 : ""
}
}
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
}
}
}

View File

@@ -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();
}

View File

@@ -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,49 @@ 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)
}
}
}

View File

@@ -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,86 @@ 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
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 +104,114 @@ 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
contentItem: Image {
source: model.avatarUrl
}
QQC2.ToolTip.text: model.name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered && !!model.name
onClicked: stickerModel.packIndex = model.index
}
}
function clearSearchField() {
searchField.text = ""
searchField.text = "";
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 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

View File

@@ -5,11 +5,10 @@
#include <Quotient/roommember.h>
#include "actionshandler.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "models/actionsmodel.h"
#include "neochatroom.h"
#include "texthandler.h"
ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent)
@@ -30,37 +29,6 @@ void ChatBarCache::setText(const QString &text)
Q_EMIT textChanged();
}
QString ChatBarCache::sendText() const
{
if (!attachmentPath().isEmpty()) {
QUrl url(attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
return text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : text();
}
return formatMentions();
}
QString ChatBarCache::formatMentions() const
{
auto mentions = m_mentions;
std::sort(mentions.begin(), mentions.end(), [](const auto &a, const auto &b) {
return a.cursor.anchor() > b.cursor.anchor();
});
auto formattedText = text();
for (const auto &mention : mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
formattedText = formattedText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
return formattedText;
}
bool ChatBarCache::isReplying() const
{
return m_relationType == Reply && !m_relationId.isEmpty();
@@ -300,35 +268,7 @@ void ChatBarCache::postMessage()
return;
}
if (!attachmentPath().isEmpty()) {
room->uploadFile(QUrl(attachmentPath()), sendText());
clearCache();
return;
}
const auto result = ActionsModel::handleAction(room, this);
if (!result.first.has_value()) {
return;
}
TextHandler textHandler;
textHandler.setData(*std::get<std::optional<QString>>(result));
const auto sendText = textHandler.handleSendText();
if (sendText.length() == 0) {
return;
}
room->postMessage(text(), sendText, *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), replyId(), editId(), threadId());
clearCache();
}
void ChatBarCache::clearCache()
{
setText({});
m_mentions.clear();
m_savedText = QString();
clearRelations();
ActionsHandler::handleMessageEvent(room, this);
}
#include "moc_chatbarcache.cpp"

View File

@@ -153,7 +153,6 @@ public:
explicit ChatBarCache(QObject *parent = nullptr);
QString text() const;
QString sendText() const;
void setText(const QString &text);
bool isReplying() const;
@@ -216,8 +215,6 @@ Q_SIGNALS:
private:
QString m_text = QString();
QString formatMentions() const;
QString m_relationId = QString();
RelationType m_relationType = RelationType::None;
QString m_threadId = QString();
@@ -226,6 +223,4 @@ private:
QString m_savedText;
QPointer<MessageContentModel> m_relationContentModel;
void clearCache();
};

View File

@@ -63,7 +63,11 @@ Controller::Controller(QObject *parent)
});
} else {
auto c = new NeoChatConnection(this);
#if Quotient_VERSION_MINOR > 8
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234"));
#else
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
#endif
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
@@ -226,7 +230,14 @@ void Controller::invokeLogin()
Qt::SingleShotConnection);
}
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
#if Quotient_VERSION_MINOR > 8
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
#else
connection->assumeIdentity(account.userId(), accessToken);
#endif
});
}
}
@@ -433,7 +444,11 @@ void Controller::removeConnection(const QString &userId)
bool Controller::csSupported() const
{
#if Quotient_VERSION_MINOR > 8
return true;
#else
return false;
#endif
}
void Controller::revertToDefaultConfig()

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2024 Emojibase
SPDX-License-Identifier: MIT

View File

@@ -4,7 +4,6 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -24,7 +23,7 @@ ColumnLayout {
model: root.connection.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.connection.accountDataJsonString(modelData)
}, {
title: i18nc("@title:window", "Event Source"),

View File

@@ -3,7 +3,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -48,7 +47,7 @@ ColumnLayout {
model: root.room.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.room.roomAcountDataJson(text)
}, {
title: i18n("Event Source"),
@@ -78,7 +77,7 @@ ColumnLayout {
if (model.eventCount === 1) {
openEventSource(model.type, model.stateKey);
} else {
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
room: root.room,
eventType: model.type
}, {
@@ -90,7 +89,7 @@ ColumnLayout {
}
}
function openEventSource(type: string, stateKey: string): void {
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateModel,
allowEdit: true,
room: root.room,

1857
src/emojis.h Normal file

File diff suppressed because it is too large Load Diff

9
src/emojitones.cpp Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -225,10 +225,14 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{
QString body;
if (event.has<EventContent::FileContent>()) {
if (event.hasFileContent()) {
// if filename is given or body is equal to filename,
// then body is a caption
QString filename = event.get<EventContent::FileContent>()->originalName;
#if Quotient_VERSION_MINOR > 8
QString filename = event.fileContent()->originalName;
#else
QString filename = event.content()->fileInfo()->originalName;
#endif
QString body = event.plainBody();
if (filename.isEmpty() || filename == body) {
return QString();
@@ -236,8 +240,12 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
return body;
}
if (event.has<EventContent::TextContent>() && event.content()) {
body = event.get<EventContent::TextContent>()->body;
if (event.hasTextContent() && event.content()) {
#if Quotient_VERSION_MINOR > 8
body = event.richTextContent()->body;
#else
body = static_cast<const EventContent::TextContent *>(event.content())->body;
#endif
} else {
body = event.plainBody();
}
@@ -464,8 +472,12 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
{
TextHandler textHandler;
if (event.has<EventContent::FileContent>()) {
QString fileCaption = event.get<EventContent::FileContent>()->originalName;
if (event.hasFileContent()) {
#if Quotient_VERSION_MINOR > 8
QString fileCaption = event.fileContent()->originalName;
#else
QString fileCaption = event.content()->fileInfo()->originalName;
#endif
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (fileCaption != event.plainBody()) {
@@ -476,8 +488,12 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
}
QString body;
if (event.has<EventContent::TextContent>() && event.content()) {
body = event.get<EventContent::TextContent>()->body;
if (event.hasTextContent() && event.content()) {
#if Quotient_VERSION_MINOR > 8
body = event.richTextContent()->body;
#else
body = static_cast<const EventContent::TextContent *>(event.content())->body;
#endif
} else {
body = event.plainBody();
}
@@ -692,15 +708,25 @@ QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Qu
// Get the file info for the event.
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->has<EventContent::FileContentBase>()) {
if (!roomMessageEvent->hasFileContent()) {
return {};
}
const auto content = roomMessageEvent->get<EventContent::FileContentBase>();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, content.get(), eventId, false, false);
#if Quotient_VERSION_MINOR > 8
const auto content = roomMessageEvent->content();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, static_cast<EventContent::FileContent *>(content.get()), eventId, false, false);
#else
const auto content = static_cast<const EventContent::FileContent *>(roomMessageEvent->content());
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, content, eventId, false, false);
#endif
// if filename isn't specifically given, it is in body
// https://spec.matrix.org/latest/client-server-api/#mfile
mediaInfo["filename"_ls] = content->commonInfo().originalName.isEmpty() ? roomMessageEvent->plainBody() : content->commonInfo().originalName;
#if Quotient_VERSION_MINOR > 8
mediaInfo["filename"_ls] =
(roomMessageEvent->fileContent()->originalName.isEmpty()) ? roomMessageEvent->plainBody() : roomMessageEvent->fileContent()->originalName;
#else
mediaInfo["filename"_ls] = (content->fileInfo()->originalName.isEmpty()) ? roomMessageEvent->plainBody() : content->fileInfo()->originalName;
#endif
return mediaInfo;
} else if (event->is<StickerEvent>()) {
@@ -714,7 +740,11 @@ QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Qu
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
#if Quotient_VERSION_MINOR > 8
const Quotient::EventContent::FileContentBase *fileContent,
#else
const Quotient::EventContent::TypedBase *fileContent,
#endif
const QString &eventId,
bool isThumbnail,
bool isSticker)
@@ -722,10 +752,18 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
QVariantMap mediaInfo;
// Get the mxc URL for the media.
#if Quotient_VERSION_MINOR > 8
if (!fileContent->url().isValid() || fileContent->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
#else
if (!fileContent->fileInfo()->url().isValid() || fileContent->fileInfo()->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
#endif
mediaInfo["source"_ls] = QUrl();
} else {
#if Quotient_VERSION_MINOR > 8
QUrl source = room->makeMediaUrl(eventId, fileContent->url());
#else
QUrl source = room->makeMediaUrl(eventId, fileContent->fileInfo()->url());
#endif
if (source.isValid()) {
mediaInfo["source"_ls] = source;
@@ -742,15 +780,25 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileContent->commonInfo().payloadSize;
#if Quotient_VERSION_MINOR > 8
mediaInfo["size"_ls] = static_cast<const EventContent::FileContent *>(fileContent)->payloadSize;
#else
mediaInfo["size"_ls] = static_cast<const EventContent::FileContent *>(fileContent)->fileInfo()->payloadSize;
#endif
mediaInfo["isSticker"_ls] = isSticker;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileContent)) {
#if Quotient_VERSION_MINOR > 8
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
#else
const auto imageInfo = static_cast<const EventContent::ImageInfo *>(castInfo->fileInfo());
mediaInfo["width"_ls] = imageInfo->imageSize.width();
mediaInfo["height"_ls] = imageInfo->imageSize.height();
#endif
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());

View File

@@ -290,7 +290,11 @@ private:
static QVariantMap getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QVariantMap static getMediaInfoFromFileInfo(const NeoChatRoom *room,
#if Quotient_VERSION_MINOR > 8
const Quotient::EventContent::FileContentBase *fileContent,
#else
const Quotient::EventContent::TypedBase *fileContent,
#endif
const QString &eventId,
bool isThumbnail = false,
bool isSticker = false);

View File

@@ -3,6 +3,7 @@
#include "imagepackevent.h"
#include <QJsonObject>
#include <Quotient/omittable.h>
using namespace Quotient;
@@ -10,10 +11,10 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
{
if (json.contains(QStringLiteral("pack"))) {
pack = ImagePackEventContent::Pack{
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
};
} else {
pack = std::nullopt;
@@ -30,9 +31,9 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
images += ImagePackImage{
k,
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
fromJson<std::optional<QString>>(json["images"_ls][k]["body"_ls]),
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
info,
fromJson<std::optional<QStringList>>(json["images"_ls][k]["usage"_ls]),
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
};
}
}

View File

@@ -7,10 +7,13 @@
#include <Quotient/accountregistry.h>
#include <Quotient/e2ee/sssshandler.h>
#include <Quotient/keyimport.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h>
#if Quotient_VERSION_MINOR > 8
#include <Quotient/keyimport.h>
#endif
#include "controller.h"
#include "neochatconfig.h"
@@ -40,9 +43,11 @@ struct ForeignSSSSHandler {
QML_NAMED_ELEMENT(SSSSHandler)
};
#if Quotient_VERSION_MINOR > 8
struct ForeignKeyImport {
Q_GADGET
QML_SINGLETON
QML_FOREIGN(Quotient::KeyImport)
QML_NAMED_ELEMENT(KeyImport)
};
#endif

View File

@@ -1,334 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "imagecontentmanager.h"
#include <QDebug>
#include <QFile>
#include <KConfigGroup>
#include <KSharedConfig>
#include "controller.h"
#include "events/imagepackevent.h"
#include "neochatroom.h"
#include <Quotient/connection.h>
#define connection Controller::instance().activeConnection()
using namespace Quotient;
ImageContentManager::ImageContentManager(QObject *parent)
: QObject(parent)
{
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
static Connection *oldActiveConnection = nullptr;
disconnect(oldActiveConnection, nullptr, this, nullptr);
oldActiveConnection = Controller::instance().activeConnection();
setupConnection();
});
loadEmojis();
loadEmojiHistory();
setupConnection();
}
void ImageContentManager::loadEmojis()
{
QFile file(":/data/emojis.json"_ls);
file.open(QFile::ReadOnly);
Q_ASSERT(file.isOpen());
auto data = QJsonDocument::fromJson(file.readAll()).array();
for (const auto &emoji : data) {
// TODO
// m_emojiPacks += ImagePackDescription{
// .description = parts[1],
// .attribution = {},
// .icon = parts[0],
// .type = ImagePackDescription::Emoji,
// .roomId = {},
// .stateKey = parts[2],
// };
m_emojis[u"TODO"_s] += Emoji{
.text = emoji[u"icon"_s].toString(),
.displayName = emoji[u"label"_s].toString(),
.shortName = emoji[u"label"_s].toString(), // TODO
};
}
}
void ImageContentManager::loadEmojiHistory()
{
auto config = KSharedConfig::openStateConfig();
auto group = config->group("RecentEmojis"_ls);
for (const auto &key : group.keyList()) {
m_usages[key] = group.readEntry(key).toInt();
}
}
void ImageContentManager::setupConnection()
{
if (!connection) {
return;
}
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == "im.ponies.user_emotes"_ls) {
loadAccountImages();
}
if (type == "im.ponies.emote_rooms"_ls) {
loadGlobalPacks();
}
});
loadAccountImages();
loadGlobalPacks();
m_roomPacks.clear();
for (const auto &room : connection->allRooms()) {
setupRoom(static_cast<NeoChatRoom *>(room));
}
connect(connection, &Connection::joinedRoom, this, [this](const auto &room) {
setupRoom(static_cast<NeoChatRoom *>(room));
});
connect(connection, &Connection::leftRoom, this, [this](const auto &room) {
cleanupRoom(static_cast<NeoChatRoom *>(room));
});
}
const QVector<ImagePackDescription> &ImageContentManager::emojiPacks() const
{
return m_emojiPacks;
}
const QHash<QString, QVector<Emoji>> &ImageContentManager::emojis() const
{
return m_emojis;
}
void ImageContentManager::loadAccountImages()
{
m_accountImages.clear();
if (connection->hasAccountData("im.ponies.user_emotes"_ls)) {
m_accountImages = ImagePackEventContent(connection->accountData("im.ponies.user_emotes"_ls)->contentJson()).images;
}
Q_EMIT accountImagesChanged();
}
const QVector<ImagePackEventContent::ImagePackImage> &ImageContentManager::accountImages() const
{
return m_accountImages;
}
void ImageContentManager::emojiUsed(const QString &text)
{
if (!m_usages.contains(text)) {
m_usages[text] = 0;
}
m_usages[text]++;
Q_EMIT recentEmojisChanged();
auto config = KSharedConfig::openStateConfig();
auto group = config->group("RecentEmojis"_ls);
for (const auto &key : m_usages.keys()) {
group.writeEntry(key, m_usages[key]);
}
}
Emoji ImageContentManager::emojiForText(const QString &text)
{
for (const auto &category : m_emojis.values()) {
for (const auto &emoji : category) {
if (emoji.text == text) {
return emoji;
}
}
}
const auto &withSelector = QString::fromUtf8(text.toUtf8() + QByteArrayLiteral("\xEF\xB8\x8F"));
for (const auto &category : m_emojis.values()) {
for (const auto &emoji : category) {
if (emoji.text == withSelector) {
return emoji;
}
}
}
return {};
}
const QMap<QString, uint32_t> &ImageContentManager::recentEmojis() const
{
return m_usages;
}
const QMap<QString, QMap<QString, ImagePackDescription>> &ImageContentManager::roomImagePacks() const
{
return m_roomPacks;
}
void ImageContentManager::loadRoomImagePacks(NeoChatRoom *room)
{
const auto &events = room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
m_roomPacks[room->id()].clear();
for (const auto &event : events) {
auto content = ImagePackEventContent(event->contentJson());
auto avatarMxc = event->contentPart<QJsonObject>("pack"_ls)["avatar_url"_ls].toString();
if (avatarMxc.isEmpty()) {
const auto &images = event->contentPart<QJsonObject>("images"_ls);
if (images.size() > 0) {
avatarMxc = images[images.keys()[0]]["url"_ls].toString();
}
}
const auto &avatarUrl = avatarMxc.isEmpty() ? QString() : Controller::instance().activeConnection()->makeMediaUrl(QUrl(avatarMxc)).toString();
ImagePackDescription::Type type = ImagePackDescription::Both;
if (!content.pack || !content.pack->usage || content.pack->usage->isEmpty()
|| (content.pack->usage->contains("emoticon"_ls) && content.pack->usage->contains("sticker"_ls))) {
type = ImagePackDescription::Both;
} else if (content.pack->usage->contains("sticker"_ls)) {
type = ImagePackDescription::Sticker;
} else {
type = ImagePackDescription::CustomEmoji;
}
m_roomPacks[room->id()][event->stateKey()] = ImagePackDescription{
.description = event->contentPart<QJsonObject>("pack"_ls)["display_name"_ls].toString(),
.attribution = {},
.icon = QStringLiteral("<img src=\"%1\" width=\"32\" height=\"32\"/>").arg(avatarUrl),
.type = type,
.roomId = room->id(),
.stateKey = event->stateKey(),
};
m_roomImages[{room->id(), event->stateKey()}] = content.images;
}
Q_EMIT roomImagePacksChanged(room);
}
const RoomImages &ImageContentManager::roomImages() const
{
return m_roomImages;
}
const QVector<std::pair<QString, QString>> &ImageContentManager::globalPacks() const
{
return m_globalPacks;
}
void ImageContentManager::loadGlobalPacks()
{
if (!connection->hasAccountData("im.ponies.emote_rooms"_ls)) {
return;
}
m_globalPacks.clear();
const auto &rooms = Controller::instance().activeConnection()->accountData("im.ponies.emote_rooms"_ls)->contentPart<QJsonObject>("rooms"_ls);
for (const auto &roomId : rooms.keys()) {
for (const auto &stateKey : rooms[roomId].toObject().keys()) {
m_globalPacks += {roomId, stateKey};
}
}
Q_EMIT globalPacksChanged();
}
void ImageContentManager::setupRoom(NeoChatRoom *room)
{
connect(room, &Room::changed, this, [this, room]() {
loadRoomImagePacks(room);
});
loadRoomImagePacks(room);
}
void ImageContentManager::cleanupRoom(NeoChatRoom *room)
{
m_roomPacks.remove(room->id());
Q_EMIT roomImagePacksChanged(room);
}
QString ImageContentManager::mxcForShortCode(const QString &shortcode) const
{
for (const auto &image : m_accountImages) {
if (image.shortcode == shortcode) {
return Controller::instance().activeConnection()->makeMediaUrl(image.url).toString();
}
}
for (const auto &id : m_roomImages.keys()) {
for (const auto &image : m_roomImages[id]) {
if (image.shortcode == shortcode) {
return Controller::instance().activeConnection()->makeMediaUrl(image.url).toString();
}
}
}
return {};
}
QString ImageContentManager::bodyForShortCode(const QString &shortcode) const
{
for (const auto &image : m_accountImages) {
if (image.shortcode == shortcode) {
return image.body.value_or(QString());
}
}
for (const auto &id : m_roomImages.keys()) {
for (const auto &image : m_roomImages[id]) {
if (image.shortcode == shortcode) {
return image.body.value_or(QString());
}
}
}
return {};
}
bool ImageContentManager::isEmojiShortCode(const QString &shortCode) const
{
for (const auto &image : m_accountImages) {
if (image.shortcode == shortCode) {
return !image.usage || image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
}
}
for (const auto &id : m_roomImages.keys()) {
for (const auto &image : m_roomImages[id]) {
if (image.shortcode == shortCode) {
const auto pack = m_roomPacks[id.first][id.second];
return pack.type == ImagePackDescription::Emoji || pack.type == ImagePackDescription::Both;
}
}
}
return true;
}
bool ImageContentManager::isStickerShortCode(const QString &shortCode) const
{
for (const auto &image : m_accountImages) {
if (image.shortcode == shortCode) {
return !image.usage || image.usage->isEmpty() || image.usage->contains("sticker"_ls);
}
}
for (const auto &id : m_roomImages.keys()) {
for (const auto &image : m_roomImages[id]) {
if (image.shortcode == shortCode) {
const auto pack = m_roomPacks[id.first][id.second];
return pack.type == ImagePackDescription::Sticker || pack.type == ImagePackDescription::Both;
}
}
}
return true;
}
QString ImageContentManager::accountImagesAvatar() const
{
if (!connection->hasAccountData("im.ponies.user_emotes"_ls)) {
return {};
}
const auto &event = ImagePackEventContent(connection->accountData("im.ponies.user_emotes"_ls)->contentJson());
QString avatarUrl;
if (event.pack) {
avatarUrl = event.pack->avatarUrl.value_or(QUrl()).toString();
}
if (avatarUrl.isEmpty()) {
//TODO avatarUrl = Controller::instance().activeConnection()->user()->avatarUrl().toString();
}
if (avatarUrl.isEmpty()) {
avatarUrl = event.images[0].url.toString();
}
return QStringLiteral("👤");
}

View File

@@ -1,172 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QHash>
#include <QObject>
#include <QQmlEngine>
#include "events/imagepackevent.h"
#include "neochatroom.h"
#define imageContentManager ImageContentManager::instance()
class ImageContentRole : public QObject
{
Q_OBJECT
public:
enum ImageRoles {
DisplayNameRole = Qt::DisplayRole, /**< The name of the emoji. */
EmojiRole, /**< The unicode character of the emoji. */
ShortCodeRole,
IsCustomRole,
IsStickerRole,
IsEmojiRole,
UsageCountRole,
HasTonesRole,
};
Q_ENUM(ImageRoles);
};
class ImageContentPackRole : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
//! Roles for the various models providing image packs.
enum ImagePackRoles {
DisplayNameRole = Qt::DisplayRole, //! Textual desription of the pack.
IconRole, //! Icon for the pack. For emojis, this is a unicode emoji; For custom emojis and stickers, this is a HTML image.
IdentifierRole, //! An internal, mostly opaque identifier for the model.
IsEmojiRole, //! Whether this pack contains emojis (including custom). For the account pack, this is true if the pack contains any emojis; for room
//! packs, this *only* considers the pack-level usage parameter
IsStickerRole, //! Equivalent to IsEmojiRole, but for stickers.
IsEmptyRole, //! Whether this image pack is empty.
IsGlobalPackRole, //! Whether this pack is enabled globally.
};
Q_ENUM(ImagePackRoles);
};
using RoomImages = QMap<std::pair<QString, QString>, QVector<Quotient::ImagePackEventContent::ImagePackImage>>;
struct Emoji {
Q_GADGET
Q_PROPERTY(QString text MEMBER text)
Q_PROPERTY(QString displayName MEMBER displayName)
Q_PROPERTY(QString shortName MEMBER shortName)
public:
QString text;
QString displayName;
QString shortName;
};
Q_DECLARE_METATYPE(Emoji)
struct ImagePackDescription {
enum Type {
Emoji,
CustomEmoji,
Sticker,
Both,
};
QString description;
QString attribution;
QString icon;
Type type;
// Only relevant for packs coming from rooms
QString roomId;
QString stateKey;
};
/**
* This class manages emojis, custom emojis, and stickers. Because naming things is hard, it has the most generic name possible.
*/
class ImageContentManager : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
// Returns the global instance of ImageContentManager.
static ImageContentManager &instance()
{
static ImageContentManager _instance;
return _instance;
}
//! Returns a list of emoji packs (categories, e.g., food, smileys, etc.)
const QVector<ImagePackDescription> &emojiPacks() const;
//! Returns a map roomId -> stateKey -> description for all image packs that exist in rooms.
const QMap<QString, QMap<QString, ImagePackDescription>> &roomImagePacks() const;
//! Returns an list (roomId, stateKey) for all globally enabled room packs.
//! This is not filtered for rooms or stateKeys that do not exist. This is left to ImagePacksProxyModel
const QVector<std::pair<QString, QString>> &globalPacks() const;
//! Returns a map pack key -> [emoji] for all (normal) emojis.
const QHash<QString, QVector<Emoji>> &emojis() const;
//! Returns a list of all account images.
const QVector<Quotient::ImagePackEventContent::ImagePackImage> &accountImages() const;
//! Returns a map roomId -> stateKey -> [image] of all images part of a room image pack.
const RoomImages &roomImages() const;
//! Returns a map emoji -> usage count to be used as an emoji history.
const QMap<QString, uint32_t> &recentEmojis() const;
//! Returns the emoji object for the given unicode symbol.
Emoji emojiForText(const QString &text);
//! Updates the history when an emoji is used.
Q_INVOKABLE void emojiUsed(const QString &text);
QString mxcForShortCode(const QString &shortcode) const;
QString bodyForShortCode(const QString &shortcode) const;
bool isEmojiShortCode(const QString &shortCode) const;
bool isStickerShortCode(const QString &shortCode) const;
QString accountImagesAvatar() const;
Q_SIGNALS:
void accountImagesChanged();
void recentEmojisChanged();
void roomImagePacksChanged(NeoChatRoom *room);
void globalPacksChanged();
private:
// Packs
QVector<ImagePackDescription> m_emojiPacks;
// [roomId, stateKey]
QVector<std::pair<QString, QString>> m_globalPacks;
// roomId -> stateKey -> description
QMap<QString, QMap<QString, ImagePackDescription>> m_roomPacks;
// Emojis
// pack name -> emojis
QHash<QString, QVector<Emoji>> m_emojis;
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_accountImages;
RoomImages m_roomImages;
// History
// emoji -> usage count
QMap<QString, uint32_t> m_usages;
// Loads both emojis and emoji packs
void loadEmojis();
void loadGlobalPacks();
void loadRoomImagePacks(NeoChatRoom *room);
void loadEmojiHistory();
void loadAccountImages();
void loadRoomImages();
ImageContentManager(QObject *parent = nullptr);
void setupConnection();
void setupRoom(NeoChatRoom *room);
void cleanupRoom(NeoChatRoom *room);
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeochatAdd3PIdJob::NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const std::optional<QJsonObject> &auth)
NeochatAdd3PIdJob::NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), makePath("/_matrix/client/v3", "/account/3pid/add"))
{
QJsonObject _dataJson;

View File

@@ -4,9 +4,10 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatAdd3PIdJob : public Quotient::BaseJob
{
public:
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const std::optional<QJsonObject> &auth = {});
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth)
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;

View File

@@ -5,8 +5,10 @@
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth = {});
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth)
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;

View File

@@ -4,9 +4,10 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = {});
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth)
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;

View File

@@ -4,9 +4,10 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth = {});
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -4,6 +4,7 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
// TODO: Upstream to libQuotient
class NeochatGetCommonRoomsJob : public Quotient::BaseJob

View File

@@ -13,7 +13,6 @@ LoginStep {
id: root
FormCard.FormTextDelegate {
textItem.wrapMode: Text.Wrap
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
}
FormCard.AbstractFormDelegate {

View File

@@ -7,7 +7,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.settings
@@ -91,27 +90,11 @@ Kirigami.Page {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
id: delegate
required property string userId
required property NeoChatConnection connection
text: QmlUtils.escapeString(connection.localUser.displayName)
description: connection.localUser.id
leadingPadding: Kirigami.Units.largeSpacing
text: model.userId
onClicked: {
Controller.activeConnection = delegate.connection;
Controller.activeConnection = model.connection;
root.connectionChosen();
}
leading: KirigamiComponents.Avatar {
id: avatar
name: delegate.text
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
source: delegate.connection.localUser.avatarUrl.toString().length > 0 ? delegate.connection.makeMediaUrl(delegate.connection.localUser.avatarUrl) : ""
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
}
}
}
Repeater {

View File

@@ -62,8 +62,6 @@
#include "fakerunner.h"
#endif
#include "imagecontentmanager.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
#endif
@@ -142,7 +140,7 @@ int main(int argc, char *argv[])
KAboutData about(QStringLiteral("neochat"),
i18n("NeoChat"),
QStringLiteral(NEOCHAT_VERSION_STRING),
i18n("Chat on Matrix"),
i18n("Matrix client"),
KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
about.addAuthor(i18n("Carl Schwan"),

View File

@@ -163,7 +163,11 @@ void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
#if Quotient_VERSION_MINOR > 8
co_await qCoro(job.get(), &BaseJob::finished);
#else
co_await qCoro(job, &BaseJob::finished);
#endif
if (job->error() != BaseJob::NoError) {
co_return;
}
@@ -185,7 +189,11 @@ QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl sourc
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
#if Quotient_VERSION_MINOR > 8
co_await qCoro(job.get(), &BaseJob::finished);
#else
co_await qCoro(job, &BaseJob::finished);
#endif
if (job->error() != BaseJob::NoError) {
co_return;
}

View 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();
};

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "accountimagepackmodel.h"
#include <KLocalizedString>
#include "imagecontentmanager.h"
QVariant AccountImagePackModel::data(const QModelIndex &index, int role) const
{
Q_UNUSED(index);
if (role == ImageContentPackRole::DisplayNameRole) {
return i18n("Your Emojis");
}
if (role == ImageContentPackRole::IconRole) {
return imageContentManager.accountImagesAvatar();
}
if (role == ImageContentPackRole::IdentifierRole) {
return QStringLiteral("account");
}
if (role == ImageContentPackRole::IsEmojiRole) {
for (const auto &image : imageContentManager.accountImages()) {
if (!image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("emoticon"))) {
return true;
}
}
return false;
}
if (role == ImageContentPackRole::IsStickerRole) {
for (const auto &image : imageContentManager.accountImages()) {
if (!image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("sticker"))) {
return true;
}
}
return false;
}
if (role == ImageContentPackRole::IsEmptyRole) {
return imageContentManager.accountImages().size() == 0;
}
return {};
}
int AccountImagePackModel::rowCount(const QModelIndex &index) const
{
Q_UNUSED(index);
return ImageContentManager::instance().accountImages().size() > 0 ? 1 : 0;
}
QHash<int, QByteArray> AccountImagePackModel::roleNames() const
{
return {
{ImageContentPackRole::DisplayNameRole, "displayName"},
{ImageContentPackRole::IconRole, "emoji"},
{ImageContentPackRole::IdentifierRole, "identifier"},
};
}
AccountImagePackModel::AccountImagePackModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(&ImageContentManager::instance(), &ImageContentManager::accountImagesChanged, this, [this]() {
beginResetModel();
endResetModel();
});
}

View File

@@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
class AccountImagePackModel : public QAbstractListModel
{
Q_OBJECT
/**
* Note: This model uses the ImagePackRoles from ImageContentManager as roles.
*/
public:
explicit AccountImagePackModel(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;
};

View File

@@ -5,11 +5,9 @@
#include "chatbarcache.h"
#include "enums/messagetype.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "roommanager.h"
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/user.h>
@@ -18,7 +16,6 @@
using Action = ActionsModel::Action;
using namespace Quotient;
using namespace Qt::StringLiterals;
QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls, "#ffd500"_ls, "#ffff00"_ls, "#d4ff00"_ls, "#aaff00"_ls, "#80ff00"_ls,
"#55ff00"_ls, "#2bff00"_ls, "#00ff00"_ls, "#00ff2b"_ls, "#00ff55"_ls, "#00ff80"_ls, "#00ffaa"_ls, "#00ffd5"_ls, "#00ffff"_ls,
@@ -576,78 +573,3 @@ QList<Action> &ActionsModel::allActions() const
{
return actions;
}
bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messageText)
{
if (room == nullptr) {
return false;
}
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
auto match = sed.match(messageText);
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
const QString flags = match.captured(3);
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == room->localMember().id() && event->has<EventContent::TextContent>()) {
QString originalString;
if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content().get())->body;
} else {
originalString = event->plainBody();
}
QString replaceId = event->id();
const auto eventRelation = event->relatesTo();
if (eventRelation && eventRelation->type == "m.replace"_L1) {
replaceId = eventRelation->eventId;
}
if (flags == "/g"_L1) {
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, replaceId);
} else {
room->postHtmlMessage(messageText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
replaceId);
}
return true;
}
}
}
}
}
return false;
}
std::pair<std::optional<QString>, std::optional<Quotient::RoomMessageEvent::MsgType>> ActionsModel::handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache)
{
auto sendText = chatBarCache->sendText();
const auto edited = handleQuickEditAction(room, sendText);
if (edited) {
return std::make_pair(std::nullopt, std::nullopt);
}
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
if (sendText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (sendText.indexOf(action.prefix) == 1
&& (sendText.indexOf(" "_ls) == action.prefix.length() + 1 || sendText.length() == action.prefix.length() + 1)) {
sendText = action.handle(sendText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache);
if (action.messageType.has_value()) {
messageType = action.messageType;
}
if (action.messageAction) {
break;
} else {
return std::make_pair(std::nullopt, std::nullopt);
}
}
}
}
return std::make_pair(sendText, messageType);
}

View File

@@ -90,21 +90,6 @@ public:
*/
QList<Action> &allActions() const;
/**
* @brief Handle special sed style edit action.
*
* @return True if the message has a sed edit which was actioned. False otherwise.
*/
static bool handleQuickEditAction(NeoChatRoom *room, const QString &messageText);
/**
* @brief Handle any action within the message contained in the given ChatBarCache.
*
* @return A modified or unmodified string that needs to be sent or an empty string if
* the handled action replaces sending a normal message.
*/
static std::pair<std::optional<QString>, std::optional<Quotient::RoomMessageEvent::MsgType>> handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache);
private:
ActionsModel() = default;
};

View File

@@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "allimagecontentmodel.h"
#include "imagecontentmanager.h"
// TODO custom emojis
AllImageContentModel::AllImageContentModel(QObject *parent)
: QAbstractListModel(parent)
{
// TODO connect to custom emojis changing;
}
QVariant AllImageContentModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
for (const auto &category : ImageContentManager::instance().emojis()) {
if (row >= category.size()) {
row -= category.size();
continue;
}
if (role == ImageContentRole::DisplayNameRole) {
return category[row].displayName;
}
if (role == ImageContentRole::EmojiRole) {
return category[row].text;
}
if (role == ImageContentRole::IsStickerRole) {
return false;
}
if (role == ImageContentRole::IsEmojiRole) {
return true;
}
}
return {};
}
int AllImageContentModel::rowCount(const QModelIndex &index) const
{
Q_UNUSED(index);
auto sum = 0;
for (const auto &category : ImageContentManager::instance().emojis()) {
sum += category.size();
}
return sum;
}
QHash<int, QByteArray> AllImageContentModel::roleNames() const
{
return {
{ImageContentRole::DisplayNameRole, "displayName"},
{ImageContentRole::EmojiRole, "text"},
};
}

View File

@@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
class AllImageContentModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit AllImageContentModel(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;
};

View File

@@ -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
@@ -86,23 +85,29 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
auto mediaId = m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
if (mediaId.isEmpty()) {
return QVariant();
}
if (m_room) {
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
}
}
}
if (m_autoCompletionType == Emoji) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
}
}
// if (m_autoCompletionType == Emoji) {
// if (role == DisplayNameRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
// }
// if (role == IconNameRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
// }
// if (role == ReplacedTextRole) {
// return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
// }
// if (role == SubtitleRole) {
// // TODO return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
// }
// }
return {};
}
@@ -148,8 +153,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();

View 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();
};

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