Compare commits
1 Commits
work/nvrwh
...
fix-editin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf23c491a |
@@ -2,7 +2,7 @@
|
|||||||
"id": "org.kde.neochat",
|
"id": "org.kde.neochat",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"runtime": "org.kde.Platform",
|
"runtime": "org.kde.Platform",
|
||||||
"runtime-version": "5.15-22.08",
|
"runtime-version": "5.15-21.08",
|
||||||
"sdk": "org.kde.Sdk",
|
"sdk": "org.kde.Sdk",
|
||||||
"command": "neochat",
|
"command": "neochat",
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,3 @@ compile_commands.json
|
|||||||
kate.project.ctags.*
|
kate.project.ctags.*
|
||||||
*.user
|
*.user
|
||||||
.flatpak-builder/
|
.flatpak-builder/
|
||||||
.idea/
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
include:
|
include:
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
# TODO enable once we can have qt6 libQuotient on the CI
|
||||||
|
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
||||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ Dependencies:
|
|||||||
'frameworks/kdbusaddons': '@stable'
|
'frameworks/kdbusaddons': '@stable'
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
|
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
|
||||||
|
|||||||
@@ -6,19 +6,11 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
# KDE Applications version, managed by release script.
|
project(NeoChat)
|
||||||
set(RELEASE_SERVICE_VERSION_MAJOR "23")
|
set(PROJECT_VERSION "22.11")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
|
||||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
|
||||||
|
|
||||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
|
||||||
|
|
||||||
set(KF5_MIN_VERSION "5.91.0")
|
set(KF5_MIN_VERSION "5.91.0")
|
||||||
set(QT_MIN_VERSION "5.15.2")
|
set(QT_MIN_VERSION "5.15.2")
|
||||||
if (ANDROID)
|
|
||||||
set(QT_MIN_VERSION "5.15.8")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
||||||
|
|
||||||
@@ -130,8 +122,6 @@ set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
|
|||||||
TYPE OPTIONAL
|
TYPE OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Sqlite3)
|
|
||||||
|
|
||||||
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
||||||
cmake_policy(SET CMP0063 OLD)
|
cmake_policy(SET CMP0063 OLD)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
0
LICENSES/MIT.txt
Normal file → Executable file
0
LICENSES/MIT.txt
Normal file → Executable file
@@ -14,8 +14,7 @@
|
|||||||
android:name="org.qtproject.qt5.android.bindings.QtActivity"
|
android:name="org.qtproject.qt5.android.bindings.QtActivity"
|
||||||
android:label="NeoChat"
|
android:label="NeoChat"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop">
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
@@ -23,6 +22,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
|
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
|
||||||
|
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||||
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
|
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
|
||||||
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
||||||
<!-- Messages maps -->
|
<!-- Messages maps -->
|
||||||
|
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
|
||||||
|
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
|
||||||
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
|
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
|
||||||
|
|
||||||
<!-- Splash screen -->
|
<!-- Splash screen -->
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
classpath 'com.android.tools.build:gradle:3.6.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +73,6 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion qtMinSdkVersion
|
minSdkVersion qtMinSdkVersion
|
||||||
targetSdkVersion qtTargetSdkVersion
|
targetSdkVersion qtTargetSdkVersion
|
||||||
applicationId "org.kde.neochat"
|
|
||||||
namespace "org.kde.neochat"
|
|
||||||
versionCode timestamp
|
|
||||||
versionName projectVersionFull
|
|
||||||
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
|
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,20 +231,6 @@
|
|||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="23.01" date="2023-01-30">
|
|
||||||
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
|
|
||||||
<description>
|
|
||||||
<p>New features and bugfixes:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Notifications will now be shown for all accounts, not just the active one</li>
|
|
||||||
<li>There is a new "compact" mode for the room list</li>
|
|
||||||
<li>You can now search in the room history</li>
|
|
||||||
<li>Emojis and Reactions have been significantly improved</li>
|
|
||||||
<li>Fixed several crashes around user invitations</li>
|
|
||||||
<li>Room permission settings can now be configured</li>
|
|
||||||
</ul>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
<release version="22.11" date="2022-11-30">
|
<release version="22.11" date="2022-11-30">
|
||||||
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
|
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
1705
po/ar/neochat.po
1705
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1857
po/az/neochat.po
1857
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1651
po/ca/neochat.po
1651
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1714
po/cs/neochat.po
1714
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1682
po/da/neochat.po
1682
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1721
po/de/neochat.po
1721
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
2422
po/el/neochat.po
2422
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1713
po/en_GB/neochat.po
1713
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1687
po/es/neochat.po
1687
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1671
po/eu/neochat.po
1671
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1851
po/fi/neochat.po
1851
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1729
po/fr/neochat.po
1729
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1839
po/hu/neochat.po
1839
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
2093
po/ia/neochat.po
2093
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1830
po/id/neochat.po
1830
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1741
po/ie/neochat.po
1741
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1745
po/it/neochat.po
1745
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1623
po/ja/neochat.po
1623
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1676
po/ka/neochat.po
1676
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1834
po/ko/neochat.po
1834
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
1675
po/nl/neochat.po
1675
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1490
po/nn/neochat.po
1490
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1815
po/pa/neochat.po
1815
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1793
po/pl/neochat.po
1793
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1725
po/pt/neochat.po
1725
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1858
po/pt_BR/neochat.po
1858
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1721
po/ru/neochat.po
1721
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1855
po/sk/neochat.po
1855
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1702
po/sl/neochat.po
1702
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1855
po/sv/neochat.po
1855
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1708
po/ta/neochat.po
1708
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1691
po/tok/neochat.po
1691
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
1702
po/tr/neochat.po
1702
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1679
po/uk/neochat.po
1679
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1665
po/zh_CN/neochat.po
1665
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1620
po/zh_TW/neochat.po
1620
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -6,47 +6,47 @@
|
|||||||
add_library(neochat STATIC
|
add_library(neochat STATIC
|
||||||
controller.cpp
|
controller.cpp
|
||||||
actionshandler.cpp
|
actionshandler.cpp
|
||||||
models/emojimodel.cpp
|
emojimodel.cpp
|
||||||
emojitones.cpp
|
emojitones.cpp
|
||||||
models/customemojimodel.cpp
|
customemojimodel.cpp
|
||||||
clipboard.cpp
|
clipboard.cpp
|
||||||
matriximageprovider.cpp
|
matriximageprovider.cpp
|
||||||
models/messageeventmodel.cpp
|
messageeventmodel.cpp
|
||||||
models/messagefiltermodel.cpp
|
messagefiltermodel.cpp
|
||||||
models/roomlistmodel.cpp
|
roomlistmodel.cpp
|
||||||
models/sortfilterspacelistmodel.cpp
|
sortfilterspacelistmodel.cpp
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
roommanager.cpp
|
roommanager.cpp
|
||||||
neochatroom.cpp
|
neochatroom.cpp
|
||||||
neochatuser.cpp
|
neochatuser.cpp
|
||||||
models/userlistmodel.cpp
|
userlistmodel.cpp
|
||||||
models/userfiltermodel.cpp
|
userfiltermodel.cpp
|
||||||
models/publicroomlistmodel.cpp
|
publicroomlistmodel.cpp
|
||||||
models/userdirectorylistmodel.cpp
|
userdirectorylistmodel.cpp
|
||||||
models/keywordnotificationrulemodel.cpp
|
keywordnotificationrulemodel.cpp
|
||||||
utils.cpp
|
utils.cpp
|
||||||
notificationsmanager.cpp
|
notificationsmanager.cpp
|
||||||
models/sortfilterroomlistmodel.cpp
|
sortfilterroomlistmodel.cpp
|
||||||
chatdocumenthandler.cpp
|
chatdocumenthandler.cpp
|
||||||
models/devicesmodel.cpp
|
devicesmodel.cpp
|
||||||
filetypesingleton.cpp
|
filetypesingleton.cpp
|
||||||
login.cpp
|
login.cpp
|
||||||
stickerevent.cpp
|
stickerevent.cpp
|
||||||
models/webshortcutmodel.cpp
|
webshortcutmodel.cpp
|
||||||
blurhash.cpp
|
blurhash.cpp
|
||||||
blurhashimageprovider.cpp
|
blurhashimageprovider.cpp
|
||||||
joinrulesevent.cpp
|
joinrulesevent.cpp
|
||||||
models/collapsestateproxymodel.cpp
|
collapsestateproxymodel.cpp
|
||||||
urlhelper.cpp
|
urlhelper.cpp
|
||||||
windowcontroller.cpp
|
windowcontroller.cpp
|
||||||
linkpreviewer.cpp
|
linkpreviewer.cpp
|
||||||
models/completionmodel.cpp
|
completionmodel.cpp
|
||||||
models/completionproxymodel.cpp
|
completionproxymodel.cpp
|
||||||
models/actionsmodel.cpp
|
actionsmodel.cpp
|
||||||
models/serverlistmodel.cpp
|
serverlistmodel.cpp
|
||||||
models/statemodel.cpp
|
statemodel.cpp
|
||||||
filetransferpseudojob.cpp
|
filetransferpseudojob.cpp
|
||||||
models/searchmodel.cpp
|
searchmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
@@ -103,9 +103,6 @@ endif()
|
|||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
target_sources(neochat PRIVATE notifyrc.qrc)
|
target_sources(neochat PRIVATE notifyrc.qrc)
|
||||||
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
|
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
|
||||||
if(SQLite3_FOUND)
|
|
||||||
target_link_libraries(neochat-app PRIVATE SQLite::SQLite3)
|
|
||||||
endif()
|
|
||||||
target_sources(neochat-app PRIVATE notifyrc.qrc)
|
target_sources(neochat-app PRIVATE notifyrc.qrc)
|
||||||
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
|
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
|
||||||
kirigami_package_breeze_icons(ICONS
|
kirigami_package_breeze_icons(ICONS
|
||||||
@@ -162,7 +159,7 @@ if(ANDROID)
|
|||||||
"zoom-out"
|
"zoom-out"
|
||||||
"image-rotate-left-symbolic"
|
"image-rotate-left-symbolic"
|
||||||
"image-rotate-right-symbolic"
|
"image-rotate-right-symbolic"
|
||||||
"channel-secure-symbolic"
|
"channel-insecure-symbolic"
|
||||||
"download"
|
"download"
|
||||||
"smiley"
|
"smiley"
|
||||||
"tools-check-spelling"
|
"tools-check-spelling"
|
||||||
|
|||||||
@@ -13,11 +13,10 @@
|
|||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
|
|
||||||
|
#include "actionsmodel.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "models/actionsmodel.h"
|
#include "customemojimodel.h"
|
||||||
#include "models/customemojimodel.h"
|
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
|
||||||
@@ -59,9 +58,9 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
|
|||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActionsHandler::handleNewMessage()
|
void ActionsHandler::handleMessage()
|
||||||
{
|
{
|
||||||
checkEffects(m_room->chatBoxText());
|
checkEffects();
|
||||||
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
|
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
|
||||||
QUrl url(m_room->chatBoxAttachmentPath());
|
QUrl url(m_room->chatBoxAttachmentPath());
|
||||||
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||||
@@ -70,39 +69,13 @@ void ActionsHandler::handleNewMessage()
|
|||||||
m_room->setChatBoxText({});
|
m_room->setChatBoxText({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString handledText = m_room->chatBoxText();
|
QString handledText = m_room->chatBoxText();
|
||||||
handledText = handleMentions(handledText);
|
|
||||||
handleMessage(m_room->chatBoxText(), handledText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ActionsHandler::handleEdit()
|
std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool {
|
||||||
{
|
|
||||||
checkEffects(m_room->editText());
|
|
||||||
|
|
||||||
QString handledText = m_room->editText();
|
|
||||||
handledText = handleMentions(handledText, true);
|
|
||||||
handleMessage(m_room->editText(), handledText, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
|
|
||||||
{
|
|
||||||
if (!m_room) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<Mention> *mentions;
|
|
||||||
if (isEdit) {
|
|
||||||
mentions = m_room->editMentions();
|
|
||||||
} else {
|
|
||||||
mentions = m_room->mentions();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
|
|
||||||
return a.cursor.anchor() > b.cursor.anchor();
|
return a.cursor.anchor() > b.cursor.anchor();
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const auto &mention : *mentions) {
|
for (const auto &mention : *m_room->mentions()) {
|
||||||
if (mention.text.isEmpty() || mention.id.isEmpty()) {
|
if (mention.text.isEmpty() || mention.id.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -110,16 +83,11 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
|
|||||||
mention.cursor.position() - mention.cursor.anchor(),
|
mention.cursor.position() - mention.cursor.anchor(),
|
||||||
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
|
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
|
||||||
}
|
}
|
||||||
mentions->clear();
|
m_room->mentions()->clear();
|
||||||
|
|
||||||
return handledText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
|
|
||||||
{
|
|
||||||
if (NeoChatConfig::allowQuickEdit()) {
|
if (NeoChatConfig::allowQuickEdit()) {
|
||||||
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
|
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
|
||||||
auto match = sed.match(text);
|
auto match = sed.match(m_room->chatBoxText());
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
const QString regex = match.captured(1);
|
const QString regex = match.captured(1);
|
||||||
const QString replacement = match.captured(2).toHtmlEscaped();
|
const QString replacement = match.captured(2).toHtmlEscaped();
|
||||||
@@ -178,13 +146,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
|||||||
if (handledText.length() == 0) {
|
if (handledText.length() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId());
|
||||||
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActionsHandler::checkEffects(const QString &text)
|
void ActionsHandler::checkEffects()
|
||||||
{
|
{
|
||||||
std::optional<QString> effect = std::nullopt;
|
std::optional<QString> effect = std::nullopt;
|
||||||
|
const auto &text = m_room->chatBoxText();
|
||||||
if (text.contains("\u2744")) {
|
if (text.contains("\u2744")) {
|
||||||
effect = QLatin1String("snowflake");
|
effect = QLatin1String("snowflake");
|
||||||
} else if (text.contains("\u1F386")) {
|
} else if (text.contains("\u1F386")) {
|
||||||
|
|||||||
@@ -33,22 +33,14 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
|
|
||||||
/**
|
/// \brief Post a message.
|
||||||
* @brief Pre-process text and send message.
|
///
|
||||||
*/
|
/// This also interprets commands if any.
|
||||||
void handleNewMessage();
|
void handleMessage();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Pre-process text and send edit.
|
|
||||||
*/
|
|
||||||
void handleEdit();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NeoChatRoom *m_room = nullptr;
|
NeoChatRoom *m_room = nullptr;
|
||||||
void checkEffects(const QString &text);
|
void checkEffects();
|
||||||
|
|
||||||
QString handleMentions(QString handledText, const bool &isEdit = false);
|
|
||||||
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
QString markdownToHTML(const QString &markdown);
|
QString markdownToHTML(const QString &markdown);
|
||||||
|
|||||||
@@ -158,12 +158,11 @@ QVector<ActionsModel::Action> actions{
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
|
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
|
||||||
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
|
|
||||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
|
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Ban) {
|
||||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
#include <Sonnet/BackgroundChecker>
|
#include <Sonnet/BackgroundChecker>
|
||||||
#include <Sonnet/Settings>
|
#include <Sonnet/Settings>
|
||||||
|
|
||||||
#include "models/actionsmodel.h"
|
#include "actionsmodel.h"
|
||||||
#include "models/roomlistmodel.h"
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
#include "roomlistmodel.h"
|
||||||
|
|
||||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||||
{
|
{
|
||||||
@@ -105,19 +105,14 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
|||||||
{
|
{
|
||||||
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
||||||
m_completionModel->setRoom(m_room);
|
m_completionModel->setRoom(m_room);
|
||||||
static QPointer<NeoChatRoom> previousRoom = nullptr;
|
static NeoChatRoom *previousRoom = nullptr;
|
||||||
if (previousRoom) {
|
if (previousRoom) {
|
||||||
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
|
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
|
||||||
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
|
|
||||||
}
|
}
|
||||||
previousRoom = m_room;
|
previousRoom = m_room;
|
||||||
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
|
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
|
||||||
int start = completionStartIndex();
|
int start = completionStartIndex();
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
|
||||||
});
|
|
||||||
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
|
|
||||||
int start = completionStartIndex();
|
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
||||||
@@ -128,7 +123,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int start = completionStartIndex();
|
int start = completionStartIndex();
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +138,7 @@ int ChatDocumentHandler::completionStartIndex() const
|
|||||||
#else
|
#else
|
||||||
const auto cursor = cursorPosition();
|
const auto cursor = cursorPosition();
|
||||||
#endif
|
#endif
|
||||||
const auto &text = getText();
|
const auto &text = m_room->chatBoxText();
|
||||||
auto start = std::min(cursor, text.size()) - 1;
|
auto start = std::min(cursor, text.size()) - 1;
|
||||||
while (start > -1) {
|
while (start > -1) {
|
||||||
if (text.at(start) == QLatin1Char(' ')) {
|
if (text.at(start) == QLatin1Char(' ')) {
|
||||||
@@ -155,20 +150,6 @@ int ChatDocumentHandler::completionStartIndex() const
|
|||||||
return start;
|
return start;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDocumentHandler::isEdit() const
|
|
||||||
{
|
|
||||||
return m_isEdit;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setIsEdit(bool edit)
|
|
||||||
{
|
|
||||||
if (edit == m_isEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_isEdit = edit;
|
|
||||||
Q_EMIT isEditChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickTextDocument *ChatDocumentHandler::document() const
|
QQuickTextDocument *ChatDocumentHandler::document() const
|
||||||
{
|
{
|
||||||
return m_document;
|
return m_document;
|
||||||
@@ -223,7 +204,7 @@ void ChatDocumentHandler::complete(int index)
|
|||||||
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
||||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
||||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||||
auto text = getText();
|
auto text = m_room->chatBoxText();
|
||||||
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
||||||
QTextCursor cursor(document()->textDocument());
|
QTextCursor cursor(document()->textDocument());
|
||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
@@ -232,11 +213,11 @@ void ChatDocumentHandler::complete(int index)
|
|||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
||||||
cursor.setKeepPositionOnInsert(true);
|
cursor.setKeepPositionOnInsert(true);
|
||||||
pushMention({cursor, name, 0, 0, id});
|
m_room->mentions()->push_back({cursor, name, 0, 0, id});
|
||||||
m_highlighter->rehighlight();
|
m_highlighter->rehighlight();
|
||||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
||||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||||
auto text = getText();
|
auto text = m_room->chatBoxText();
|
||||||
auto at = text.lastIndexOf(QLatin1Char('/'));
|
auto at = text.lastIndexOf(QLatin1Char('/'));
|
||||||
QTextCursor cursor(document()->textDocument());
|
QTextCursor cursor(document()->textDocument());
|
||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
@@ -244,7 +225,7 @@ void ChatDocumentHandler::complete(int index)
|
|||||||
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
||||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
||||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||||
auto text = getText();
|
auto text = m_room->chatBoxText();
|
||||||
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
||||||
QTextCursor cursor(document()->textDocument());
|
QTextCursor cursor(document()->textDocument());
|
||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
@@ -253,11 +234,11 @@ void ChatDocumentHandler::complete(int index)
|
|||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
||||||
cursor.setKeepPositionOnInsert(true);
|
cursor.setKeepPositionOnInsert(true);
|
||||||
pushMention({cursor, alias, 0, 0, alias});
|
m_room->mentions()->push_back({cursor, alias, 0, 0, alias});
|
||||||
m_highlighter->rehighlight();
|
m_highlighter->rehighlight();
|
||||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
||||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||||
auto text = getText();
|
auto text = m_room->chatBoxText();
|
||||||
auto at = text.lastIndexOf(QLatin1Char(':'));
|
auto at = text.lastIndexOf(QLatin1Char(':'));
|
||||||
QTextCursor cursor(document()->textDocument());
|
QTextCursor cursor(document()->textDocument());
|
||||||
cursor.setPosition(at);
|
cursor.setPosition(at);
|
||||||
@@ -300,27 +281,3 @@ void ChatDocumentHandler::setSelectionEnd(int position)
|
|||||||
m_selectionEnd = position;
|
m_selectionEnd = position;
|
||||||
Q_EMIT selectionEndChanged();
|
Q_EMIT selectionEndChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatDocumentHandler::getText() const
|
|
||||||
{
|
|
||||||
if (!m_room) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
if (m_isEdit) {
|
|
||||||
return m_room->editText();
|
|
||||||
} else {
|
|
||||||
return m_room->chatBoxText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::pushMention(const Mention mention) const
|
|
||||||
{
|
|
||||||
if (!m_room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_isEdit) {
|
|
||||||
m_room->editMentions()->push_back(mention);
|
|
||||||
} else {
|
|
||||||
m_room->mentions()->push_back(mention);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@
|
|||||||
#include <QQuickTextDocument>
|
#include <QQuickTextDocument>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
|
||||||
#include "models/completionmodel.h"
|
#include "completionmodel.h"
|
||||||
#include "models/userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
class QTextDocument;
|
class QTextDocument;
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
@@ -18,14 +17,6 @@ class SyntaxHighlighter;
|
|||||||
class ChatDocumentHandler : public QObject
|
class ChatDocumentHandler : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Is the instance being used to handle an edit message.
|
|
||||||
*
|
|
||||||
* This is needed to ensure that the text and mentions are saved and retrieved
|
|
||||||
* from the correct parameters in the assigned room.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
|
|
||||||
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||||
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
||||||
@@ -33,14 +24,11 @@ class ChatDocumentHandler : public QObject
|
|||||||
|
|
||||||
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
|
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
|
||||||
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] bool isEdit() const;
|
|
||||||
void setIsEdit(bool edit);
|
|
||||||
|
|
||||||
[[nodiscard]] QQuickTextDocument *document() const;
|
[[nodiscard]] QQuickTextDocument *document() const;
|
||||||
void setDocument(QQuickTextDocument *document);
|
void setDocument(QQuickTextDocument *document);
|
||||||
|
|
||||||
@@ -61,7 +49,6 @@ public:
|
|||||||
void updateCompletions();
|
void updateCompletions();
|
||||||
CompletionModel *completionModel() const;
|
CompletionModel *completionModel() const;
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void isEditChanged();
|
|
||||||
void documentChanged();
|
void documentChanged();
|
||||||
void cursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
@@ -72,8 +59,6 @@ Q_SIGNALS:
|
|||||||
private:
|
private:
|
||||||
int completionStartIndex() const;
|
int completionStartIndex() const;
|
||||||
|
|
||||||
bool m_isEdit;
|
|
||||||
|
|
||||||
QQuickTextDocument *m_document;
|
QQuickTextDocument *m_document;
|
||||||
|
|
||||||
NeoChatRoom *m_room = nullptr;
|
NeoChatRoom *m_room = nullptr;
|
||||||
@@ -83,9 +68,6 @@ private:
|
|||||||
int m_selectionStart;
|
int m_selectionStart;
|
||||||
int m_selectionEnd;
|
int m_selectionEnd;
|
||||||
|
|
||||||
QString getText() const;
|
|
||||||
void pushMention(const Mention mention) const;
|
|
||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
|
|
||||||
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
|
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
|
||||||
|
|||||||
@@ -33,14 +33,13 @@ QImage Clipboard::image() const
|
|||||||
|
|
||||||
QString Clipboard::saveImage(QString localPath) const
|
QString Clipboard::saveImage(QString localPath) const
|
||||||
{
|
{
|
||||||
QString imageDir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
|
if (!QDir().exists(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)))) {
|
||||||
|
QDir().mkdir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
|
||||||
if (!QDir().exists(imageDir)) {
|
|
||||||
QDir().mkdir(imageDir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localPath.isEmpty()) {
|
if (localPath.isEmpty()) {
|
||||||
localPath = QStringLiteral("file://%1/%2.png").arg(imageDir, QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
|
localPath = QStringLiteral("file://%1/screenshots/%2.png")
|
||||||
|
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
|
||||||
|
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
|
||||||
}
|
}
|
||||||
QUrl url(localPath);
|
QUrl url(localPath);
|
||||||
if (!url.isLocalFile()) {
|
if (!url.isLocalFile()) {
|
||||||
@@ -52,11 +51,14 @@ QString Clipboard::saveImage(QString localPath) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.save(url.toLocalFile())) {
|
QDir dir;
|
||||||
return localPath;
|
if (!dir.exists(localPath)) {
|
||||||
} else {
|
dir.mkpath(localPath);
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
image.save(url.toLocalFile());
|
||||||
|
|
||||||
|
return localPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Clipboard::saveText(QString message)
|
void Clipboard::saveText(QString message)
|
||||||
|
|||||||
85
src/collapsestateproxymodel.cpp
Normal file
85
src/collapsestateproxymodel.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "collapsestateproxymodel.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(source_parent);
|
||||||
|
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
|
||||||
|
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
||||||
|
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
||||||
|
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|
||||||
|
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool() // If it's a new day, show it
|
||||||
|
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
|
||||||
|
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0),
|
||||||
|
MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
|
||||||
|
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
|
||||||
|
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (role == AggregateDisplayRole) {
|
||||||
|
return aggregateEventToString(mapToSource(index).row());
|
||||||
|
}
|
||||||
|
return sourceModel()->data(mapToSource(index), role);
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
|
||||||
|
{
|
||||||
|
auto roles = sourceModel()->roleNames();
|
||||||
|
roles[AggregateDisplayRole] = "aggregateDisplay";
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||||
|
{
|
||||||
|
QStringList parts;
|
||||||
|
for (int i = sourceRow; i >= 0; i--) {
|
||||||
|
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
|
||||||
|
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole)
|
||||||
|
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||||
|
|| (i > 0
|
||||||
|
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
|
||||||
|
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
|
||||||
|
|| (i > 0
|
||||||
|
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
|
||||||
|
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
|
||||||
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
QStringList chunks;
|
||||||
|
while (!parts.isEmpty()) {
|
||||||
|
chunks += QString();
|
||||||
|
int count = 1;
|
||||||
|
auto part = parts.takeFirst();
|
||||||
|
chunks.last() += part;
|
||||||
|
while (!parts.isEmpty() && parts.first() == part) {
|
||||||
|
parts.removeFirst();
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count > 1) {
|
||||||
|
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QString text = chunks.takeFirst();
|
||||||
|
|
||||||
|
if (chunks.size() > 0) {
|
||||||
|
while (chunks.size() > 1) {
|
||||||
|
text += i18nc("[action 1], [action 2 and action 3]", ", ");
|
||||||
|
text += chunks.takeFirst();
|
||||||
|
}
|
||||||
|
text += i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||||
|
text += chunks.takeFirst();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,29 +12,10 @@ class CollapseStateProxyModel : public QSortFilterProxyModel
|
|||||||
public:
|
public:
|
||||||
enum Roles {
|
enum Roles {
|
||||||
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
||||||
StateEventsRole,
|
|
||||||
AuthorListRole,
|
|
||||||
};
|
};
|
||||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief QString aggregating the text of consecutive state events starting at row.
|
|
||||||
*
|
|
||||||
* If state events happen on different days they will be split into two aggregate
|
|
||||||
* events.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||||
/**
|
|
||||||
* @brief Return a list of consecutive state events starting at row.
|
|
||||||
*
|
|
||||||
* If state events happen on different days they will be split into two aggregate
|
|
||||||
* events.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QVariantList stateEventsList(int row) const;
|
|
||||||
/**
|
|
||||||
* @brief List of unique authors for the aggregate state events starting at row.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QVariantList authorList(int row) const;
|
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,7 @@ CompletionModel::CompletionModel(QObject *parent)
|
|||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_filterModel(new CompletionProxyModel())
|
, m_filterModel(new CompletionProxyModel())
|
||||||
, m_userListModel(new UserListModel(this))
|
, m_userListModel(new UserListModel(this))
|
||||||
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
, m_emojiModel(new KConcatenateRowsProxyModel(this))
|
||||||
{
|
{
|
||||||
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
|
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
|
||||||
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QConcatenateTablesProxyModel>
|
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
#include <KConcatenateRowsProxyModel>
|
||||||
|
|
||||||
#include "roomlistmodel.h"
|
#include "roomlistmodel.h"
|
||||||
|
|
||||||
class CompletionProxyModel;
|
class CompletionProxyModel;
|
||||||
@@ -74,6 +75,6 @@ private:
|
|||||||
|
|
||||||
UserListModel *m_userListModel;
|
UserListModel *m_userListModel;
|
||||||
RoomListModel *m_roomListModel;
|
RoomListModel *m_roomListModel;
|
||||||
QConcatenateTablesProxyModel *m_emojiModel;
|
KConcatenateRowsProxyModel *m_emojiModel;
|
||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);
|
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);
|
||||||
@@ -4,11 +4,7 @@
|
|||||||
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
#include <qt5keychain/keychain.h>
|
#include <qt5keychain/keychain.h>
|
||||||
#else
|
|
||||||
#include <qt6keychain/keychain.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <KConfig>
|
#include <KConfig>
|
||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
@@ -132,7 +128,16 @@ Controller::Controller(QObject *parent)
|
|||||||
if (AccountRegistry::instance().size() > oldAccountCount) {
|
if (AccountRegistry::instance().size() > oldAccountCount) {
|
||||||
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
|
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
|
||||||
connect(connection, &Connection::syncDone, this, [=]() {
|
connect(connection, &Connection::syncDone, this, [=]() {
|
||||||
handleNotifications(connection);
|
bool changes = false;
|
||||||
|
for (const auto &room : connection->allRooms()) {
|
||||||
|
if (m_notificationCounts[room] != room->unreadStats().notableCount) {
|
||||||
|
m_notificationCounts[room] = room->unreadStats().notableCount;
|
||||||
|
changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changes) {
|
||||||
|
handleNotifications();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
oldAccountCount = AccountRegistry::instance().size();
|
oldAccountCount = AccountRegistry::instance().size();
|
||||||
@@ -141,16 +146,19 @@ Controller::Controller(QObject *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
void Controller::handleNotifications()
|
||||||
{
|
{
|
||||||
static QStringList initial;
|
static bool initial = true;
|
||||||
static QStringList oldNotifications;
|
static QStringList oldNotifications;
|
||||||
auto job = connection->callApi<GetNotificationsJob>();
|
if (!m_connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto job = m_connection->callApi<GetNotificationsJob>();
|
||||||
|
|
||||||
connect(job, &BaseJob::success, this, [job, connection]() {
|
connect(job, &BaseJob::success, this, [this, job]() {
|
||||||
const auto notifications = job->jsonData()["notifications"].toArray();
|
const auto notifications = job->jsonData()["notifications"].toArray();
|
||||||
if (!initial.contains(connection->user()->id())) {
|
if (initial) {
|
||||||
initial.append(connection->user()->id());
|
initial = false;
|
||||||
for (const auto &n : notifications) {
|
for (const auto &n : notifications) {
|
||||||
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
|
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
|
||||||
}
|
}
|
||||||
@@ -166,7 +174,7 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
||||||
auto room = connection->room(notification["room_id"].toString());
|
auto room = m_connection->room(notification["room_id"].toString());
|
||||||
|
|
||||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||||
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||||
@@ -188,7 +196,7 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
|||||||
|
|
||||||
if (notification["event"]["type"] == "m.room.encrypted") {
|
if (notification["event"]["type"] == "m.room.encrypted") {
|
||||||
#ifdef Quotient_E2EE_ENABLED
|
#ifdef Quotient_E2EE_ENABLED
|
||||||
auto decrypted = connection->decryptNotification(notification);
|
auto decrypted = m_connection->decryptNotification(notification);
|
||||||
body = decrypted["content"].toObject()["body"].toString();
|
body = decrypted["content"].toObject()["body"].toString();
|
||||||
#endif
|
#endif
|
||||||
if (body.isEmpty()) {
|
if (body.isEmpty()) {
|
||||||
@@ -373,11 +381,6 @@ void Controller::invokeLogin()
|
|||||||
if (error == "Unrecognised access token") {
|
if (error == "Unrecognised access token") {
|
||||||
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
|
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
|
||||||
logout(connection, false);
|
logout(connection, false);
|
||||||
} else if (error == "Connection closed") {
|
|
||||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
|
||||||
// Failed due to network connection issue. This might happen when the homeserver is
|
|
||||||
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
|
|
||||||
// connect to the homeserver. In this case, we don't want to do logout().
|
|
||||||
} else {
|
} else {
|
||||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||||
logout(connection, true);
|
logout(connection, true);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ private:
|
|||||||
|
|
||||||
bool hasWindowSystem() const;
|
bool hasWindowSystem() const;
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
void handleNotifications(QPointer<Quotient::Connection> connection);
|
void handleNotifications();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
struct CustomEmoji {
|
struct CustomEmoji {
|
||||||
QString name; // with :semicolons:
|
QString name; // with :semicolons:
|
||||||
@@ -70,12 +70,7 @@ void DevicesModel::logout(int index, const QString &password)
|
|||||||
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
|
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
|
||||||
|
|
||||||
connect(job, &BaseJob::result, this, [this, job, password, index] {
|
connect(job, &BaseJob::result, this, [this, job, password, index] {
|
||||||
auto onSuccess = [this, index]() {
|
if (job->error() != 0) {
|
||||||
beginRemoveRows(QModelIndex(), index, index);
|
|
||||||
m_devices.remove(index);
|
|
||||||
endRemoveRows();
|
|
||||||
};
|
|
||||||
if (job->error() != BaseJob::Success) {
|
|
||||||
QJsonObject replyData = job->jsonData();
|
QJsonObject replyData = job->jsonData();
|
||||||
QJsonObject authData;
|
QJsonObject authData;
|
||||||
authData["session"] = replyData["session"];
|
authData["session"] = replyData["session"];
|
||||||
@@ -84,9 +79,11 @@ void DevicesModel::logout(int index, const QString &password)
|
|||||||
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
|
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
|
||||||
authData["identifier"] = identifier;
|
authData["identifier"] = identifier;
|
||||||
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
|
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
|
||||||
connect(innerJob, &BaseJob::success, this, onSuccess);
|
connect(innerJob, &BaseJob::success, this, [this, index]() {
|
||||||
} else {
|
beginRemoveRows(QModelIndex(), index, index);
|
||||||
onSuccess();
|
m_devices.remove(index);
|
||||||
|
endRemoveRows();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
#include "emojitones.h"
|
#include "emojitones.h"
|
||||||
#include "models/emojimodel.h"
|
#include "emojimodel.h"
|
||||||
|
|
||||||
QMultiHash<QString, QVariant> EmojiTones::_tones = {
|
QMultiHash<QString, QVariant> EmojiTones::_tones = {
|
||||||
#include "emojitones_data.h"
|
#include "emojitones_data.h"
|
||||||
|
|||||||
42
src/main.cpp
42
src/main.cpp
@@ -42,39 +42,39 @@
|
|||||||
#include "blurhashimageprovider.h"
|
#include "blurhashimageprovider.h"
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatdocumenthandler.h"
|
||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
|
#include "collapsestateproxymodel.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "customemojimodel.h"
|
||||||
|
#include "devicesmodel.h"
|
||||||
|
#include "emojimodel.h"
|
||||||
#include "filetypesingleton.h"
|
#include "filetypesingleton.h"
|
||||||
#include "joinrulesevent.h"
|
#include "joinrulesevent.h"
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
|
#include "keywordnotificationrulemodel.h"
|
||||||
#include "login.h"
|
#include "login.h"
|
||||||
#include "matriximageprovider.h"
|
#include "matriximageprovider.h"
|
||||||
#include "models/collapsestateproxymodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "models/customemojimodel.h"
|
#include "messagefiltermodel.h"
|
||||||
#include "models/devicesmodel.h"
|
|
||||||
#include "models/emojimodel.h"
|
|
||||||
#include "models/keywordnotificationrulemodel.h"
|
|
||||||
#include "models/messageeventmodel.h"
|
|
||||||
#include "models/messagefiltermodel.h"
|
|
||||||
#include "models/publicroomlistmodel.h"
|
|
||||||
#include "models/roomlistmodel.h"
|
|
||||||
#include "models/searchmodel.h"
|
|
||||||
#include "models/serverlistmodel.h"
|
|
||||||
#include "models/sortfilterroomlistmodel.h"
|
|
||||||
#include "models/sortfilterspacelistmodel.h"
|
|
||||||
#include "models/userdirectorylistmodel.h"
|
|
||||||
#include "models/userfiltermodel.h"
|
|
||||||
#include "models/userlistmodel.h"
|
|
||||||
#include "models/webshortcutmodel.h"
|
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "notificationsmanager.h"
|
#include "notificationsmanager.h"
|
||||||
|
#include "searchmodel.h"
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#include "pollhandler.h"
|
#include "pollhandler.h"
|
||||||
#endif
|
#endif
|
||||||
|
#include "publicroomlistmodel.h"
|
||||||
|
#include "roomlistmodel.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
#include "serverlistmodel.h"
|
||||||
|
#include "sortfilterroomlistmodel.h"
|
||||||
|
#include "sortfilterspacelistmodel.h"
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
#include "urlhelper.h"
|
#include "urlhelper.h"
|
||||||
|
#include "userdirectorylistmodel.h"
|
||||||
|
#include "userfiltermodel.h"
|
||||||
|
#include "userlistmodel.h"
|
||||||
|
#include "webshortcutmodel.h"
|
||||||
#include "windowcontroller.h"
|
#include "windowcontroller.h"
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#include <keyverificationsession.h>
|
#include <keyverificationsession.h>
|
||||||
@@ -82,19 +82,15 @@
|
|||||||
#ifdef HAVE_COLORSCHEME
|
#ifdef HAVE_COLORSCHEME
|
||||||
#include "colorschemer.h"
|
#include "colorschemer.h"
|
||||||
#endif
|
#endif
|
||||||
#include "models/completionmodel.h"
|
#include "completionmodel.h"
|
||||||
#include "models/statemodel.h"
|
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
|
#include "statemodel.h"
|
||||||
|
|
||||||
#ifdef HAVE_RUNNER
|
#ifdef HAVE_RUNNER
|
||||||
#include "runner.h"
|
#include "runner.h"
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_WINDOWS
|
|
||||||
#include <Windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[IsNameChangeRole] = "isNameChange";
|
roles[IsNameChangeRole] = "isNameChange";
|
||||||
roles[IsAvatarChangeRole] = "isAvatarChange";
|
roles[IsAvatarChangeRole] = "isAvatarChange";
|
||||||
roles[IsRedactedRole] = "isRedacted";
|
roles[IsRedactedRole] = "isRedacted";
|
||||||
roles[GenericDisplayRole] = "genericDisplay";
|
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +439,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
case EventTypeRole:
|
case EventTypeRole:
|
||||||
return DelegateType::ReadMarker;
|
return DelegateType::ReadMarker;
|
||||||
case TimeRole: {
|
case TimeRole: {
|
||||||
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
|
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
|
||||||
const KFormat format;
|
const KFormat format;
|
||||||
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
||||||
}
|
}
|
||||||
@@ -463,14 +462,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
return m_currentRoom->eventToString(evt, Qt::RichText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == GenericDisplayRole) {
|
|
||||||
if (evt.isRedacted()) {
|
|
||||||
return i18n("<i>[This message was deleted]</i>");
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_currentRoom->eventToGenericString(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == FormattedBodyRole) {
|
if (role == FormattedBodyRole) {
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||||
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
||||||
@@ -946,12 +937,24 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
|
|||||||
if (content.contains("m.new_content")) {
|
if (content.contains("m.new_content")) {
|
||||||
// The message has been edited so we have to return the id of the original message instead of the replacement
|
// The message has been edited so we have to return the id of the original message instead of the replacement
|
||||||
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
||||||
|
e = eventCast<const RoomMessageEvent>(m_currentRoom->findInTimeline(eventId)->event());
|
||||||
|
if (!e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
content = e->contentJson();
|
||||||
} else {
|
} else {
|
||||||
// For any message that isn't an edit return the id of the current message
|
// For any message that isn't an edit return the id of the current message
|
||||||
eventId = (*it)->id();
|
eventId = (*it)->id();
|
||||||
}
|
}
|
||||||
targetMessage.insert("event_id", eventId);
|
targetMessage.insert("event_id", eventId);
|
||||||
targetMessage.insert("formattedBody", content["formatted_body"].toString());
|
targetMessage.insert("formattedBody", content["formatted_body"].toString());
|
||||||
|
|
||||||
|
// keep reply relationship
|
||||||
|
if (content.contains("m.relates_to")) {
|
||||||
|
targetMessage.insert("m.relates_to", content["m.relates_to"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
// Need to get the message from the original eventId or body will have * on the front
|
// Need to get the message from the original eventId or body will have * on the front
|
||||||
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
||||||
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
|
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
|
||||||
@@ -45,7 +45,6 @@ public:
|
|||||||
AnnotationRole,
|
AnnotationRole,
|
||||||
UserMarkerRole,
|
UserMarkerRole,
|
||||||
FormattedBodyRole,
|
FormattedBodyRole,
|
||||||
GenericDisplayRole,
|
|
||||||
|
|
||||||
MimeTypeRole,
|
MimeTypeRole,
|
||||||
FileMimetypeIcon,
|
FileMimetypeIcon,
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "collapsestateproxymodel.h"
|
|
||||||
#include "messageeventmodel.h"
|
|
||||||
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(source_parent);
|
|
||||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
|
|
||||||
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
|
||||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
|
||||||
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|
|
||||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (role == AggregateDisplayRole) {
|
|
||||||
return aggregateEventToString(mapToSource(index).row());
|
|
||||||
} else if (role == StateEventsRole) {
|
|
||||||
return stateEventsList(mapToSource(index).row());
|
|
||||||
} else if (role == AuthorListRole) {
|
|
||||||
return authorList(mapToSource(index).row());
|
|
||||||
}
|
|
||||||
return sourceModel()->data(mapToSource(index), role);
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
|
|
||||||
{
|
|
||||||
auto roles = sourceModel()->roleNames();
|
|
||||||
roles[AggregateDisplayRole] = "aggregateDisplay";
|
|
||||||
roles[StateEventsRole] = "stateEvents";
|
|
||||||
roles[AuthorListRole] = "authorList";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
|
||||||
{
|
|
||||||
QStringList parts;
|
|
||||||
QVariantList uniqueAuthors;
|
|
||||||
for (int i = sourceRow; i >= 0; i--) {
|
|
||||||
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
|
|
||||||
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
|
||||||
if (!uniqueAuthors.contains(nextAuthor)) {
|
|
||||||
uniqueAuthors.append(nextAuthor);
|
|
||||||
}
|
|
||||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.sort(); // Sort them so that all identical events can be collected.
|
|
||||||
if (!parts.isEmpty()) {
|
|
||||||
QStringList chunks;
|
|
||||||
while (!parts.isEmpty()) {
|
|
||||||
chunks += QString();
|
|
||||||
int count = 1;
|
|
||||||
auto part = parts.takeFirst();
|
|
||||||
chunks.last() += part;
|
|
||||||
while (!parts.isEmpty() && parts.first() == part) {
|
|
||||||
parts.removeFirst();
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
if (count > 1 && uniqueAuthors.length() == 1) {
|
|
||||||
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chunks.removeDuplicates();
|
|
||||||
QString text = "<style>a {text-decoration: none;}</style>"; // There can be links in the event text so make sure all are styled.
|
|
||||||
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
|
|
||||||
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
|
|
||||||
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
|
|
||||||
.arg(uniqueAuthors[0].toMap()["id"].toString(),
|
|
||||||
uniqueAuthors[0].toMap()["color"].toString(),
|
|
||||||
uniqueAuthors[0].toMap()["displayName"].toString());
|
|
||||||
text += userText;
|
|
||||||
text += chunks.takeFirst();
|
|
||||||
|
|
||||||
if (chunks.size() > 0) {
|
|
||||||
while (chunks.size() > 1) {
|
|
||||||
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
|
||||||
text += chunks.takeFirst();
|
|
||||||
}
|
|
||||||
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
|
|
||||||
text += chunks.takeFirst();
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
|
|
||||||
{
|
|
||||||
QVariantList stateEvents;
|
|
||||||
for (int i = sourceRow; i >= 0; i--) {
|
|
||||||
auto nextState = QVariantMap{
|
|
||||||
{"author", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
|
|
||||||
{"authorDisplayName", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
|
|
||||||
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
|
|
||||||
};
|
|
||||||
stateEvents.append(nextState);
|
|
||||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stateEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
|
|
||||||
{
|
|
||||||
QVariantList uniqueAuthors;
|
|
||||||
for (int i = sourceRow; i >= 0; i--) {
|
|
||||||
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
|
||||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
|
||||||
uniqueAuthors.append(nextAvatar);
|
|
||||||
}
|
|
||||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uniqueAuthors;
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
<label>Background transparency value</label>
|
<label>Background transparency value</label>
|
||||||
<default>0.3</default>
|
<default>0.3</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="ShowNotifications" type="bool">
|
||||||
|
<label>Show notifications</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
<entry name="MergeRoomList" type="bool">
|
<entry name="MergeRoomList" type="bool">
|
||||||
<label>Merge Room Lists</label>
|
<label>Merge Room Lists</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
|
|||||||
@@ -658,135 +658,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
|||||||
i18n("Unknown event"));
|
i18n("Unknown event"));
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
|
||||||
{
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
return switchOnType(
|
|
||||||
#else
|
|
||||||
return visit(
|
|
||||||
#endif
|
|
||||||
evt,
|
|
||||||
[](const RoomMessageEvent &e) {
|
|
||||||
Q_UNUSED(e)
|
|
||||||
return i18n("sent a message");
|
|
||||||
},
|
|
||||||
[](const StickerEvent &e) {
|
|
||||||
Q_UNUSED(e)
|
|
||||||
return i18n("sent a sticker");
|
|
||||||
},
|
|
||||||
[](const RoomMemberEvent &e) {
|
|
||||||
switch (e.membership()) {
|
|
||||||
case MembershipType::Invite:
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
return i18n("reinvited someone to the room");
|
|
||||||
}
|
|
||||||
Q_FALLTHROUGH();
|
|
||||||
case MembershipType::Join: {
|
|
||||||
QString text{};
|
|
||||||
// Part 1: invites and joins
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
text = i18n("joined the room (repeated)");
|
|
||||||
} else if (e.changesMembership()) {
|
|
||||||
text = e.membership() == MembershipType::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
|
||||||
}
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
// Part 2: profile changes of joined members
|
|
||||||
if (e.isRename()) {
|
|
||||||
if (e.displayName().isEmpty()) {
|
|
||||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
|
||||||
} else {
|
|
||||||
text = i18nc("their refers to a singular user", "changed their display name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.isAvatarUpdate()) {
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
text += i18n(" and ");
|
|
||||||
}
|
|
||||||
if (e.avatarUrl().isEmpty()) {
|
|
||||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
} else if (!e.prevContent()->avatarUrl) {
|
|
||||||
#else
|
|
||||||
} else if (e.prevContent()->avatarUrl.isEmpty()) {
|
|
||||||
#endif
|
|
||||||
text += i18n("set an avatar");
|
|
||||||
} else {
|
|
||||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
text = i18nc("<user> changed nothing", "changed nothing");
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
case MembershipType::Leave:
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
|
||||||
}
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
|
||||||
case MembershipType::Ban:
|
|
||||||
if (e.senderId() != e.userId()) {
|
|
||||||
return i18n("banned a user from the room");
|
|
||||||
} else {
|
|
||||||
return i18n("self-banned from the room");
|
|
||||||
}
|
|
||||||
case MembershipType::Knock: {
|
|
||||||
return i18n("requested an invite");
|
|
||||||
}
|
|
||||||
default:;
|
|
||||||
}
|
|
||||||
return i18n("made something unknown");
|
|
||||||
},
|
|
||||||
[](const RoomCanonicalAliasEvent &e) {
|
|
||||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
|
||||||
},
|
|
||||||
[](const RoomNameEvent &e) {
|
|
||||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
|
||||||
},
|
|
||||||
[](const RoomTopicEvent &e) {
|
|
||||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
|
||||||
},
|
|
||||||
[](const RoomAvatarEvent &) {
|
|
||||||
return i18n("changed the room avatar");
|
|
||||||
},
|
|
||||||
[](const EncryptionEvent &) {
|
|
||||||
return i18n("activated End-to-End Encryption");
|
|
||||||
},
|
|
||||||
[](const RoomCreateEvent &e) {
|
|
||||||
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
|
||||||
},
|
|
||||||
[](const RoomPowerLevelsEvent &) {
|
|
||||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
|
||||||
},
|
|
||||||
[](const StateEventBase &e) {
|
|
||||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
|
||||||
return i18n("changed the server access control lists for this room");
|
|
||||||
}
|
|
||||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
|
||||||
if (e.fullJson()["unsigned"]["prev_content"].toObject().isEmpty()) {
|
|
||||||
return i18n("added a widget");
|
|
||||||
}
|
|
||||||
if (e.contentJson().isEmpty()) {
|
|
||||||
return i18n("removed a widget");
|
|
||||||
}
|
|
||||||
return i18n("configured a widget");
|
|
||||||
}
|
|
||||||
return i18n("updated the state");
|
|
||||||
},
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
[](const PollStartEvent &e) {
|
|
||||||
return i18n("started a poll");
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
i18n("Unknown event"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||||
{
|
{
|
||||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||||
@@ -868,13 +739,25 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
QJsonObject content{{"body", text}, {"msgtype", msgTypeToString(type)}, {"format", "org.matrix.custom.html"}, {"formatted_body", html}};
|
||||||
|
if (isReply) {
|
||||||
|
content["m.relates_to"] =
|
||||||
|
QJsonObject {
|
||||||
|
{"m.in_reply_to",
|
||||||
|
QJsonObject {
|
||||||
|
{"event_id", replyEventId}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject json{
|
QJsonObject json{
|
||||||
{"type", "m.room.message"},
|
{"type", "m.room.message"},
|
||||||
{"msgtype", msgTypeToString(type)},
|
{"msgtype", msgTypeToString(type)},
|
||||||
{"body", "* " + text},
|
{"body", "* " + text},
|
||||||
{"format", "org.matrix.custom.html"},
|
{"format", "org.matrix.custom.html"},
|
||||||
{"formatted_body", html},
|
{"formatted_body", html},
|
||||||
{"m.new_content", QJsonObject{{"body", text}, {"msgtype", msgTypeToString(type)}, {"format", "org.matrix.custom.html"}, {"formatted_body", html}}},
|
{"m.new_content", content},
|
||||||
{"m.relates_to", QJsonObject{{"rel_type", "m.replace"}, {"event_id", relateToEventId}}}};
|
{"m.relates_to", QJsonObject{{"rel_type", "m.replace"}, {"event_id", relateToEventId}}}};
|
||||||
|
|
||||||
postJson("m.room.message", json);
|
postJson("m.room.message", json);
|
||||||
@@ -1626,17 +1509,6 @@ void NeoChatRoom::setChatBoxText(const QString &text)
|
|||||||
Q_EMIT chatBoxTextChanged();
|
Q_EMIT chatBoxTextChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::editText() const
|
|
||||||
{
|
|
||||||
return m_editText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void NeoChatRoom::setEditText(const QString &text)
|
|
||||||
{
|
|
||||||
m_editText = text;
|
|
||||||
Q_EMIT editTextChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString NeoChatRoom::chatBoxReplyId() const
|
QString NeoChatRoom::chatBoxReplyId() const
|
||||||
{
|
{
|
||||||
return m_chatBoxReplyId;
|
return m_chatBoxReplyId;
|
||||||
@@ -1713,11 +1585,6 @@ QVector<Mention> *NeoChatRoom::mentions()
|
|||||||
return &m_mentions;
|
return &m_mentions;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<Mention> *NeoChatRoom::editMentions()
|
|
||||||
{
|
|
||||||
return &m_editMentions;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString NeoChatRoom::savedText() const
|
QString NeoChatRoom::savedText() const
|
||||||
{
|
{
|
||||||
return m_savedText;
|
return m_savedText;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
#include <qcoro/task.h>
|
#include <qcoro/task.h>
|
||||||
|
|
||||||
#include "neochatuser.h"
|
class PollHandler;
|
||||||
#include "pollhandler.h"
|
class NeoChatUser;
|
||||||
|
|
||||||
class PushNotificationState : public QObject
|
class PushNotificationState : public QObject
|
||||||
{
|
{
|
||||||
@@ -79,11 +79,6 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
|
|
||||||
// Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms
|
// Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms
|
||||||
Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged)
|
Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged)
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The text for any message currently being edited in the room.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(QString editText READ editText WRITE setEditText NOTIFY editTextChanged)
|
|
||||||
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
|
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
|
||||||
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
|
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
|
||||||
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
|
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
|
||||||
@@ -254,7 +249,6 @@ public:
|
|||||||
[[nodiscard]] QString avatarMediaId() const;
|
[[nodiscard]] QString avatarMediaId() const;
|
||||||
|
|
||||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const;
|
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const;
|
||||||
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
|
||||||
|
|
||||||
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
||||||
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
|
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
|
||||||
@@ -276,9 +270,6 @@ public:
|
|||||||
QString chatBoxText() const;
|
QString chatBoxText() const;
|
||||||
void setChatBoxText(const QString &text);
|
void setChatBoxText(const QString &text);
|
||||||
|
|
||||||
QString editText() const;
|
|
||||||
void setEditText(const QString &text);
|
|
||||||
|
|
||||||
QString chatBoxReplyId() const;
|
QString chatBoxReplyId() const;
|
||||||
void setChatBoxReplyId(const QString &replyId);
|
void setChatBoxReplyId(const QString &replyId);
|
||||||
|
|
||||||
@@ -296,11 +287,6 @@ public:
|
|||||||
|
|
||||||
QVector<Mention> *mentions();
|
QVector<Mention> *mentions();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Vector of mentions in the current edit text.
|
|
||||||
*/
|
|
||||||
QVector<Mention> *editMentions();
|
|
||||||
|
|
||||||
QString savedText() const;
|
QString savedText() const;
|
||||||
void setSavedText(const QString &savedText);
|
void setSavedText(const QString &savedText);
|
||||||
|
|
||||||
@@ -350,12 +336,10 @@ private:
|
|||||||
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
|
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
|
||||||
|
|
||||||
QString m_chatBoxText;
|
QString m_chatBoxText;
|
||||||
QString m_editText;
|
|
||||||
QString m_chatBoxReplyId;
|
QString m_chatBoxReplyId;
|
||||||
QString m_chatBoxEditId;
|
QString m_chatBoxEditId;
|
||||||
QString m_chatBoxAttachmentPath;
|
QString m_chatBoxAttachmentPath;
|
||||||
QVector<Mention> m_mentions;
|
QVector<Mention> m_mentions;
|
||||||
QVector<Mention> m_editMentions;
|
|
||||||
QString m_savedText;
|
QString m_savedText;
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
QCache<QString, PollHandler> m_polls;
|
QCache<QString, PollHandler> m_polls;
|
||||||
@@ -378,7 +362,6 @@ Q_SIGNALS:
|
|||||||
void pushNotificationStateChanged(PushNotificationState::State state);
|
void pushNotificationStateChanged(PushNotificationState::State state);
|
||||||
void showMessage(MessageType messageType, const QString &message);
|
void showMessage(MessageType messageType, const QString &message);
|
||||||
void chatBoxTextChanged();
|
void chatBoxTextChanged();
|
||||||
void editTextChanged();
|
|
||||||
void chatBoxReplyIdChanged();
|
void chatBoxReplyIdChanged();
|
||||||
void chatBoxEditIdChanged();
|
void chatBoxEditIdChanged();
|
||||||
void chatBoxAttachmentPathChanged();
|
void chatBoxAttachmentPathChanged();
|
||||||
|
|||||||
@@ -11,12 +11,6 @@
|
|||||||
#include <KNotification>
|
#include <KNotification>
|
||||||
#include <KNotificationReplyAction>
|
#include <KNotificationReplyAction>
|
||||||
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
#include <accountregistry.h>
|
|
||||||
#else
|
|
||||||
#include "neochataccountregistry.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
#include <csapi/pushrules.h>
|
#include <csapi/pushrules.h>
|
||||||
#include <jobs/basejob.h>
|
#include <jobs/basejob.h>
|
||||||
@@ -55,6 +49,10 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
const QString &replyEventId,
|
const QString &replyEventId,
|
||||||
bool canReply)
|
bool canReply)
|
||||||
{
|
{
|
||||||
|
if (!NeoChatConfig::self()->showNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QPixmap img;
|
QPixmap img;
|
||||||
img.convertFromImage(icon);
|
img.convertFromImage(icon);
|
||||||
KNotification *notification = new KNotification("message");
|
KNotification *notification = new KNotification("message");
|
||||||
@@ -70,15 +68,8 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
|
|
||||||
notification->setDefaultAction(i18n("Open NeoChat in this room"));
|
notification->setDefaultAction(i18n("Open NeoChat in this room"));
|
||||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||||
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
|
||||||
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
|
|
||||||
#else
|
|
||||||
Controller::instance().setActiveConnection(AccountRegistry::instance().get(room->localUser()->id()));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
RoomManager::instance().enterRoom(room);
|
RoomManager::instance().enterRoom(room);
|
||||||
|
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canReply) {
|
if (canReply) {
|
||||||
@@ -99,6 +90,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
|
|
||||||
void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
|
void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
|
||||||
{
|
{
|
||||||
|
if (!NeoChatConfig::self()->showNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
QPixmap img;
|
QPixmap img;
|
||||||
img.convertFromImage(icon);
|
img.convertFromImage(icon);
|
||||||
KNotification *notification = new KNotification("invite");
|
KNotification *notification = new KNotification("invite");
|
||||||
@@ -108,9 +102,9 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
|
|||||||
notification->setFlags(KNotification::Persistent);
|
notification->setFlags(KNotification::Persistent);
|
||||||
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
|
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||||
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
|
||||||
notification->close();
|
notification->close();
|
||||||
RoomManager::instance().enterRoom(room);
|
RoomManager::instance().enterRoom(room);
|
||||||
|
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
||||||
});
|
});
|
||||||
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
|
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
|
||||||
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
|
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
|
||||||
@@ -268,10 +262,6 @@ void NotificationsManager::updateNotificationRules(const QString &type)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Controller::instance().activeConnection()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
|
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
|
||||||
|
|
||||||
// Update override rules
|
// Update override rules
|
||||||
@@ -281,9 +271,7 @@ void NotificationsManager::updateNotificationRules(const QString &type)
|
|||||||
if (overrideRule["rule_id"] == ".m.rule.master") {
|
if (overrideRule["rule_id"] == ".m.rule.master") {
|
||||||
bool ruleEnabled = overrideRule["enabled"].toBool();
|
bool ruleEnabled = overrideRule["enabled"].toBool();
|
||||||
m_globalNotificationsEnabled = !ruleEnabled;
|
m_globalNotificationsEnabled = !ruleEnabled;
|
||||||
if (!m_globalNotificationsSet) {
|
NeoChatConfig::self()->setShowNotifications(m_globalNotificationsEnabled);
|
||||||
m_globalNotificationsSet = true;
|
|
||||||
}
|
|
||||||
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
|
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class NotificationsManager : public QObject
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
|
Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
|
||||||
Q_PROPERTY(bool globalNotificationsSet MEMBER m_globalNotificationsSet NOTIFY globalNotificationsSetChanged)
|
|
||||||
Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY
|
Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY
|
||||||
oneToOneNotificationActionChanged)
|
oneToOneNotificationActionChanged)
|
||||||
Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE
|
Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE
|
||||||
@@ -74,8 +73,7 @@ private:
|
|||||||
QMultiMap<QString, KNotification *> m_notifications;
|
QMultiMap<QString, KNotification *> m_notifications;
|
||||||
QHash<QString, QPointer<KNotification>> m_invitations;
|
QHash<QString, QPointer<KNotification>> m_invitations;
|
||||||
|
|
||||||
bool m_globalNotificationsEnabled = false;
|
bool m_globalNotificationsEnabled;
|
||||||
bool m_globalNotificationsSet = false;
|
|
||||||
PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown;
|
PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown;
|
||||||
PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown;
|
PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown;
|
||||||
PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown;
|
PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown;
|
||||||
@@ -109,7 +107,6 @@ private Q_SLOTS:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void globalNotificationsEnabledChanged(bool newState);
|
void globalNotificationsEnabledChanged(bool newState);
|
||||||
void globalNotificationsSetChanged(bool newState);
|
|
||||||
void oneToOneNotificationActionChanged(PushNotificationAction::Action action);
|
void oneToOneNotificationActionChanged(PushNotificationAction::Action action);
|
||||||
void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action);
|
void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action);
|
||||||
void groupChatNotificationActionChanged(PushNotificationAction::Action action);
|
void groupChatNotificationActionChanged(PushNotificationAction::Action action);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Comment[nl]=Rooms zoeken in NeoChat
|
|||||||
Comment[pl]=Znajdź pokoje w NeoChat
|
Comment[pl]=Znajdź pokoje w NeoChat
|
||||||
Comment[pt]=Procurar salas no NeoChat
|
Comment[pt]=Procurar salas no NeoChat
|
||||||
Comment[pt_BR]=Encontrar salas no NeoChat
|
Comment[pt_BR]=Encontrar salas no NeoChat
|
||||||
Comment[ru]=Поиск комнат NeoChat
|
Comment[ru]=Поиск комнаты NeoChat
|
||||||
Comment[sl]=Najdi sobe v NeoChatu
|
Comment[sl]=Najdi sobe v NeoChatu
|
||||||
Comment[sv]=Sök efter rum i NeoChat
|
Comment[sv]=Sök efter rum i NeoChat
|
||||||
Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும்
|
Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும்
|
||||||
|
|||||||
@@ -10,115 +10,188 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
ColumnLayout {
|
Loader {
|
||||||
id: root
|
id: attachmentPaneLoader
|
||||||
|
|
||||||
signal attachmentCancelled()
|
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath)
|
||||||
|
|
||||||
property string attachmentPath
|
|
||||||
|
|
||||||
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
|
|
||||||
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
||||||
|
readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath
|
||||||
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
|
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
|
||||||
|
|
||||||
RowLayout {
|
active: visible
|
||||||
spacing: Kirigami.Units.smallSpacing
|
sourceComponent: Component {
|
||||||
|
QQC2.Pane {
|
||||||
|
id: attachmentPane
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
|
||||||
QQC2.Label {
|
contentItem: Item {
|
||||||
Layout.fillWidth: true
|
property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing
|
||||||
Layout.alignment: Qt.AlignLeft
|
implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth)
|
||||||
text: i18n("Attachment:")
|
implicitHeight: Math.max(
|
||||||
horizontalAlignment: Text.AlignLeft
|
(hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0)
|
||||||
verticalAlignment: Text.AlignVCenter
|
+ fileInfoLayout.implicitHeight,
|
||||||
}
|
toolBar.implicitHeight
|
||||||
QQC2.ToolButton {
|
)
|
||||||
id: editImageButton
|
|
||||||
visible: hasImage
|
|
||||||
icon.name: "document-edit"
|
|
||||||
text: i18n("Edit")
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
Component {
|
Image {
|
||||||
id: imageEditorPage
|
id: image
|
||||||
ImageEditorPage {
|
property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8)
|
||||||
imagePath: root.attachmentPath
|
height: preferredHeight
|
||||||
|
anchors {
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
bottom: fileInfoLayout.top
|
||||||
|
bottomMargin: parent.spacing
|
||||||
|
}
|
||||||
|
width: Math.min(implicitWidth, attachmentPane.availableWidth)
|
||||||
|
asynchronous: true
|
||||||
|
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
||||||
|
smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops
|
||||||
|
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
|
||||||
|
visible: hasImage
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
||||||
|
onSourceChanged: {
|
||||||
|
// Reset source size height, which affect implicitHeight
|
||||||
|
sourceSize.height = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
onSourceSizeChanged: {
|
||||||
|
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
|
||||||
|
// This can save a lot of RAM when loading large images.
|
||||||
|
// It also improves visual quality for large images.
|
||||||
|
sourceSize.height = Kirigami.Units.gridUnit * 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "height"
|
||||||
|
duration: Kirigami.Units.shortDuration
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.BusyIndicator {
|
||||||
|
id: imageBusyIndicator
|
||||||
|
anchors {
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
top: parent.top
|
||||||
|
bottom: fileInfoLayout.top
|
||||||
|
bottomMargin: parent.spacing
|
||||||
|
}
|
||||||
|
visible: running
|
||||||
|
running: image.visible && image.progress < 1
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: fileInfoLayout
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: undefined
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
spacing: parent.spacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
id: mimetypeIcon
|
||||||
|
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight)
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
source: attachmentMimetype.iconName
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
id: fileLabel
|
||||||
|
text: baseFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
states: State {
|
||||||
|
when: !hasImage
|
||||||
|
AnchorChanges {
|
||||||
|
target: fileInfoLayout
|
||||||
|
anchors.bottom: undefined
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a toolbar to get a button spacing consistent with what the QQC2 style normally has
|
||||||
|
// Also has some accessibility info
|
||||||
|
QQC2.ToolBar {
|
||||||
|
id: toolBar
|
||||||
|
width: parent.width
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
|
||||||
|
Kirigami.Theme.inherit: true
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: parent.spacing
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.leftMargin: -attachmentPane.leftPadding
|
||||||
|
Layout.topMargin: -attachmentPane.topPadding
|
||||||
|
leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding
|
||||||
|
rightPadding: cancelAttachmentButton.rightPadding + 1
|
||||||
|
topPadding: cancelAttachmentButton.topPadding + attachmentPane.topPadding
|
||||||
|
bottomPadding: cancelAttachmentButton.bottomPadding
|
||||||
|
text: i18n("Attachment:")
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
background: Kirigami.ShadowedRectangle {
|
||||||
|
property real cornerRadius: cancelAttachmentButton.background.hasOwnProperty("radius") ?
|
||||||
|
Math.min(cancelAttachmentButton.background.radius, height/2) : 0
|
||||||
|
corners.bottomLeftRadius: toolBar.mirrored ? cornerRadius : 0
|
||||||
|
corners.bottomRightRadius: toolBar.mirrored ? 0 : cornerRadius
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
opacity: 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: editImageButton
|
||||||
|
visible: hasImage
|
||||||
|
icon.name: "document-edit"
|
||||||
|
text: i18n("Edit")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: imageEditorPage
|
||||||
|
ImageEditorPage {
|
||||||
|
imagePath: attachmentPaneLoader.attachmentPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
|
||||||
|
imageEditor.newPathChanged.connect(function(newPath) {
|
||||||
|
applicationWindow().pageStack.layers.pop();
|
||||||
|
attachmentPaneLoader.attachmentPath = newPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: cancelAttachmentButton
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
text: i18n("Cancel sending Image")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: currentRoom.chatBoxAttachmentPath = "";
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
background: Rectangle {
|
||||||
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
|
color: Kirigami.Theme.backgroundColor
|
||||||
imageEditor.newPathChanged.connect(function(newPath) {
|
|
||||||
applicationWindow().pageStack.layers.pop();
|
|
||||||
root.attachmentPath = newPath;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: cancelAttachmentButton
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18n("Cancel sending attachment")
|
|
||||||
icon.name: "dialog-close"
|
|
||||||
onTriggered: attachmentCancelled();
|
|
||||||
shortcut: "Escape"
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: image
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
|
|
||||||
asynchronous: true
|
|
||||||
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
|
||||||
source: hasImage ? root.attachmentPath : ""
|
|
||||||
visible: hasImage
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
|
|
||||||
onSourceChanged: {
|
|
||||||
// Reset source size height, which affect implicitHeight
|
|
||||||
sourceSize.height = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
onSourceSizeChanged: {
|
|
||||||
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
|
|
||||||
// This can save a lot of RAM when loading large images.
|
|
||||||
// It also improves visual quality for large images.
|
|
||||||
sourceSize.height = Kirigami.Units.gridUnit * 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.BusyIndicator {
|
|
||||||
id: imageBusyIndicator
|
|
||||||
|
|
||||||
visible: running
|
|
||||||
running: image.visible && image.progress < 1
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
id: fileInfoLayout
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: parent.spacing
|
|
||||||
|
|
||||||
Kirigami.Icon {
|
|
||||||
id: mimetypeIcon
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
||||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
||||||
source: attachmentMimetype.iconName
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
id: fileLabel
|
|
||||||
text: baseFileName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,60 +5,208 @@
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Window 2.15
|
||||||
|
|
||||||
import org.kde.kirigami 2.18 as Kirigami
|
import org.kde.kirigami 2.18 as Kirigami
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
QQC2.Control {
|
QQC2.ToolBar {
|
||||||
id: root
|
id: chatBar
|
||||||
|
property alias inputFieldText: inputField.text
|
||||||
property alias textField: textField
|
property alias textField: inputField
|
||||||
property bool isReplying: currentRoom.chatBoxReplyId.length > 0
|
property alias cursorPosition: inputField.cursorPosition
|
||||||
property NeoChatUser replyUser: currentRoom.chatBoxReplyUser
|
|
||||||
property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0
|
|
||||||
|
|
||||||
|
signal inputFieldForceActiveFocusTriggered()
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
|
|
||||||
property list<Kirigami.Action> actions : [
|
onInputFieldForceActiveFocusTriggered: {
|
||||||
Kirigami.Action {
|
inputField.forceActiveFocus();
|
||||||
id: attachmentAction
|
// set the cursor to the end of the text
|
||||||
|
inputField.cursorPosition = inputField.length;
|
||||||
|
}
|
||||||
|
|
||||||
property bool isBusy: currentRoom && currentRoom.hasFileUploading
|
position: QQC2.ToolBar.Footer
|
||||||
|
|
||||||
// Matrix does not allow sending attachments in replies
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0
|
|
||||||
icon.name: "mail-attachment"
|
|
||||||
text: i18n("Attach an image or file")
|
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
|
||||||
|
|
||||||
onTriggered: {
|
// Using a custom background because some styles like Material
|
||||||
if (Clipboard.hasImage) {
|
// or Fusion might have ugly colors for a TextArea placed inside
|
||||||
attachDialog.open()
|
// of a toolbar. ToolBar is otherwise the closest QQC2 component
|
||||||
} else {
|
// to what we want because of the padding and spacing values.
|
||||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
background: Rectangle {
|
||||||
fileDialog.chosen.connect((path) => {
|
color: Kirigami.Theme.backgroundColor
|
||||||
if (!path) {
|
}
|
||||||
return;
|
|
||||||
}
|
contentItem: RowLayout {
|
||||||
currentRoom.chatBoxAttachmentPath = path;
|
spacing: chatBar.spacing
|
||||||
})
|
|
||||||
fileDialog.open()
|
QQC2.ScrollView {
|
||||||
}
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: inputField.implicitHeight
|
||||||
|
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
|
||||||
|
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
|
||||||
|
+ inputField.topPadding + inputField.bottomPadding
|
||||||
|
|
||||||
|
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||||
|
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
FontMetrics {
|
||||||
|
id: fontMetrics
|
||||||
|
font: inputField.font
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
QQC2.TextArea {
|
||||||
},
|
id: inputField
|
||||||
Kirigami.Action {
|
focus: true
|
||||||
id: emojiAction
|
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
|
||||||
|
* Make sure there is no background since we are using the ToolBar background.
|
||||||
|
*
|
||||||
|
* This could cause a problem if the QQC2 style was designed around TextArea
|
||||||
|
* background colors being very different from the QPalette::Base color.
|
||||||
|
* Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles.
|
||||||
|
*/
|
||||||
|
background: MouseArea {
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
property bool isBusy: false
|
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||||
|
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
|
||||||
|
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message…")
|
||||||
|
verticalAlignment: TextEdit.AlignVCenter
|
||||||
|
horizontalAlignment: TextEdit.AlignLeft
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
|
||||||
|
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
|
||||||
|
color: Kirigami.Theme.textColor
|
||||||
|
selectionColor: Kirigami.Theme.highlightColor
|
||||||
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
|
hoverEnabled: !Kirigami.Settings.tabletMode
|
||||||
|
|
||||||
|
selectByMouse: !Kirigami.Settings.tabletMode
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
if (completionMenu.visible) {
|
||||||
|
completionMenu.complete()
|
||||||
|
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
inputField.insert(cursorPosition, "\n")
|
||||||
|
} else {
|
||||||
|
chatBar.postMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
if (completionMenu.visible) {
|
||||||
|
completionMenu.complete()
|
||||||
|
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
inputField.insert(cursorPosition, "\n")
|
||||||
|
} else {
|
||||||
|
chatBar.postMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onTabPressed: {
|
||||||
|
if (completionMenu.visible) {
|
||||||
|
completionMenu.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: {
|
||||||
|
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||||
|
chatBar.pasteImage();
|
||||||
|
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
||||||
|
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
|
||||||
|
if (replyEvent && replyEvent["event_id"]) {
|
||||||
|
currentRoom.chatBoxReplyId = replyEvent["event_id"]
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
|
||||||
|
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
|
||||||
|
if (editEvent) {
|
||||||
|
if(editEvent["m.relates_to"]) {
|
||||||
|
currentRoom.chatBoxReplyId = editEvent["m.relates_to"]["m.in_reply_to"]["event_id"];
|
||||||
|
}
|
||||||
|
currentRoom.chatBoxEditId = editEvent["event_id"]
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
|
||||||
|
completionMenu.decrementIndex()
|
||||||
|
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||||
|
completionMenu.incrementIndex()
|
||||||
|
} else if (event.key === Qt.Key_Backspace && inputField.text.length <= 1) {
|
||||||
|
currentRoom.sendTypingNotification(false)
|
||||||
|
repeatTimer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: repeatTimer
|
||||||
|
interval: 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
if (!repeatTimer.running && Config.typingNotifications) {
|
||||||
|
var textExists = text.length > 0
|
||||||
|
currentRoom.sendTypingNotification(textExists)
|
||||||
|
textExists ? repeatTimer.start() : repeatTimer.stop()
|
||||||
|
}
|
||||||
|
currentRoom.chatBoxText = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: currentRoom.chatBoxReplyId.length === 0 && (currentRoom.chatBoxAttachmentPath.length === 0 || uploadingBusySpinner.running)
|
||||||
|
implicitWidth: uploadButton.implicitWidth
|
||||||
|
implicitHeight: uploadButton.implicitHeight
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: uploadButton
|
||||||
|
anchors.fill: parent
|
||||||
|
// Matrix does not allow sending attachments in replies
|
||||||
|
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running
|
||||||
|
icon.name: "mail-attachment"
|
||||||
|
text: i18n("Attach an image or file")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (Clipboard.hasImage) {
|
||||||
|
attachDialog.open()
|
||||||
|
} else {
|
||||||
|
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||||
|
fileDialog.chosen.connect((path) => {
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentRoom.chatBoxAttachmentPath = path;
|
||||||
|
})
|
||||||
|
fileDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
QQC2.BusyIndicator {
|
||||||
|
id: uploadingBusySpinner
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: running
|
||||||
|
running: currentRoom && currentRoom.hasFileUploading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: emojiButton
|
||||||
icon.name: "smiley"
|
icon.name: "smiley"
|
||||||
text: i18n("Add an Emoji")
|
text: i18n("Add an Emoji")
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
onTriggered: {
|
onClicked: {
|
||||||
if (emojiDialog.visible) {
|
if (emojiDialog.visible) {
|
||||||
emojiDialog.close()
|
emojiDialog.close()
|
||||||
} else {
|
} else {
|
||||||
@@ -66,264 +214,29 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
QQC2.ToolTip.text: text
|
||||||
},
|
QQC2.ToolTip.visible: hovered
|
||||||
Kirigami.Action {
|
}
|
||||||
id: sendAction
|
|
||||||
|
|
||||||
property bool isBusy: false
|
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: sendButton
|
||||||
icon.name: "document-send"
|
icon.name: "document-send"
|
||||||
text: i18n("Send message")
|
text: i18n("Send message")
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
|
||||||
|
|
||||||
onTriggered: {
|
onClicked: {
|
||||||
root.postMessage()
|
chatBar.postMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
QQC2.ToolTip.text: text
|
||||||
}
|
QQC2.ToolTip.visible: hovered
|
||||||
]
|
|
||||||
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
|
|
||||||
contentItem: QQC2.ScrollView {
|
|
||||||
id: chatBarScrollView
|
|
||||||
|
|
||||||
property var textFieldHeight: textField.height
|
|
||||||
|
|
||||||
property var visualLeftPadding: (root.width - chatBoxMaxWidth) / 2 - (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing : 0)
|
|
||||||
property var visualRightPadding: (root.width - chatBoxMaxWidth) / 2 + (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0)
|
|
||||||
leftPadding: LayoutMirroring.enabled ? visualRightPadding : visualLeftPadding
|
|
||||||
rightPadding: LayoutMirroring.enabled ? visualLeftPadding : visualRightPadding
|
|
||||||
|
|
||||||
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
|
||||||
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
id: chatBarHeightAnimation
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.InOutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.TextArea{
|
|
||||||
id: textField
|
|
||||||
|
|
||||||
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
|
|
||||||
bottomPadding: Kirigami.Units.largeSpacing
|
|
||||||
leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing)
|
|
||||||
rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width
|
|
||||||
|
|
||||||
placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
|
|
||||||
verticalAlignment: TextEdit.AlignVCenter
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported)
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: repeatTimer
|
|
||||||
interval: 5000
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
if (!repeatTimer.running && Config.typingNotifications) {
|
|
||||||
var textExists = text.length > 0
|
|
||||||
currentRoom.sendTypingNotification(textExists)
|
|
||||||
textExists ? repeatTimer.start() : repeatTimer.stop()
|
|
||||||
}
|
|
||||||
currentRoom.chatBoxText = text
|
|
||||||
}
|
|
||||||
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
|
|
||||||
|
|
||||||
Keys.onEnterPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
|
||||||
textField.insert(cursorPosition, "\n")
|
|
||||||
} else {
|
|
||||||
chatBar.postMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onReturnPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
|
||||||
textField.insert(cursorPosition, "\n")
|
|
||||||
} else {
|
|
||||||
chatBar.postMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onPressed: {
|
|
||||||
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
|
||||||
chatBar.pasteImage();
|
|
||||||
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
|
||||||
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
|
|
||||||
if (replyEvent && replyEvent["event_id"]) {
|
|
||||||
currentRoom.chatBoxReplyId = replyEvent["event_id"]
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
|
|
||||||
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
|
|
||||||
if (editEvent) {
|
|
||||||
currentRoom.chatBoxEditId = editEvent["event_id"]
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
|
|
||||||
completionMenu.decrementIndex()
|
|
||||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
|
||||||
completionMenu.incrementIndex()
|
|
||||||
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
|
|
||||||
currentRoom.sendTypingNotification(false)
|
|
||||||
repeatTimer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: paneLoader
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
|
|
||||||
|
|
||||||
active: visible
|
|
||||||
visible: root.isReplying || root.attachmentPaneVisible
|
|
||||||
sourceComponent: root.isReplying ? replyPane : attachmentPane
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: replyPane
|
|
||||||
ReplyPane {
|
|
||||||
userName: root.replyUser ? root.replyUser.displayName : ""
|
|
||||||
userColor: root.replyUser ? root.replyUser.color : ""
|
|
||||||
userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : ""
|
|
||||||
text: currentRoom.chatBoxReplyMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: attachmentPane
|
|
||||||
AttachmentPane {
|
|
||||||
attachmentPath: currentRoom.chatBoxAttachmentPath
|
|
||||||
|
|
||||||
onAttachmentCancelled: {
|
|
||||||
currentRoom.chatBoxAttachmentPath = "";
|
|
||||||
root.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: MouseArea {
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
z: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because of the paneLoader we have to manage the scroll
|
|
||||||
* position manually or it doesn't keep the cursor visible properly all the time.
|
|
||||||
*/
|
|
||||||
function ensureVisible(r) {
|
|
||||||
// Find the child that is the Flickable created by ScrollView.
|
|
||||||
let flickable = undefined;
|
|
||||||
for (var index in children) {
|
|
||||||
if (children[index] instanceof Flickable) {
|
|
||||||
flickable = children[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flickable) {
|
|
||||||
if (flickable.contentX >= r.x) {
|
|
||||||
flickable.contentX = r.x;
|
|
||||||
} else if (flickable.contentX + width <= r.x + r.width) {
|
|
||||||
flickable.contentX = r.x + r.width - width;
|
|
||||||
} if (flickable.contentY >= r.y) {
|
|
||||||
flickable.contentY = r.y;
|
|
||||||
} else if (flickable.contentY + height <= r.y + r.height) {
|
|
||||||
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: cancelButton
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
|
|
||||||
|
|
||||||
visible: root.isReplying
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Cancel reply")
|
|
||||||
icon.name: "dialog-close"
|
|
||||||
onTriggered: {
|
|
||||||
currentRoom.chatBoxReplyId = "";
|
|
||||||
currentRoom.chatBoxAttachmentPath = "";
|
|
||||||
root.forceActiveFocus()
|
|
||||||
}
|
|
||||||
shortcut: "Escape"
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: actionsRow
|
|
||||||
padding: Kirigami.Units.smallSpacing
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
anchors.right: parent.right
|
|
||||||
property var requiredMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
|
|
||||||
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
|
|
||||||
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: Kirigami.Units.largeSpacing - 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.actions
|
|
||||||
Kirigami.Icon {
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
||||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
||||||
|
|
||||||
source: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
|
||||||
active: actionArea.containsPress
|
|
||||||
visible: modelData.visible
|
|
||||||
enabled: modelData.enabled
|
|
||||||
MouseArea {
|
|
||||||
id: actionArea
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: modelData.trigger()
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered
|
|
||||||
QQC2.ToolTip.text: modelData.tooltip
|
|
||||||
HoverHandler { id: hoverHandler }
|
|
||||||
|
|
||||||
QQC2.BusyIndicator {
|
|
||||||
anchors.fill: parent
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
visible: running
|
|
||||||
running: modelData.isBusy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiDialog {
|
EmojiDialog {
|
||||||
id: emojiDialog
|
id: emojiDialog
|
||||||
x: parent.width - implicitWidth
|
x: parent.width - implicitWidth
|
||||||
y: -implicitHeight // - Kirigami.Units.smallSpacing
|
y: -implicitHeight - Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
modal: false
|
modal: false
|
||||||
includeCustom: true
|
includeCustom: true
|
||||||
@@ -333,10 +246,6 @@ QQC2.Control {
|
|||||||
onClosed: if (emojiButton.checked) emojiButton.checked = false
|
onClosed: if (emojiButton.checked) emojiButton.checked = false
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
@@ -352,29 +261,26 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: currentRoom
|
||||||
|
function onChatBoxEditIdChanged() {
|
||||||
|
if (currentRoom.chatBoxEditMessage.length > 0) {
|
||||||
|
chatBar.inputFieldText = currentRoom.chatBoxEditMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChatDocumentHandler {
|
ChatDocumentHandler {
|
||||||
id: documentHandler
|
id: documentHandler
|
||||||
document: textField.textDocument
|
document: inputField.textDocument
|
||||||
cursorPosition: textField.cursorPosition
|
cursorPosition: inputField.cursorPosition
|
||||||
selectionStart: textField.selectionStart
|
selectionStart: inputField.selectionStart
|
||||||
selectionEnd: textField.selectionEnd
|
selectionEnd: inputField.selectionEnd
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
RoomManager.chatDocumentHandler = documentHandler;
|
RoomManager.chatDocumentHandler = documentHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceActiveFocus() {
|
|
||||||
textField.forceActiveFocus();
|
|
||||||
// set the cursor to the end of the text
|
|
||||||
textField.cursorPosition = textField.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertText(text) {
|
|
||||||
let initialCursorPosition = textField.cursorPosition;
|
|
||||||
|
|
||||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
|
|
||||||
textField.cursorPosition = initialCursorPosition + text.length
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteImage() {
|
function pasteImage() {
|
||||||
let localPath = Clipboard.saveImage();
|
let localPath = Clipboard.saveImage();
|
||||||
@@ -385,11 +291,12 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function postMessage() {
|
function postMessage() {
|
||||||
actionsHandler.handleNewMessage();
|
actionsHandler.handleMessage();
|
||||||
repeatTimer.stop()
|
repeatTimer.stop()
|
||||||
currentRoom.markAllMessagesAsRead();
|
currentRoom.markAllMessagesAsRead();
|
||||||
textField.clear();
|
inputField.clear();
|
||||||
currentRoom.chatBoxReplyId = "";
|
currentRoom.chatBoxReplyId = "";
|
||||||
|
currentRoom.chatBoxEditId = "";
|
||||||
messageSent()
|
messageSent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,108 @@
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: chatBox
|
id: chatBox
|
||||||
|
|
||||||
|
property alias inputFieldText: chatBar.inputFieldText
|
||||||
|
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
|
|
||||||
property alias chatBar: chatBar
|
|
||||||
|
|
||||||
readonly property int extraWidth: width >= Kirigami.Units.gridUnit * 47 ? Math.min((width - Kirigami.Units.gridUnit * 47), Kirigami.Units.gridUnit * 20) : 0
|
|
||||||
readonly property int chatBoxMaxWidth: Config.compactLayout ? width : Math.min(width, Kirigami.Units.gridUnit * 39 + extraWidth)
|
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.Separator {
|
||||||
|
id: connectionPaneSeparator
|
||||||
|
visible: connectionPane.visible
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 1 // So we can see the border
|
}
|
||||||
Layout.rightMargin: 1 // So we can see the border
|
|
||||||
|
|
||||||
text: i18n("NeoChat is offline. Please check your network connection.")
|
QQC2.Pane {
|
||||||
|
id: connectionPane
|
||||||
|
padding: fontMetrics.lineSpacing * 0.25
|
||||||
|
FontMetrics {
|
||||||
|
id: fontMetrics
|
||||||
|
font: networkLabel.font
|
||||||
|
}
|
||||||
|
spacing: 0
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
background: Rectangle {
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
}
|
||||||
visible: !Controller.isOnline
|
visible: !Controller.isOnline
|
||||||
|
Layout.fillWidth: true
|
||||||
|
QQC2.Label {
|
||||||
|
id: networkLabel
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: i18n("NeoChat is offline. Please check your network connection.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
|
id: replySeparator
|
||||||
|
visible: replyPane.visible
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyPane {
|
||||||
|
id: replyPane
|
||||||
|
visible: currentRoom.chatBoxReplyId.length > 0 || currentRoom.chatBoxEditId.length > 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onReplyCancelled: {
|
||||||
|
chatBox.focusInputField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
id: attachmentSeparator
|
||||||
|
visible: attachmentPane.visible
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachmentPane {
|
||||||
|
id: attachmentPane
|
||||||
|
visible: currentRoom.chatBoxAttachmentPath.length > 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
id: chatBarSeparator
|
||||||
|
visible: chatBar.visible
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatBar {
|
ChatBar {
|
||||||
id: chatBar
|
id: chatBar
|
||||||
|
|
||||||
visible: currentRoom.canSendEvent("m.room.message")
|
visible: currentRoom.canSendEvent("m.room.message")
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: implicitHeight + Kirigami.Units.largeSpacing
|
|
||||||
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
|
|
||||||
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding
|
|
||||||
|
|
||||||
FontMetrics {
|
|
||||||
id: chatBarFontMetrics
|
|
||||||
font: chatBar.textField.font
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessageSent: {
|
onMessageSent: {
|
||||||
chatBox.messageSent();
|
chatBox.messageSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "implicitHeight"
|
||||||
|
duration: Kirigami.Units.shortDuration
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertText(str) {
|
||||||
|
let index = chatBar.cursorPosition;
|
||||||
|
chatBox.inputFieldText = inputFieldText.substr(0, chatBar.cursorPosition) + str + inputFieldText.substr(chatBar.cursorPosition);
|
||||||
|
chatBar.cursorPosition = index + str.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInputField() {
|
||||||
|
chatBar.inputFieldForceActiveFocusTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ QQC2.Popup {
|
|||||||
connection: Controller.activeConnection
|
connection: Controller.activeConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
property var chatDocumentHandler
|
required property var chatDocumentHandler
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
chatDocumentHandler.completionModel.roomListModel = roomListModel;
|
chatDocumentHandler.completionModel.roomListModel = roomListModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,82 +10,108 @@ import org.kde.kirigami 2.14 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
GridLayout {
|
Loader {
|
||||||
id: root
|
id: replyPane
|
||||||
property string userName
|
property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
|
||||||
property color userColor: Kirigami.Theme.highlightColor
|
|
||||||
property var userAvatar: ""
|
|
||||||
property var text
|
|
||||||
|
|
||||||
rows: 3
|
signal replyCancelled()
|
||||||
columns: 3
|
|
||||||
rowSpacing: Kirigami.Units.smallSpacing
|
|
||||||
columnSpacing: Kirigami.Units.largeSpacing
|
|
||||||
|
|
||||||
QQC2.Label {
|
active: visible
|
||||||
id: replyLabel
|
sourceComponent: QQC2.Pane {
|
||||||
Layout.fillWidth: true
|
id: replyPane
|
||||||
Layout.alignment: Qt.AlignLeft
|
|
||||||
Layout.columnSpan: 3
|
|
||||||
topPadding: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
text: i18n("Replying to:")
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
id: verticalBorder
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
spacing: leftPadding
|
||||||
Layout.rowSpan: 2
|
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.smallSpacing
|
contentItem: RowLayout {
|
||||||
color: userColor
|
Layout.fillWidth: true
|
||||||
}
|
spacing: replyPane.spacing
|
||||||
Kirigami.Avatar {
|
|
||||||
id: replyAvatar
|
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
FontMetrics {
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
id: fontMetrics
|
||||||
|
font: textArea.font
|
||||||
|
}
|
||||||
|
|
||||||
source: userAvatar
|
Kirigami.Avatar {
|
||||||
name: userName
|
id: avatar
|
||||||
color: userColor
|
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
|
||||||
}
|
Layout.preferredWidth: Layout.preferredHeight
|
||||||
QQC2.Label {
|
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
|
||||||
Layout.fillWidth: true
|
source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : ""
|
||||||
Layout.alignment: Qt.AlignLeft
|
name: user ? user.displayName : ""
|
||||||
|
color: user ? user.color : "transparent"
|
||||||
|
visible: Boolean(user)
|
||||||
|
}
|
||||||
|
|
||||||
color: userColor
|
ColumnLayout {
|
||||||
text: userName
|
id: textContentLayout
|
||||||
elide: Text.ElideRight
|
Layout.alignment: Qt.AlignCenter
|
||||||
}
|
Layout.fillWidth: true
|
||||||
QQC2.TextArea {
|
spacing: fontMetrics.leading
|
||||||
id: textArea
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: {
|
||||||
|
let heading = "<b>%1</b>"
|
||||||
|
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
|
||||||
|
if (currentRoom.chatBoxEditId.length > 0) {
|
||||||
|
heading = heading.arg(i18n("Editing message:")) + "<br/>"
|
||||||
|
} else {
|
||||||
|
heading = heading.arg(i18n("Replying to %1:", userName))
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
return heading
|
||||||
Layout.columnSpan: 2
|
}
|
||||||
|
}
|
||||||
|
//TODO edit user mentions
|
||||||
|
QQC2.ScrollView {
|
||||||
|
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
|
||||||
|
|
||||||
leftPadding: 0
|
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||||
rightPadding: 0
|
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
QQC2.TextArea {
|
||||||
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
|
id: textArea
|
||||||
selectByMouse: true
|
leftPadding: 0
|
||||||
selectByKeyboard: true
|
rightPadding: 0
|
||||||
readOnly: true
|
topPadding: 0
|
||||||
wrapMode: QQC2.Label.Wrap
|
bottomPadding: 0
|
||||||
textFormat: TextEdit.RichText
|
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage)
|
||||||
background: Item {}
|
selectByMouse: true
|
||||||
HoverHandler {
|
selectByKeyboard: true
|
||||||
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
readOnly: true
|
||||||
|
wrapMode: QQC2.Label.Wrap
|
||||||
|
textFormat: TextEdit.RichText
|
||||||
|
background: Item {}
|
||||||
|
HoverHandler {
|
||||||
|
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
action: Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Cancel reply")
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
onTriggered: {
|
||||||
|
currentRoom.chatBoxReplyId = "";
|
||||||
|
currentRoom.chatBoxEditId = "";
|
||||||
|
}
|
||||||
|
shortcut: "Escape"
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextMetrics {
|
background: Rectangle {
|
||||||
id: replyTextMetrics
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
|
||||||
text: root.text
|
|
||||||
font: textArea.font
|
|
||||||
elide: Qt.ElideRight
|
|
||||||
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,4 @@ Kirigami.LoadingPlaceholder {
|
|||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: i18n("Please wait. This might take a little while.")
|
text: i18n("Please wait. This might take a little while.")
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Controller
|
|
||||||
function onInitiated() {
|
|
||||||
closeDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,24 +27,23 @@ TimelineContainer {
|
|||||||
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
|
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
|
||||||
|
|
||||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
||||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
|
||||||
|
|
||||||
innerObject: AnimatedImage {
|
innerObject: AnimatedImage {
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
property var imageWidth: {
|
property var imageWidth: {
|
||||||
if (imageDelegate.info && imageDelegate.info.w && imageDelegate.info.w > 0) {
|
if (imageDelegate.info.w > 0) {
|
||||||
return imageDelegate.info.w;
|
return imageDelegate.info.w;
|
||||||
} else if (sourceSize.width && sourceSize.width > 0) {
|
} else if (sourceSize.width > 0) {
|
||||||
return sourceSize.width;
|
return sourceSize.width;
|
||||||
} else {
|
} else {
|
||||||
return imageDelegate.contentMaxWidth;
|
return imageDelegate.contentMaxWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var imageHeight: {
|
property var imageHeight: {
|
||||||
if (imageDelegate.info && imageDelegate.info.h && imageDelegate.info.h > 0) {
|
if (imageDelegate.info.h > 0) {
|
||||||
return imageDelegate.info.h;
|
return imageDelegate.info.h;
|
||||||
} else if (sourceSize.height && sourceSize.height > 0) {
|
} else if (sourceSize.height > 0) {
|
||||||
return sourceSize.height;
|
return sourceSize.height;
|
||||||
} else {
|
} else {
|
||||||
// Default to a 16:9 placeholder
|
// Default to a 16:9 placeholder
|
||||||
@@ -52,29 +51,8 @@ TimelineContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var aspectRatio: imageWidth / imageHeight
|
Layout.maximumWidth: Math.min(imageDelegate.contentMaxWidth, imageDelegate.maxWidth)
|
||||||
/**
|
Layout.maximumHeight: Math.min(imageDelegate.contentMaxWidth / imageWidth * imageHeight, imageDelegate.maxWidth / imageWidth * imageHeight)
|
||||||
* Whether the image should be limited by height or width.
|
|
||||||
* We need to prevent excessively tall as well as excessively wide media.
|
|
||||||
*
|
|
||||||
* @note In the case of a tie the media is width limited.
|
|
||||||
*/
|
|
||||||
readonly property bool limitWidth: imageWidth >= imageHeight
|
|
||||||
|
|
||||||
readonly property size maxSize: {
|
|
||||||
if (limitWidth) {
|
|
||||||
let width = Math.min(imageDelegate.contentMaxWidth, imageDelegate.maxWidth);
|
|
||||||
let height = width / aspectRatio;
|
|
||||||
return Qt.size(width, height);
|
|
||||||
} else {
|
|
||||||
let height = Math.min(imageDelegate.maxHeight, imageDelegate.contentMaxWidth / aspectRatio);
|
|
||||||
let width = height * aspectRatio;
|
|
||||||
return Qt.size(width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.maximumWidth: maxSize.width
|
|
||||||
Layout.maximumHeight: maxSize.height
|
|
||||||
Layout.preferredWidth: imageWidth
|
Layout.preferredWidth: imageWidth
|
||||||
Layout.preferredHeight: imageHeight
|
Layout.preferredHeight: imageHeight
|
||||||
source: model.mediaUrl
|
source: model.mediaUrl
|
||||||
@@ -91,8 +69,6 @@ TimelineContainer {
|
|||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
paused: !applicationWindow().active
|
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
id: hoverHandler
|
id: hoverHandler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
||||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
@@ -10,140 +9,58 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
Loader {
|
RowLayout {
|
||||||
id: root
|
id: row
|
||||||
|
|
||||||
/**
|
property var links: model.display.match(/\bhttps?:\/\/[^\s\<\>\"\']+/g)
|
||||||
* @brief Get a list of hyperlinks in the text.
|
// don't show previews for room links or user mentions or custom emojis
|
||||||
*
|
.filter(link => !(
|
||||||
* User links i.e. anything starting with https://matrix.to are ignored.
|
link.includes("https://matrix.to") || link.includes("/_matrix/media/r0/download/")
|
||||||
*/
|
))
|
||||||
property var links: {
|
// remove ending fullstops and commas
|
||||||
let matches = model.display.match(/\bhttps?:\/\/[^\s\<\>\"\']+/g)
|
.map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link)
|
||||||
if (matches && matches.length > 0) {
|
|
||||||
// don't show previews for room links or user mentions or custom emojis
|
|
||||||
return matches.filter(link => !(
|
|
||||||
link.includes("https://matrix.to") || link.includes("/_matrix/media/r0/download/")
|
|
||||||
))
|
|
||||||
// remove ending fullstops and commas
|
|
||||||
.map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
|
|
||||||
}
|
|
||||||
LinkPreviewer {
|
LinkPreviewer {
|
||||||
id: linkPreviewer
|
id: lp
|
||||||
url: root.links && root.links.length > 0 ? root.links[0] : ""
|
url: links.length > 0 ? links[0] : ""
|
||||||
}
|
}
|
||||||
|
visible: lp.loaded && lp.title
|
||||||
/**
|
Rectangle {
|
||||||
* @brief Standard height for the link preview.
|
Layout.fillHeight: true
|
||||||
*
|
width: Kirigami.Units.smallSpacing
|
||||||
* When the content of the link preview is larger than this it will be
|
visible: lp.loaded && lp.title
|
||||||
* elided/hidden until maximized.
|
color: Kirigami.Theme.highlightColor
|
||||||
*/
|
|
||||||
property var defaultHeight : Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
|
||||||
|
|
||||||
active: !currentRoom.usesEncryption && model.display && links && links.length > 0
|
|
||||||
visible: Config.showLinkPreview && active
|
|
||||||
sourceComponent: linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: linkPreviewComponent
|
|
||||||
QQC2.Control {
|
|
||||||
id: componentRoot
|
|
||||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
|
||||||
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
width: Kirigami.Units.smallSpacing
|
|
||||||
color: Kirigami.Theme.highlightColor
|
|
||||||
}
|
|
||||||
Image {
|
|
||||||
visible: linkPreviewer.imageSource
|
|
||||||
Layout.maximumHeight: root.defaultHeight
|
|
||||||
Layout.maximumWidth: root.defaultHeight
|
|
||||||
source: linkPreviewer.imageSource.replace("mxc://", "image://mxc/")
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
id: column
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
Kirigami.Heading {
|
|
||||||
id: linkPreviewTitle
|
|
||||||
Layout.fillWidth: true
|
|
||||||
level: 3
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
textFormat: Text.RichText
|
|
||||||
text: "<style>
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
Image {
|
||||||
<a href=\"" + root.links[0] + "\">" + (maximizeButton.checked ? linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
visible: lp.imageSource
|
||||||
onLinkActivated: RoomManager.openResource(link)
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
|
||||||
|
Layout.maximumWidth: Kirigami.Units.gridUnit * 5
|
||||||
TextMetrics {
|
source: lp.imageSource.replace("mxc://", "image://mxc/")
|
||||||
id: titleTextMetrics
|
fillMode: Image.PreserveAspectFit
|
||||||
text: linkPreviewer.title
|
}
|
||||||
font: linkPreviewTitle.font
|
ColumnLayout {
|
||||||
elide: Text.ElideRight
|
id: column
|
||||||
elideWidth: (linkPreviewTitle.width - Kirigami.Units.largeSpacing * 2.5) * 3
|
spacing: Kirigami.Units.smallSpacing
|
||||||
}
|
Kirigami.Heading {
|
||||||
}
|
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
|
||||||
QQC2.Label {
|
Layout.fillWidth: true
|
||||||
id: linkPreviewDescription
|
level: 4
|
||||||
Layout.fillWidth: true
|
wrapMode: Text.Wrap
|
||||||
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
textFormat: Text.RichText
|
||||||
visible: linkPreviewTitle.height + column.spacing <= root.defaultHeight || maximizeButton.checked
|
text: "<style>
|
||||||
text: linkPreviewer.description
|
a {
|
||||||
wrapMode: Text.Wrap
|
text-decoration: none;
|
||||||
elide: Text.ElideRight
|
}
|
||||||
}
|
</style>
|
||||||
}
|
<a href=\"" + links[0] + "\">" + lp.title.replace("–", "—") + "</a>"
|
||||||
}
|
visible: lp.loaded
|
||||||
|
onLinkActivated: RoomManager.openResource(link)
|
||||||
QQC2.Button {
|
|
||||||
id: maximizeButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
visible: componentRoot.hovered && (componentRoot.truncated || checked)
|
|
||||||
checkable: true
|
|
||||||
icon.name: checked ? "go-up" : "go-down"
|
|
||||||
|
|
||||||
QQC2.ToolTip.text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
QQC2.Label {
|
||||||
|
text: lp.description
|
||||||
Component {
|
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
|
||||||
id: loadingComponent
|
Layout.fillWidth: true
|
||||||
RowLayout {
|
wrapMode: Text.Wrap
|
||||||
property bool hovered: false
|
visible: lp.loaded && lp.description
|
||||||
property bool truncated: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
width: Kirigami.Units.smallSpacing
|
|
||||||
color: Kirigami.Theme.highlightColor
|
|
||||||
}
|
|
||||||
QQC2.BusyIndicator { }
|
|
||||||
Kirigami.Heading {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: root.defaultHeight
|
|
||||||
level: 2
|
|
||||||
text: i18n("Loading URL preview")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,24 +21,16 @@ TimelineContainer {
|
|||||||
RichLabel {
|
RichLabel {
|
||||||
id: label
|
id: label
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: currentRoom.chatBoxEditId !== model.eventId
|
|
||||||
isEmote: messageDelegate.isEmote
|
isEmote: messageDelegate.isEmote
|
||||||
}
|
}
|
||||||
MessageEditComponent {
|
Loader {
|
||||||
|
id: linkPreviewLoader
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
messageId: model.eventId
|
active: !currentRoom.usesEncryption && model.display && model.display.includes("http")
|
||||||
visible: currentRoom.chatBoxEditId === model.eventId
|
visible: Config.showLinkPreview && active
|
||||||
onVisibleChanged: {
|
sourceComponent: LinkPreviewDelegate {
|
||||||
if (visible) {
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
editChatDocumentHandler.document = textDocument
|
|
||||||
editChatDocumentHandler.cursorPosition = cursorPosition
|
|
||||||
editChatDocumentHandler.selectionStart = selectionStart
|
|
||||||
editChatDocumentHandler.selectionEnd = selectionEnd
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LinkPreviewDelegate {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
||||||
|
|
||||||
import QtQuick 2.15
|
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
|
||||||
import QtQuick.Layouts 1.15
|
|
||||||
|
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
|
||||||
import org.kde.neochat 1.0
|
|
||||||
|
|
||||||
QQC2.TextArea {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string messageId
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: editButtons.height + topPadding + bottomPadding
|
|
||||||
Layout.preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
|
|
||||||
rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2
|
|
||||||
|
|
||||||
color: Kirigami.Theme.textColor
|
|
||||||
verticalAlignment: TextEdit.AlignVCenter
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
forceActiveFocus();
|
|
||||||
root.cursorPosition = root.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onTextChanged: {
|
|
||||||
currentRoom.editText = text
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEnterPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
|
||||||
root.insert(cursorPosition, "\n")
|
|
||||||
} else {
|
|
||||||
root.postEdit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onReturnPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
|
||||||
root.insert(cursorPosition, "\n")
|
|
||||||
} else {
|
|
||||||
root.postEdit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onPressed: {
|
|
||||||
if (event.key === Qt.Key_Up && completionMenu.visible) {
|
|
||||||
completionMenu.decrementIndex()
|
|
||||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
|
||||||
completionMenu.incrementIndex()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is anchored like this so that control expands properly as the edited
|
|
||||||
* text grows in length.
|
|
||||||
*/
|
|
||||||
RowLayout {
|
|
||||||
id: editButtons
|
|
||||||
anchors.verticalCenter: root.verticalCenter
|
|
||||||
anchors.right: root.right
|
|
||||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
|
||||||
spacing: 0
|
|
||||||
QQC2.ToolButton {
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Confirm edit")
|
|
||||||
icon.name: "checkmark"
|
|
||||||
onTriggered: {
|
|
||||||
root.postEdit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Cancel edit")
|
|
||||||
icon.name: "dialog-close"
|
|
||||||
onTriggered: {
|
|
||||||
currentRoom.chatBoxEditId = "";
|
|
||||||
}
|
|
||||||
shortcut: "Escape"
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: currentRoom
|
|
||||||
function onChatBoxEditIdChanged() {
|
|
||||||
if (currentRoom.chatBoxEditId == messageId && currentRoom.chatBoxEditMessage.length > 0) {
|
|
||||||
root.text = currentRoom.chatBoxEditMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionMenu {
|
|
||||||
id: completionMenu
|
|
||||||
height: implicitHeight
|
|
||||||
y: -height - 5
|
|
||||||
z: 10
|
|
||||||
chatDocumentHandler: documentHandler
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: editTextMetrics
|
|
||||||
text: root.text
|
|
||||||
}
|
|
||||||
|
|
||||||
function postEdit() {
|
|
||||||
actionsHandler.handleEdit();
|
|
||||||
root.clear();
|
|
||||||
currentRoom.chatBoxEditId = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,13 +11,13 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
TextEdit {
|
TextEdit {
|
||||||
id: contentLabel
|
id: contentLabel
|
||||||
|
|
||||||
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
|
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
|
||||||
readonly property var hasSpoiler: /data-mx-spoiler/g
|
readonly property var hasSpoiler: /data-mx-spoiler/g
|
||||||
|
|
||||||
property bool isEmote: false
|
property bool isEmote: false
|
||||||
property bool isReplyLabel: false
|
property bool isReplyLabel: false
|
||||||
|
|
||||||
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g
|
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g
|
||||||
property string textMessage: model.display.includes("http")
|
property string textMessage: model.display.includes("http")
|
||||||
? model.display.replace(linkRegex, function() {
|
? model.display.replace(linkRegex, function() {
|
||||||
if (arguments[0].includes("/_matrix/media/r0/download/")) {
|
if (arguments[0].includes("/_matrix/media/r0/download/")) {
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
||||||
|
|
||||||
import QtQuick 2.15
|
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
|
||||||
import QtQuick.Layouts 1.15
|
|
||||||
|
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat 1.0
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: root
|
|
||||||
property var name
|
|
||||||
property alias avatar: stateAvatar.source
|
|
||||||
property var color
|
|
||||||
property alias text: label.text
|
|
||||||
|
|
||||||
signal avatarClicked()
|
|
||||||
signal linkClicked(string link)
|
|
||||||
|
|
||||||
implicitHeight: Math.max(label.contentHeight, stateAvatar.implicitHeight)
|
|
||||||
|
|
||||||
Kirigami.Avatar {
|
|
||||||
id: stateAvatar
|
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
|
||||||
|
|
||||||
name: root.name
|
|
||||||
color: root.color
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: avatarClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
id: label
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
textFormat: Text.RichText
|
|
||||||
onLinkActivated: linkClicked(link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,9 +51,6 @@ QQC2.Control {
|
|||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: columnLayout
|
id: columnLayout
|
||||||
|
|
||||||
property bool folded: true
|
|
||||||
|
|
||||||
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
|
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
|
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
|
||||||
@@ -66,73 +63,45 @@ QQC2.Control {
|
|||||||
visible: sectionVisible
|
visible: sectionVisible
|
||||||
labelText: sectionVisible ? section : ""
|
labelText: sectionVisible ? section : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: rowLayout
|
||||||
|
implicitHeight: label.contentHeight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
visible: stateEventRepeater.count !== 1
|
|
||||||
|
|
||||||
Flow {
|
Kirigami.Avatar {
|
||||||
visible: columnLayout.folded
|
id: icon
|
||||||
spacing: -Kirigami.Units.iconSizes.small / 2
|
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||||
Repeater {
|
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||||
model: authorList
|
|
||||||
delegate: Kirigami.Avatar {
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
|
||||||
|
|
||||||
name: modelData.displayName
|
name: author.displayName
|
||||||
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
||||||
color: modelData.color
|
color: author.color
|
||||||
}
|
|
||||||
|
Component {
|
||||||
|
id: userDetailDialog
|
||||||
|
|
||||||
|
UserDetailDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: columnLayout.folded
|
|
||||||
|
|
||||||
text: aggregateDisplay
|
QQC2.Label {
|
||||||
elide: Qt.ElideRight
|
id: label
|
||||||
textFormat: Text.RichText
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
textFormat: Text.RichText
|
||||||
|
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${model.authorDisplayName}</a> ${aggregateDisplay}`
|
||||||
onLinkActivated: RoomManager.openResource(link)
|
onLinkActivated: RoomManager.openResource(link)
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: !columnLayout.folded
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
icon.name: (!columnLayout.folded ? "go-up" : "go-down")
|
|
||||||
icon.width: Kirigami.Units.iconSizes.small
|
|
||||||
icon.height: Kirigami.Units.iconSizes.small
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
columnLayout.toggleFolded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Repeater {
|
|
||||||
id: stateEventRepeater
|
|
||||||
model: stateEvents
|
|
||||||
delegate: StateComponent {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
||||||
visible: !columnLayout.folded || stateEventRepeater.count === 1
|
|
||||||
|
|
||||||
name: modelData.author.displayName
|
|
||||||
avatar: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
|
|
||||||
color: modelData.author.color
|
|
||||||
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${modelData.author.id}" style="color: ${modelData.author.color}">${modelData.authorDisplayName}</a> ${modelData.text}`
|
|
||||||
|
|
||||||
onAvatarClicked: RoomManager.openResource("https://matrix.to/#/" + modelData.author.id)
|
|
||||||
onLinkClicked: RoomManager.openResource(link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFolded() {
|
|
||||||
folded = !folded
|
|
||||||
foldedChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ ColumnLayout {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
|
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
|
||||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)
|
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user