Compare commits

..

24 Commits

Author SHA1 Message Date
l10n daemon script
13042d9ba6 GIT_SILENT Sync po/docbooks with svn 2025-12-04 03:20:39 +00:00
Heiko Becker
0e4b52ee62 GIT_SILENT Update Appstream for new release 2025-12-04 00:19:55 +01:00
Heiko Becker
4c32280343 GIT_SILENT Upgrade release service version to 25.12.0. 2025-12-03 23:04:35 +01:00
l10n daemon script
a2e540d6ef GIT_SILENT Sync po/docbooks with svn 2025-12-03 03:26:04 +00:00
Carl Schwan
573c8925d2 Fix krunner integration with Flatpak
(cherry picked from commit 1da24191f0)

Co-authored-by: renner 03 <renner0@posteo.de>
2025-12-02 23:00:25 +01:00
l10n daemon script
6c0bd850b0 GIT_SILENT Sync po/docbooks with svn 2025-12-01 03:28:35 +00:00
l10n daemon script
1da9719314 GIT_SILENT Sync po/docbooks with svn 2025-11-25 03:15:21 +00:00
Heiko Becker
3cd5f3a1c6 Drop unused dependencies
Both KF6Crash and KF6IconThemes aren't used anymore after porting to
KirigamiApp in eab45e761a.

(cherry picked from commit 0cb3fd32f4)
2025-11-24 21:56:55 +01:00
Albert Astals Cid
b4108f2eef GIT_SILENT Upgrade release service version to 25.11.90. 2025-11-24 00:57:39 +01:00
l10n daemon script
bb7de18341 GIT_SILENT Sync po/docbooks with svn 2025-11-23 03:19:33 +00:00
l10n daemon script
7b8328fce6 GIT_SILENT Sync po/docbooks with svn 2025-11-21 03:20:12 +00:00
l10n daemon script
aeee6570c0 GIT_SILENT Sync po/docbooks with svn 2025-11-20 03:14:24 +00:00
l10n daemon script
780b9a6f9b GIT_SILENT Sync po/docbooks with svn 2025-11-17 15:49:57 +00:00
l10n daemon script
0ba06882d1 GIT_SILENT Sync po/docbooks with svn 2025-11-17 03:16:16 +00:00
l10n daemon script
ce131a53e5 GIT_SILENT Sync po/docbooks with svn 2025-11-16 03:16:40 +00:00
l10n daemon script
e9421e28dd SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-11-16 03:13:00 +00:00
l10n daemon script
1c64a6b5f0 GIT_SILENT Sync po/docbooks with svn 2025-11-15 03:19:51 +00:00
l10n daemon script
9b5200c344 GIT_SILENT Sync po/docbooks with svn 2025-11-14 03:21:54 +00:00
l10n daemon script
5baf4ab823 GIT_SILENT Sync po/docbooks with svn 2025-11-13 03:20:11 +00:00
l10n daemon script
58a72a08f2 GIT_SILENT Sync po/docbooks with svn 2025-11-11 03:37:16 +00:00
l10n daemon script
c9b97d4d0d GIT_SILENT Sync po/docbooks with svn 2025-11-10 03:26:49 +00:00
Carl Schwan
2a7d61c73b Improve invited room counting
I didn't realize when redoing the tooltip for DMs that directChatInvites
was a boolean, not an integer type. Now it's changed to an integer type,
which fixes the DM invite count.

NeoChat apparently didn't count normal room invites until now either, so
now Home is highlighted in that case. Now it should be harder to miss
these kinds of invites.


(cherry picked from commit 887865c0aa)

Co-authored-by: Joshua Goins <josh@redstrate.com>
2025-11-09 22:23:09 +01:00
l10n daemon script
f55bd28e10 GIT_SILENT Sync po/docbooks with svn 2025-11-09 03:45:49 +00:00
Albert Astals Cid
2a67861099 GIT_SILENT Upgrade release service version to 25.11.80. 2025-11-06 17:40:37 +01:00
157 changed files with 28097 additions and 54716 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.10",
"runtime-version": "6.9",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -31,6 +31,19 @@
"/share/ndk-modules"
],
"modules": [
{
"name": "kirigamiaddons",
"config-opts": [
"-DBUILD_TESTING=OFF"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/libraries/kirigami-addons.git"
}
]
},
{
"name": "opencv",
"config-opts": [
@@ -65,7 +78,6 @@
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DOLM_TESTS=OFF"
],
"sources": [
@@ -172,8 +184,8 @@
"sources": [
{
"type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.3/src/kunifiedpush-25.08.3.tar.xz",
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a",
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz",
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518",
"x-checker-data": {
"type": "anitya",
"project-id": 8763,

View File

@@ -10,7 +10,6 @@ Dependencies:
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "26")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "12")
set(RELEASE_SERVICE_VERSION_MICRO "0")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -46,6 +46,10 @@ if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
@@ -65,7 +69,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme IconThemes)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"

View File

@@ -63,7 +63,7 @@ void ActionsTest::testActions_data()
QTest::addColumn<std::optional<QString>>("resultText");
QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type");
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\\\_(ツ)\\_/¯ Hello"_s)
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\_(ツ)_/¯ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("lenny") << u"/lenny Hello"_s << std::make_optional(u"( ͡° ͜ʖ ͡°) Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s)

View File

@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0);
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
}
void TimelineMessageModelTest::cleanup()

14
cmake/Flatpak.cmake Normal file
View File

@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the
# KDE Flatpak SDK.
install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -320,6 +320,7 @@
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::supporters">Anonymous donor, Akseli</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -487,7 +488,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="25.12.1" date="2026-01-08"/>
<release version="25.12.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/>
<release version="25.08.2" date="2025-10-09"/>

View File

@@ -108,7 +108,6 @@ Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[ko]=Matrix에서 대화하기
Comment[lt]=Pokalbiai per Matrix
Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,122 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Brazilian-Portuguese "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Manual do Usuário do NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat man page.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01/11/2022</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Cliente para interação com o protocolo de mensagens Matrix.</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Descrição</title>
<para
>O <command
>neochat</command
> é um aplicativo de bate-papo para o protocolo Matrix. Ele funciona tanto em computadores quanto em dispositivos móveis. </para>
</refsect1>
<refsect1 id="options"
><title
>Opções</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>O URI da matriz para um usuário ou uma sala. Por exemplo, matrix:u/usuário:exemplo.org e matrix:r/root:exemplo.org. Isso fará com que o NeoChat tente abrir a sala ou conversa especificada. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Relatar bugs</title>
<para
>Você pode reportar erros e solicitar novas funcionalidades em <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Veja também</title>
<simplelist>
<member
>Lista de perguntas frequentes sobre o Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Direitos autorais</title>
<para
>Direitos autorais &copy; 2020-2022 Tobias Fella </para>
<para
>Direitos autorais &copy; 2020-2022 Carl Schwan </para>
<para
>Licença: GNU General Public Versão 3 ou posterior <ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>carl@carlschwan.eu</email
></author>
<date
>20221101</date>
>2022-11-01</date>
<releaseinfo
>22.09</releaseinfo>
<productname
@@ -111,9 +111,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
><title
>Telif Hakkı</title>
<para
>Telif hakkı &copy; 20202022 Tobias Fella </para>
>Telif hakkı &copy; 2020-2022 Tobias Fella </para>
<para
>Telif hakkı &copy; 20202022 Carl Schwan </para>
>Telif hakkı &copy; 2020-2022 Carl Schwan </para>
<para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(neochat STATIC
add_library(neochat STATIC
controller.cpp
controller.h
roommanager.cpp
@@ -35,10 +35,6 @@ qt_add_library(neochat STATIC
models/commonroomsmodel.h
texttospeechhelper.h
texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -108,8 +104,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/NewPollDialog.qml
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -146,17 +140,10 @@ if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
qt_add_executable(neochat-app
add_executable(neochat-app
main.cpp
)
if(ANDROID)
set_target_properties(neochat-app PROPERTIES
OUTPUT_NAME "neochat-app"
PREFIX "lib"
)
endif()
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
@@ -166,7 +153,6 @@ target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
neochat
KF6::IconThemes
)
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -197,7 +183,7 @@ else()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_link_libraries(neochat PRIVATE neochatplugin Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PUBLIC
LibNeoChat
Timeline
@@ -220,7 +206,6 @@ target_link_libraries(neochat PUBLIC
KF6::ItemModels
KF6::I18nQml
KirigamiApp
KirigamiAddonsComponents
QuotientQt6
Login
Rooms

View File

@@ -246,10 +246,7 @@ void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatC
if (newConnection) {
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
// Refresh and update manually, in case we init too late for the badge count to actually change.
newConnection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(newConnection->badgeNotificationCount());
}
Q_EMIT activeConnectionChanged(newConnection);
}

View File

@@ -33,7 +33,6 @@
#include <KWindowSystem>
#endif
#include <KIconTheme>
#include <KLocalizedQmlContext>
#include <KLocalizedString>
#include <KirigamiApp>
@@ -103,10 +102,6 @@ int main(int argc, char *argv[])
{
QNetworkProxyFactory::setUseSystemConfiguration(true);
// We currently need to do this ourselves,
// KirigamiApp currently called this after constructing the app which breaks icons on Windows.
KIconTheme::initTheme();
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
@@ -166,6 +161,12 @@ int main(int argc, char *argv[])
Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy(u"/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"_s, u"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"_s);
#endif
ColorSchemer colorScheme;
QCommandLineParser parser;

View File

@@ -5,7 +5,6 @@
#include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication>
#include <Quotient/room.h>
using namespace Quotient;
@@ -40,22 +39,8 @@ void CommonRoomsModel::setUserId(const QString &userId)
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{
auto roomId = m_commonRooms[index.row()];
auto room = connection()->room(roomId);
if (!room) {
return {};
}
switch (roleName) {
case Qt::DisplayRole:
case RoomNameRole:
return room->displayName();
case RoomAvatarRole:
return room->avatarUrl();
case RoomIdRole:
return roomId;
}
Q_UNUSED(index)
Q_UNUSED(roleName)
return {};
}
@@ -65,15 +50,6 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const
return m_commonRooms.size();
}
QHash<int, QByteArray> CommonRoomsModel::roleNames() const
{
return {
{RoomIdRole, "roomId"},
{RoomNameRole, "roomName"},
{RoomAvatarRole, "roomAvatar"},
};
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {

View File

@@ -24,9 +24,7 @@ class CommonRoomsModel : public QAbstractListModel
public:
enum Roles {
RoomIdRole = Qt::UserRole,
RoomNameRole,
RoomAvatarRole,
RoomIdRole = Qt::DisplayRole,
};
Q_ENUM(Roles)
@@ -41,8 +39,6 @@ public:
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "models/limitermodel.h"
LimiterModel::LimiterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged);
}
int LimiterModel::maximumCount() const
{
return m_maximumCount;
}
void LimiterModel::setMaximumCount(int maximumCount)
{
if (m_maximumCount != maximumCount) {
m_maximumCount = maximumCount;
Q_EMIT maximumCountChanged();
}
}
int LimiterModel::extraCount() const
{
if (sourceModel()) {
return std::max(sourceModel()->rowCount() - maximumCount(), 0);
}
return 0;
}
bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent)
return source_row < maximumCount();
}
#include "moc_limitermodel.cpp"

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class LimiterModel
*
* @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount.
*
* Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch.
*/
class LimiterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged)
Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged)
public:
explicit LimiterModel(QObject *parent = nullptr);
[[nodiscard]] int maximumCount() const;
void setMaximumCount(int maximumCount);
[[nodiscard]] int extraCount() const;
Q_SIGNALS:
void maximumCountChanged();
void extraCountChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
int m_maximumCount = 0;
};

View File

@@ -287,7 +287,6 @@ Name[ia]=Comparti
Name[it]=Condivisione
Name[ka]=გაზიარება
Name[ko]=공유
Name[lt]=Bendrinti
Name[lv]=Kopīgot
Name[nl]=Gedeelde
Name[nn]=Del
@@ -323,7 +322,6 @@ Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[ko]=콘텐츠 공유 결과
Comment[lt]=Turinio dalies bendrinimo rezultatas
Comment[lv]=Satura kopīgošanas rezultāts
Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald

View File

@@ -211,6 +211,10 @@
<label>Enable threads</label>
<default>false</default>
</entry>
<entry name="SecretBackup" type="bool">
<label>Enable secret backup</label>
<default>false</default>
</entry>
<entry name="Phone3PId" type="bool">
<label>Enable add phone numbers as 3PIDs</label>
<default>false</default>

View File

@@ -11,7 +11,6 @@
#include <KNotification>
#include <KNotificationPermission>
#include <KNotificationReplyAction>
#include <KirigamiAddons/Components/NameUtils>
#include <QPainter>
#include <Quotient/accountregistry.h>
@@ -145,9 +144,16 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
}
QImage avatar_image;
if (!sender.avatarUrl().isEmpty()) {
avatar_image = room->member(sender.id()).avatar(128, 128, {});
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
room->member(sender.id()),
sender.displayName(),
body,
avatar_image,
notification["event"_L1].toObject()["event_id"_L1].toString(),
true,
pair.first);
@@ -189,8 +195,9 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const RoomMember &member,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp)
@@ -209,17 +216,17 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
}
});
notification->setTitle(room->displayName());
QString entry;
if (room->isDirectChat()) {
if (sender == room->displayName()) {
notification->setTitle(sender);
entry = text.toHtmlEscaped();
} else {
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
notification->setTitle(room->displayName());
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
}
notification->setText(entry);
notification->setPixmap(createNotificationImage(member, room));
notification->setPixmap(createNotificationImage(icon, room));
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
@@ -246,9 +253,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setReplyAction(std::move(replyAction));
}
if (Controller::instance().accounts()->rowCount() > 1) {
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->sendEvent();
}
@@ -287,10 +292,18 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
}
const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image;
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else {
qWarning() << "using this room's avatar";
avatar_image = room->avatar(128);
}
KNotification *notification = new KNotification(u"invite"_s);
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
notification->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(sender, room));
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) {
@@ -334,9 +347,7 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
m_invitations.remove(room->id());
});
if (Controller::instance().accounts()->rowCount() > 1) {
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->sendEvent();
}
@@ -396,125 +407,41 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
}
QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room)
{
QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {});
bool senderIconIsPlaceholder = false;
if (senderIcon.isNull()) {
senderIcon = createPlaceholderImage(member.displayName());
senderIconIsPlaceholder = true;
}
QImage icon;
if (room->isDirectChat()) {
icon = senderIcon;
} else {
QImage roomIcon = room->avatar(avatarDimension, avatarDimension);
bool roomIconIsPlaceholder = false;
if (roomIcon.isNull()) {
roomIcon = createPlaceholderImage(room->displayName());
roomIconIsPlaceholder = true;
}
icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder);
}
return QPixmap::fromImage(icon);
}
QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon,
const bool senderIconIsPlaceholder,
const QImage &roomIcon,
const bool roomIconIsPlaceholder)
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
{
// Handle avatars that are lopsided in one dimension
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
const int biggestDimension = std::max(icon.width(), icon.height());
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
roundedImage.fill(Qt::transparent);
QPainter painter(&roundedImage);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setPen(Qt::NoPen);
if (senderIconIsPlaceholder) {
painter.drawImage(imageRect, senderIcon);
} else {
// Fill background for transparent non-placeholder avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
// Fill background for transparent avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
}
const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0};
if (roomIconIsPlaceholder) {
// Ditto for room icons, but we also want to "carve out" the transparent area for readability
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.setBrush(Qt::transparent);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawImage(lowerQuarter, roomIcon);
} else {
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
return roundedImage;
}
QImage NotificationsManager::createPlaceholderImage(const QString &name)
{
const QColor color = NameUtils().colorsFromString(name);
QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
// Draw background
QColor backgroundColor = color;
backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons.
painter.setBrush(backgroundColor);
painter.setPen(Qt::transparent);
painter.drawRoundedRect(image.rect(), image.width(), image.height());
constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions.
// Draw border
painter.setBrush(Qt::transparent);
painter.setPen(QPen(color, borderWidth));
painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)),
image.width(),
image.height());
const QString initials = NameUtils().initialsFromString(name);
QTextOption option;
option.setAlignment(Qt::AlignCenter);
// Calculation similar to the one found in Kirigami Add-ons.
constexpr int largeSpacing = 8; // Same as what's defined in kirigami.
constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2));
QFont font;
font.setPixelSize((avatarDimension - padding) / 2);
painter.setBrush(color);
painter.setPen(color);
painter.setFont(font);
painter.drawText(image.rect(), initials, option);
return image;
return QPixmap::fromImage(roundedImage);
}
#include "moc_notificationsmanager.cpp"

View File

@@ -13,11 +13,6 @@
#include <Quotient/csapi/notifications.h>
#include <Quotient/jobs/basejob.h>
namespace Quotient
{
class RoomMember;
}
class NeoChatConnection;
class KNotification;
class NeoChatRoom;
@@ -72,24 +67,15 @@ private:
QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection);
/**
* @return A combined image of the sender and room's avatar.
*/
static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room);
/**
* @return The sender and room icon combined together into one image. Used internally by createNotificationImage.
*/
static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder);
/**
* @return A placeholder avatar image, similar to the one found in Kirigami Add-ons.
*/
static QImage createPlaceholderImage(const QString &name);
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
void
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
void postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp);
void doPostInviteNotification(QPointer<NeoChatRoom> room);
@@ -98,8 +84,6 @@ private:
bool permissionAsked = false;
static constexpr int avatarDimension = 128;
private Q_SLOTS:
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
};

View File

@@ -5,7 +5,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtMultimedia
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -19,10 +18,6 @@ KirigamiComponents.ConvergentContextMenu {
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window
data: MediaDevices {
id: devices
}
Kirigami.Action {
text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
@@ -38,14 +33,12 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Scan a QR Code")
icon.name: "document-scan-symbolic"
visible: devices.videoInputs.length > 0
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection
}, {
title: i18nc("@title", "Scan a QR Code")
})
}) as Kirigami.Dialog).open();
}
Kirigami.Action {
@@ -62,6 +55,14 @@ KirigamiComponents.ConvergentContextMenu {
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Devices")
icon.name: "computer-symbolic"
onTriggered: {
NeoChatSettingsView.open('devices');
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Developer Tools")
icon.name: "tools"
@@ -75,6 +76,15 @@ KirigamiComponents.ConvergentContextMenu {
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock"
visible: NeoChatConfig.secretBackup
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup")
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low"
@@ -94,25 +104,10 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu Open support dialog", "Support")
icon.name: "help-contents-symbolic"
onTriggered: {
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection,
}).open();
}
}
Kirigami.Action {
separator: true
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
text: i18nc("@action:inmenu", "Logout…")
icon.name: "im-kick-user"
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection
}) as Kirigami.Dialog).open();
}) as Kirigami.Dialog).open()
}
}

View File

@@ -45,12 +45,14 @@ Labs.MenuBar {
}
Labs.MenuItem {
icon.name: "compass-symbolic"
text: i18nc("@action:inmenu Explore public rooms and spaces", "Explore")
text: i18nc("@action:inmenu", "Explore Rooms")
enabled: root.connection
onTriggered: {
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {});
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});

View File

@@ -27,17 +27,16 @@ ColumnLayout {
Layout.fillHeight: true
}
KirigamiComponents.AvatarButton {
KirigamiComponents.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
name: root.invitingMember.displayName
source: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl
color: root.invitingMember.color
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
}
Loader {
@@ -55,12 +54,6 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
}
Kirigami.Heading {
text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
@@ -68,7 +61,12 @@ ColumnLayout {
visible: root.currentRoom && root.currentRoom.canonicalAlias
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor
horizontalAlignment: Text.AlignHCenter
}
Kirigami.Heading {
text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
}
}
}
@@ -139,24 +137,8 @@ ColumnLayout {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
id: viewProfileDelegate
icon.name: "user-properties-symbolic"
text: i18nc("@action:button View this user's profile", "View %1's Profile", root.invitingMember.displayName)
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
}
FormCard.FormDelegateSeparator {
above: viewProfileDelegate
below: ignoreUserDelegate
}
FormCard.FormButtonDelegate {
id: ignoreUserDelegate
icon.name: "list-remove-symbolic"
text: i18nc("@action:button Ignore the user", "Ignore %1 and Reject Invite", root.invitingMember.displayName)
text: i18nc("@action:button Block the user", "Block %1", root.invitingMember.displayName)
onClicked: {
root.currentRoom.forget()

View File

@@ -23,63 +23,72 @@ Kirigami.Page {
name: "cancelled"
when: root.session.state === KeyVerificationSession.CANCELED
PropertyChanges {
stateLoader.sourceComponent: verificationCanceled
target: stateLoader
sourceComponent: verificationCanceled
}
},
State {
name: "waitingForVerification"
when: root.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
PropertyChanges {
stateLoader.sourceComponent: emojiSas
target: stateLoader
sourceComponent: emojiSas
}
},
State {
name: "waitingForReady"
when: root.session.state === KeyVerificationSession.WAITINGFORREADY
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
},
State {
name: "incoming"
when: root.session.state === KeyVerificationSession.INCOMING
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
},
State {
name: "waitingForKey"
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
},
State {
name: "waitingForAccept"
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
},
State {
name: "waitingForMac"
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
},
State {
name: "ready"
when: root.session.state === KeyVerificationSession.READY
PropertyChanges {
stateLoader.sourceComponent: chooseVerificationComponent
target: stateLoader
sourceComponent: chooseVerificationComponent
}
},
State {
name: "done"
when: root.session.state === KeyVerificationSession.DONE
PropertyChanges {
stateLoader.sourceComponent: message
target: stateLoader
sourceComponent: message
}
}
]

View File

@@ -23,45 +23,13 @@ Components.AlbumMaximizeComponent {
*/
required property NeoChatRoom currentRoom
readonly property string currentEventId: {
const idx = (content as ListView).currentIndex;
readonly property string currentEventId: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.EventIdRole)
if (idx === -1) {
return ""
}
readonly property var currentAuthor: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.AuthorRole)
return model.data(model.index(idx, 0), TimelineMessageModel.EventIdRole)
}
readonly property var currentTime: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.TimeRole)
readonly property var currentAuthor: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
return model.data(model.index(idx, 0), TimelineMessageModel.AuthorRole)
}
readonly property var currentTime: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
model.data(model.index(idx, 0), TimelineMessageModel.TimeRole)
}
readonly property var currentProgressInfo: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
model.data(model.index(idx, 0), TimelineMessageModel.ProgressInfoRole)
}
readonly property var currentProgressInfo: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
actions: [
ShareAction {
@@ -154,10 +122,7 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus()
onItemRightClicked: {
const event = root.currentRoom.findEvent(root.currentEventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
}
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;

View File

@@ -30,13 +30,15 @@ Kirigami.SearchDialog {
emptyText: i18nc("Placeholder message", "No room found")
Kirigami.Action {
id: exploreRoomAction
text: i18nc("@action:button Explore public rooms and spaces", "Explore")
text: i18nc("@action:button", "Explore rooms")
icon.name: "compass"
onTriggered: {
root.close()
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {});
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});

View File

@@ -6,7 +6,6 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
@@ -14,8 +13,6 @@ Kirigami.Page {
required property string placeholder
required property string actionText
required property string icon
required property bool reporting
required property NeoChatConnection connection
signal accepted(reason: string)
@@ -24,15 +21,6 @@ Kirigami.Page {
topPadding: 0
bottomPadding: 0
header: Kirigami.InlineMessage {
showCloseButton: false
visible: root.reporting
type: Kirigami.MessageType.Information
position: Kirigami.InlineMessage.Position.Header
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
}
QQC2.TextArea {
id: reason
placeholderText: root.placeholder
@@ -43,7 +31,7 @@ Kirigami.Page {
Keys.onReturnPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
root.accepted(reason.text);
root.Kirigami.PageStack.closeDialog();
root.closeDialog();
}
}
@@ -64,14 +52,14 @@ Kirigami.Page {
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.accepted(reason.text);
root.Kirigami.PageStack.closeDialog();
root.closeDialog();
}
}
QQC2.Button {
icon.name: "dialog-cancel-symbolic"
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: root.Kirigami.PageStack.closeDialog()
onClicked: root.closeDialog()
}
}
}

View File

@@ -75,12 +75,6 @@ Kirigami.Page {
focus: true
padding: 0
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
actions: [
Kirigami.Action {
id: jitsiMeetingAction
@@ -119,7 +113,7 @@ Kirigami.Page {
}
},
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow)?.wideMode
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
icon.name: "view-right-new"
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
}
@@ -233,8 +227,6 @@ Kirigami.Page {
// Used to keep track of messages so we can hide the right one at the right time
property string messageId
Layout.fillWidth: true
showCloseButton: true
visible: false
position: Kirigami.InlineMessage.Position.Header
@@ -261,6 +253,7 @@ Kirigami.Page {
id: timelineView
messageFilterModel: root.messageFilterModel
compactLayout: NeoChatConfig.compactLayout
fileDropEnabled: !Controller.isFlatpak
markReadCondition: NeoChatConfig.markReadCondition
}
}
@@ -295,7 +288,7 @@ Kirigami.Page {
footer: Loader {
id: chatBarLoader
height: active ? (item as ChatBar)?.implicitHeight : 0
height: active ? (item as ChatBar).implicitHeight : 0
active: timelineViewLoader.active && !root.currentRoom.readOnly
sourceComponent: ChatBar {
id: chatBar
@@ -355,17 +348,14 @@ Kirigami.Page {
});
}
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(parent, {
room: room,
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
messageComponentType: messageComponentType,
selectedText,
hoveredLink,
}) as DelegateContextMenu).popup();
}

View File

@@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.Dialog {
id: root
property var model
standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
maximumHeight: Kirigami.Units.gridUnit * 24
title: i18nc("@title:menu Seen by/read marker dialog", "Seen By")
contentItem: ColumnLayout {
spacing: 0
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
id: listView
model: root.model
spacing: Kirigami.Units.smallSpacing
onCountChanged: {
if (listView.count === 0) {
root.close();
}
}
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string displayName
required property url avatarUrl
required property color memberColor
required property string userId
implicitHeight: Kirigami.Units.gridUnit * 2
text: displayName
highlighted: false
onClicked: {
root.close();
RoomManager.resolveResource(userDelegate.userId);
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatarUrl
name: userDelegate.displayName
color: userDelegate.memberColor
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.displayName
textFormat: Text.PlainText
elide: Text.ElideRight
clip: true // Intentional to limit insane Unicode in display names
Layout.fillWidth: true
}
}
}
}
}
}
}

View File

@@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
Kirigami.Dialog {
id: root
required property NeoChatConnection connection
readonly property SupportController supportController: SupportController {
connection: root.connection
}
readonly property bool hasSupportResources: supportController.supportPage.length > 0 && supportController.contacts.length > 0
title: i18nc("@title Support information", "Support")
width: Math.min(Kirigami.Units.gridUnit * 30, QQC2.ApplicationWindow.window.width)
ColumnLayout {
spacing: 0
FormCard.FormTextDelegate {
id: explanationTextDelegate
text: root.hasSupportResources ?
i18nc("@info:label %1 is the domain of the server", "Official support resources provided by %1:", root.connection.domain)
: i18nc("@info:label %1 is the domain of the server", "%1 has no support resources.", root.connection.domain)
}
FormCard.FormDelegateSeparator {
above: explanationTextDelegate
below: openSupportPageDelegate
visible: openSupportPageDelegate.visible
}
FormCard.FormLinkDelegate {
id: openSupportPageDelegate
icon.name: "help-contents-symbolic"
text: i18nc("@action:button Open support webpage", "Open Support")
url: root.supportController.supportPage
visible: root.supportController.supportPage.length > 0
}
FormCard.FormDelegateSeparator {
above: openSupportPageDelegate
visible: root.supportController.contacts.length > 0
}
Repeater {
model: root.supportController.contacts
delegate: FormCard.AbstractFormDelegate {
id: contactDelegate
required property string role
required property string matrixId
required property string emailAddress
background: null
Layout.fillWidth: true
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "user"
}
QQC2.Label {
text: {
// Translate known keys
if (contactDelegate.role === "m.role.admin") {
return i18nc("@info:label Adminstrator contact", "Admin")
} else if (contactDelegate.role === "m.role.security") {
return i18nc("@info:label Security contact", "Security")
}
return contactDelegate.role;
}
elide: Text.ElideRight
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: contactDelegate.matrixId.length > 0
icon.name: "document-send-symbolic"
onClicked: {
root.close();
root.connection.requestDirectChat(contactDelegate.matrixId);
}
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is a Matrix ID", "Contact via Matrix (%1)", contactDelegate.matrixId)
}
QQC2.ToolButton {
visible: contactDelegate.emailAddress.length > 0
icon.name: "mail-sent-symbolic"
onClicked: Qt.openUrlExternally("mailto:%1".arg(contactDelegate.emailAddress))
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is an e-mail address", "Contact via e-mail (%1)", contactDelegate.emailAddress)
}
}
}
}
}
}

View File

@@ -11,9 +11,7 @@ import org.kde.neochat
FormCard.FormCardPage {
id: root
property bool processing: false
title: i18nc("@title:window", "Manage Secret Backup")
title: i18nc("@title:window", "Load your encrypted messages")
topPadding: Kirigami.Units.gridUnit
leftPadding: 0
@@ -27,42 +25,75 @@ FormCard.FormCardPage {
position: Kirigami.InlineMessage.Position.Header
}
Connections {
target: Controller.activeConnection
function onKeyBackupError(): void {
securityKeyField.clear()
root.processing = false
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
banner.visible = true
}
property SSSSHandler ssssHandler: SSSSHandler {
id: ssssHandler
function onKeyBackupUnlocked(): void {
root.processing = false
property bool processing: false
connection: Controller.activeConnection
onKeyBackupUnlocked: {
ssssHandler.processing = false
banner.text = i18nc("@info:status", "Encryption keys restored.")
banner.type = Kirigami.MessageType.Positive
banner.visible = true
}
onError: error => {
if (error !== SSSSHandler.WrongKeyError) {
banner.text = error
banner.visible = true
return;
}
passwordField.clear()
ssssHandler.processing = false
banner.text = i18nc("@info:status", "The security phrase was not correct.")
banner.visible = true
}
}
FormCard.FormHeader {
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
title: i18nc("@title", "Unlock using Passphrase")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
description: i18nc("@info", "If you have a backup passphrase for this account, enter it below.")
}
FormCard.FormTextFieldDelegate {
id: passwordField
label: i18nc("@label:textbox", "Backup Password:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
id: unlockButton
text: i18nc("@action:button", "Unlock")
icon.name: "unlock"
enabled: passwordField.text.length > 0 && !ssssHandler.processing
onClicked: {
ssssHandler.processing = true
banner.visible = false
ssssHandler.unlockSSSSWithPassphrase(passwordField.text)
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Unlock using Security Key")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have a security key for this account, enter it below or upload it as a file.")
}
FormCard.FormTextFieldDelegate {
id: securityKeyField
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
label: i18nc("@label:textbox", "Security Key:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
id: uploadSecurityKeyButton
text: i18nc("@action:button", "Upload from File")
icon.name: "cloud-upload"
enabled: !root.processing
enabled: !ssssHandler.processing
onClicked: {
root.processing = true
ssssHandler.processing = true
openFileDialog.open()
}
}
@@ -70,10 +101,10 @@ FormCard.FormCardPage {
id: unlockSecurityKeyButton
text: i18nc("@action:button", "Unlock")
icon.name: "unlock"
enabled: securityKeyField.text.length > 0 && !root.processing
enabled: securityKeyField.text.length > 0 && !ssssHandler.processing
onClicked: {
root.processing = true
Controller.activeConnection.unlockSSSS(securityKeyField.text)
ssssHandler.processing = true
ssssHandler.unlockSSSSFromSecurityKey(securityKeyField.text)
}
}
}
@@ -89,10 +120,10 @@ FormCard.FormCardPage {
id: unlockCrossSigningButton
icon.name: "emblem-shared-symbolic"
text: i18nc("@action:button", "Request from other Devices")
enabled: !root.processing
enabled: !ssssHandler.processing
onClicked: {
root.processing = true
Controller.activeConnection.unlockSSSS("")
ssssHandler.processing = true
ssssHandler.unlockSSSSFromCrossSigning()
}
}
}

View File

@@ -20,47 +20,29 @@ Kirigami.Dialog {
// Make sure that code is prepared to deal with this property being null
property NeoChatRoom room
property var user
// Used for the "View Main Profile" feature so you can toggle back to the room profile.
property NeoChatRoom oldRoom
property NeoChatConnection connection
property CommonRoomsModel model: CommonRoomsModel {
connection: root.connection
userId: root.user.id
}
property LimiterModel limiterModel: LimiterModel {
maximumCount: 5
sourceModel: root.model
}
readonly property bool isSelf: root.user.id === root.connection.localUserId
readonly property bool hasMutualRooms: root.model.count > 0
readonly property bool isRoomProfile: root.room
readonly property string shareUrl: "https://matrix.to/#/" + root.user.id
leftPadding: Kirigami.Units.largeSpacing * 2
rightPadding: Kirigami.Units.largeSpacing * 2
topPadding: Kirigami.Units.largeSpacing * 2
bottomPadding: Kirigami.Units.largeSpacing * 2
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title:menu Account details dialog", "Account Details")
header: null
contentItem: ColumnLayout {
spacing: 0
RowLayout {
id: detailRow
spacing: Kirigami.Units.largeSpacing
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
id: avatar
@@ -68,14 +50,7 @@ Kirigami.Dialog {
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
source: {
if (root.room) {
return root.room.member(root.user.id).avatarUrl;
} else if(root.user.avatarUrl.toString() !== '') {
return root.connection.makeMediaUrl(root.user.avatarUrl);
}
return "";
}
source: root.room ? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
}
@@ -100,363 +75,223 @@ Kirigami.Dialog {
id: idLabel
textFormat: TextEdit.PlainText
text: idLabelTextMetrics.elidedText
color: Kirigami.Theme.disabledTextColor
TextMetrics {
id: idLabelTextMetrics
text: root.user.id
elide: Qt.ElideRight
elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
}
}
Kirigami.ActionToolBar {
QQC2.Label {
property CommonRoomsModel model: CommonRoomsModel {
connection: root.connection
userId: root.user.id
}
text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count)
color: Kirigami.Theme.disabledTextColor
visible: model.count > 0
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
}
}
QQC2.AbstractButton {
id: qrButton
Layout.minimumHeight: avatar.height * 0.75
Layout.maximumHeight: avatar.height * 1.5
Layout.maximumWidth: avatar.height * 1.5
Kirigami.Heading {
text: i18nc("@title Moderation actions for this user", "Moderation")
level: 2
visible: root.isRoomProfile && moderationToolbar.actions.filter(function (it) { return it.visible; }).length > 0
Layout.topMargin: Kirigami.Units.largeSpacing
}
Kirigami.ActionToolBar {
id: moderationToolbar
flat: false
visible: root.isRoomProfile
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
text: i18nc("@action:button Kick the user from the room", "Kick…")
icon.name: "im-kick-user"
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
text: i18nc("@action:button Ban this user from the room", "Ban…")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
}
return false;
}
text: i18nc("@action:button Unban the user from this room", "Unban")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
root.room.unban(root.user.id);
root.close();
}
},
Kirigami.Action {
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
}
contentItem: Barcode {
id: barcode
barcodeType: Barcode.QRCode
content: "https://matrix.to/#/" + root.user.id
}
]
}
Kirigami.Heading {
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
level: 2
visible: root.isRoomProfile
Layout.topMargin: Kirigami.Units.largeSpacing
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: root.isRoomProfile
Layout.topMargin: Kirigami.Units.smallSpacing
QQC2.Label {
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
}
QQC2.Button {
visible: {
if (root.room) {
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
}
return false;
}
text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level")
icon.name: "document-edit-symbolic"
display: QQC2.AbstractButton.IconOnly
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: {
(powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
}) as PowerLevelDialog).open();
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: barcode.content,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
}
Component {
id: powerLevelDialog
PowerLevelDialog {
id: powerLevelDialog
}
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: barcode.content
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
Kirigami.Heading {
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
level: 4
visible: !root.isSelf && root.hasMutualRooms
Kirigami.Chip {
visible: root.room
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
closable: false
checkable: false
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: !root.isSelf && root.hasMutualRooms
Layout.topMargin: Kirigami.Units.smallSpacing
Repeater {
model: root.limiterModel
delegate: KirigamiComponents.AvatarButton {
required property string roomName
required property string roomAvatar
required property string roomId
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
name: roomName
source: roomAvatar
onClicked: {
root.close();
RoomManager.resolveResource(roomId);
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.Label {
text: i18ncp("@info:label And '%1' more rooms you have in common with this user, but are not shown", "and 1 more…", "and %1 more…", root.limiterModel.extraCount)
visible: root.limiterModel.extraCount > 0
color: Kirigami.Theme.disabledTextColor
}
}
Kirigami.Heading {
text: i18nc("@title Private note for this user", "Private Note")
level: 4
Layout.topMargin: Kirigami.Units.largeSpacing
}
QQC2.TextArea {
id: noteText
text: root.connection.noteForUser(root.user.id)
textFormat: TextEdit.PlainText
wrapMode: TextEdit.Wrap
placeholderText: i18nc("@info:placeholder", "Only visible to you")
onTextEdited: editTimer.restart()
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
}
// Prevent unnecessary edits by waiting 1 second
Timer {
id: editTimer
interval: 1000
onTriggered: root.connection.setNoteForUser(root.user.id, noteText.text)
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId && !!root.user
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
icon.name: "im-invisible-user"
onClicked: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Kick this user")
icon.name: "im-kick-user"
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("invite") && !root.room.containsUser(root.user.id)
enabled: root.room && !root.room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Invite this user")
icon.name: "list-add-user"
onClicked: {
root.room.inviteToRoom(root.user.id);
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Ban this user")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
root.room.unban(root.user.id);
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.room.canSendState("m.room.power_levels")
text: i18nc("@action:button", "Set user power level")
icon.name: "visibility"
onClicked: {
(powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
}) as PowerLevelDialog).open();
root.close();
}
Component {
id: powerLevelDialog
PowerLevelDialog {
id: powerLevelDialog
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
text: i18nc("@action:button", "Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
icon.name: "document-send"
onClicked: {
root.connection.requestDirectChat(root.user.id);
root.close();
}
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button %1 is the name of the user.", "Search room for %1's messages", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName))
icon.name: "search-symbolic"
onClicked: {
((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
}
FormCard.FormButtonDelegate {
text: i18n("Copy link")
icon.name: "username-copy"
onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id)
}
}
}

View File

@@ -29,28 +29,6 @@
#include <KIO/OpenUrlJob>
#endif
/**
* @brief Stops RoomManager from updating the last room and space config.
*/
class LastRoomBlocker
{
public:
explicit LastRoomBlocker(RoomManager *manager)
: m_manager(manager)
{
Q_ASSERT(manager);
m_manager->m_dontUpdateLastRoom = true;
}
~LastRoomBlocker()
{
m_manager->m_dontUpdateLastRoom = false;
}
private:
RoomManager *m_manager;
};
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openStateConfig())
@@ -304,22 +282,26 @@ void RoomManager::viewEventSource(const QString &eventId)
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
{
if (!event) {
qWarning() << "Tried to open event menu with empty event";
if (eventId.isEmpty()) {
qWarning() << "Tried to open event menu with empty event id";
return;
}
Q_EMIT showDelegateMenu(parent,
room,
event->id(),
room->qmlSafeMember(event->senderId()),
MessageComponentType::typeForEvent(*event),
EventHandler::plainBody(room, event),
EventHandler::richBody(room, event),
EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
room->fileTransferInfo(event->id()),
const auto it = room->findInTimeline(eventId);
if (it == room->historyEdge()) {
// This is probably a pending event
return;
}
const auto &event = **it;
Q_EMIT showDelegateMenu(eventId,
room->qmlSafeMember(event.senderId()),
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
room->fileTransferInfo(eventId),
selectedText,
hoveredLink);
}
@@ -342,6 +324,17 @@ void RoomManager::loadInitialRoom()
resolveResource(m_arg);
}
if (m_isMobile) {
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
// We can't have empty keys in KConfig, so we stored it as "Home"
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, false);
// We don't want to open a room on startup on mobile
return;
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
@@ -354,14 +347,16 @@ void RoomManager::loadInitialRoom()
void RoomManager::openRoomForActiveConnection()
{
Q_ASSERT(m_connection);
if (!m_connection) {
setCurrentRoom({});
setCurrentSpace({}, false);
return;
}
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
// We don't want to open a room on startup on mobile
setCurrentSpace(lastSpace, !m_isMobile);
setCurrentSpace(lastSpace, true);
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
@@ -518,7 +513,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
Q_EMIT connectionChanged();
}
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
{
m_currentSpaceId = spaceId;
@@ -538,65 +533,25 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
}
// If we requested to change to the last opened room, do so:
if (goToLastUsedRoom) {
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
LastRoomBlocker blocker(this);
if (!setRoom) {
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home":
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
// We intentionally don't want to open the last room on mobile
if (m_isMobile) {
return;
}
// If no last room was opened, go to the space home:
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
// Fallback to no room opened:
setCurrentRoom({});
// We can't have empty keys in KConfig, so it's stored as "Home"
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
}
QString RoomManager::findSpaceIdForCurrentRoom() const
{
if (!m_currentRoom) {
return m_currentSpaceId;
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
if (m_currentRoom->isDirectChat()) {
const auto roomsInSpace = SpaceHierarchyCache::instance().getRoomListForSpace(m_currentSpaceId, false);
if (roomsInSpace.contains(m_currentRoom->id())) {
return m_currentSpaceId;
}
return "DM"_L1;
}
const auto &parentSpaces = SpaceHierarchyCache::instance().parentSpaces(m_currentRoom->id());
if (parentSpaces.contains(m_currentSpaceId)) {
return m_currentSpaceId;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome()) {
return {};
}
if (const auto &parent = m_connection->room(m_currentRoom->canonicalParent())) {
for (const auto &parentParent : SpaceHierarchyCache::instance().parentSpaces(parent->id())) {
if (SpaceHierarchyCache::instance().parentSpaces(parentParent).isEmpty()) {
return parentParent;
}
}
return parent->id();
}
for (const auto &space : parentSpaces) {
if (SpaceHierarchyCache::instance().parentSpaces(space).isEmpty()) {
return space;
}
}
if (m_currentRoom->isSpace()) {
return m_currentSpaceId;
}
return {};
setCurrentRoom({});
}
void RoomManager::setCurrentRoom(const QString &roomId)
@@ -616,25 +571,57 @@ void RoomManager::setCurrentRoom(const QString &roomId)
}
Q_EMIT currentRoomChanged();
if (!m_dontUpdateLastRoom) {
if (m_connection) {
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
return;
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
// We can't have empty keys in KConfig, so name it "Home"
if (m_currentSpaceId.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(m_currentSpaceId, roomId);
}
}
}
if (roomId.isEmpty()) {
return;
}
if (m_currentRoom->isSpace()) {
return;
}
if (m_currentRoom->isDirectChat()) {
const auto roomsInSpace = SpaceHierarchyCache::instance().getRoomListForSpace(m_currentSpaceId, false);
if (!roomsInSpace.contains(m_currentRoom->id()) && m_currentSpaceId != "DM"_L1) {
setCurrentSpace("DM"_L1, false);
}
return;
}
const auto &parentSpaces = SpaceHierarchyCache::instance().parentSpaces(roomId);
if (parentSpaces.contains(m_currentSpaceId)) {
return;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome()) {
setCurrentSpace({}, false);
return;
}
if (const auto &parent = m_connection->room(m_currentRoom->canonicalParent())) {
for (const auto &parentParent : SpaceHierarchyCache::instance().parentSpaces(parent->id())) {
if (SpaceHierarchyCache::instance().parentSpaces(parentParent).isEmpty()) {
setCurrentSpace(parentParent, false);
return;
}
}
setCurrentSpace(parent->id(), false);
return;
}
for (const auto &space : parentSpaces) {
if (SpaceHierarchyCache::instance().parentSpaces(space).isEmpty()) {
setCurrentSpace(space, false);
return;
}
}
setCurrentSpace({}, false);
}
void RoomManager::clearCurrentRoom()

View File

@@ -233,8 +233,7 @@ public:
/**
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
@@ -307,9 +306,7 @@ Q_SIGNALS:
/**
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(QObject *parent,
NeoChatRoom *room,
const QString &eventId,
void showDelegateMenu(const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
@@ -340,11 +337,6 @@ Q_SIGNALS:
void currentSpaceChanged();
protected:
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
friend class LastRoomBlocker;
private:
bool m_isMobile = false;
@@ -381,22 +373,8 @@ private:
void setCurrentRoom(const QString &roomId);
/**
* @brief Find the most appropriate space for the currently selected room
*
* Should be used to figure out what space to switch to after a room change.
*
* @return The Space ID that the currently set room should be displayed as part of. (or "DM" for DM and "" for Home)
*/
QString findSpaceIdForCurrentRoom() const;
/**
* @brief Sets the current space.
*
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
*/
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
// Space ID, "DM", or empty string
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
/**
* @brief Resolve a user URI.

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#include "supportcontroller.h"
#include <Quotient/csapi/support.h>
#include <QDebug>
using namespace Quotient;
void SupportController::setConnection(NeoChatConnection *connection)
{
if (m_connection != connection) {
m_connection = connection;
Q_EMIT connectionChanged();
load();
}
}
NeoChatConnection *SupportController::connection() const
{
return m_connection;
}
QString SupportController::supportPage() const
{
return m_supportPage;
}
QList<SupportContact> SupportController::contacts() const
{
return m_contacts;
}
void SupportController::load()
{
if (!m_connection) {
qWarning() << "Tried to load support information without a valid connection?";
return;
}
m_connection->callApi<GetWellknownSupportJob>()
.onResult([this](const auto &job) {
m_supportPage = job->supportPage();
m_contacts.reserve(job->contacts().size());
for (const auto &contact : job->contacts()) {
m_contacts.push_back(SupportContact{
.role = contact.role,
.matrixId = contact.matrixId,
.emailAddress = contact.emailAddress,
});
}
Q_EMIT loaded();
})
.onFailure([this](const auto &job) {
Q_UNUSED(job)
// Just do nothing, our properties will be empty.
Q_EMIT loaded();
});
}
#include "moc_supportcontroller.cpp"

View File

@@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "neochatconnection.h"
class SupportContact
{
Q_GADGET
Q_PROPERTY(QString role MEMBER role)
Q_PROPERTY(QString matrixId MEMBER matrixId)
Q_PROPERTY(QString emailAddress MEMBER emailAddress)
public:
QString role;
QString matrixId;
QString emailAddress;
};
class SupportController : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString supportPage READ supportPage NOTIFY loaded)
Q_PROPERTY(QList<SupportContact> contacts READ contacts NOTIFY loaded)
public:
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QString supportPage() const;
QList<SupportContact> contacts() const;
Q_SIGNALS:
void connectionChanged();
void loaded();
private:
void load();
QPointer<NeoChatConnection> m_connection = nullptr;
QList<SupportContact> m_contacts;
QString m_supportPage;
};

View File

@@ -286,8 +286,6 @@ QQC2.Control {
quickFormatBar.selectionStart = selectionStart;
quickFormatBar.selectionEnd = selectionEnd;
quickFormatBar.open();
} else if (quickFormatBar.visible) {
quickFormatBar.close();
}
}

View File

@@ -65,7 +65,7 @@ QQC2.Popup {
padding: 2
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window?.width)
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window.width)
contentItem: EmojiPicker {
id: emojiPicker
height: 400

View File

@@ -148,7 +148,7 @@ ColumnLayout {
id: quickReactions
Layout.fillWidth: true
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
delegate: EmojiDelegate {
required property string modelData

View File

@@ -21,6 +21,12 @@ FormCard.FormCard {
onToggled: NeoChatConfig.threads = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
checked: NeoChatConfig.secretBackup
onToggled: NeoChatConfig.secretBackup = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
checked: NeoChatConfig.phone3PId

View File

@@ -33,8 +33,6 @@ target_sources(LibNeoChat PRIVATE
events/imagepackevent.cpp
events/pollevent.cpp
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatreportroomjob.cpp
jobs/neochatreportuserjob.cpp
models/actionsmodel.cpp
models/completionmodel.cpp
models/completionproxymodel.cpp

View File

@@ -3,10 +3,6 @@
#include "chatbarcache.h"
#include <QMimeData>
#include <KUrlMimeData>
#include <Quotient/roommember.h>
#include "eventhandler.h"
@@ -150,12 +146,7 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
if (m_relationId.isEmpty()) {
return room->member(QString());
}
const auto [event, _] = room->getEvent(m_relationId);
if (event != nullptr) {
return room->member(event->senderId());
}
qWarning() << "Failed to find relation" << m_relationId << "in timeline?";
return room->member(QString());
return room->member((*room->findInTimeline(m_relationId))->senderId());
}
bool ChatBarCache::relationAuthorIsPresent() const
@@ -178,8 +169,8 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
return EventHandler::markdownBody(event);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
return EventHandler::markdownBody(&**event);
}
return {};
}
@@ -285,6 +276,11 @@ void ChatBarCache::postMessage()
return;
}
const auto replyIt = room->findInTimeline(replyId());
if (replyIt == room->historyEdge()) {
isReply = false;
}
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
room->post<Quotient::RoomMessageEvent>(text(), *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), std::move(content), relatesTo);
@@ -299,17 +295,4 @@ void ChatBarCache::clearCache()
clearRelations();
}
void ChatBarCache::drop(QList<QUrl> u, const QString &transferPortal)
{
QMimeData mimeData;
mimeData.setUrls(u);
if (!transferPortal.isEmpty()) {
mimeData.setData(u"application/vnd.portal.filetransfer"_s, transferPortal.toLatin1());
}
auto urls = KUrlMimeData::urlsFromMimeData(&mimeData);
if (urls.size() > 0) {
setAttachmentPath(urls[0].toString());
}
}
#include "moc_chatbarcache.cpp"

View File

@@ -198,8 +198,6 @@ public:
*/
Q_INVOKABLE void postMessage();
Q_INVOKABLE void drop(QList<QUrl> urls, const QString &transferPortal);
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);

View File

@@ -12,10 +12,6 @@ QString PowerLevel::nameForLevel(Level level)
return i18n("Moderator");
case PowerLevel::Admin:
return i18n("Admin");
case PowerLevel::Owner:
return i18nc("The person that owns a room", "Owner");
case PowerLevel::Creator:
return i18nc("The person that created a room", "Creator");
case PowerLevel::Mute:
return i18n("Mute");
case PowerLevel::Custom:
@@ -34,8 +30,6 @@ int PowerLevel::valueForLevel(Level level)
return 50;
case PowerLevel::Admin:
return 100;
case PowerLevel::Owner:
return 150;
case PowerLevel::Mute:
return -1;
default:
@@ -52,12 +46,8 @@ PowerLevel::Level PowerLevel::levelForValue(int value)
return PowerLevel::Moderator;
case 100:
return PowerLevel::Admin;
case 150:
return PowerLevel::Owner;
case -1:
return PowerLevel::Mute;
case std::numeric_limits<int>::max():
return PowerLevel::Creator;
default:
return PowerLevel::Custom;
}

View File

@@ -31,12 +31,10 @@ public:
enum Level {
Member, /**< A basic member. */
Moderator, /**< A moderator with enhanced powers. */
Admin, /**< Power level 100. */
Owner, /**< Power level 150. */
Admin, /**< The highest power level in the room. */
Mute, /**< The level to remove posting privileges. */
NUMLevels,
Custom, /**< A non-standard value. Intentionally after NUMLevels so it doesn't appear in the model. */
Creator, /**< The user creating the (co-)creating the room. */
};
Q_ENUM(Level);

View File

@@ -389,9 +389,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
return i18n("left the room");
}
if (const auto &reason = e.contentJson()["reason"_L1].toString().toHtmlEscaped(); !reason.isEmpty()) {
return i18n("has removed %1 from the room: %2", subjectName, reason);
return i18n("has put %1 out of the room: %2", subjectName, reason);
}
return i18n("has removed %1 from the room", subjectName);
return i18n("has put %1 out of the room", subjectName);
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
@@ -697,9 +697,6 @@ QString EventHandler::subtitleText(const NeoChatRoom *room, const Quotient::Room
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
return {};
}
if (room->isDirectChat()) {
return plainBody(room, event, true);
}
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? u" "_s : u": "_s) + plainBody(room, event, true);
}

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatreportroomjob.h"
using namespace Quotient;
NeochatReportRoomJob::NeochatReportRoomJob(const QString &userId, const QString &reason)
: BaseJob(HttpVerb::Post, u"ReportRoomJob"_s, makePath(" /_matrix/client/v3/", userId, "/report"))
{
QJsonObject _dataJson;
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
setRequestData({_dataJson});
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
// TODO: Remove once libQuotient updates to Matrix API v1.14
class NeochatReportRoomJob : public Quotient::BaseJob
{
public:
explicit NeochatReportRoomJob(const QString &roomId, const QString &reason);
};

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatreportuserjob.h"
using namespace Quotient;
NeochatReportUserJob::NeochatReportUserJob(const QString &userId, const QString &reason)
: BaseJob(HttpVerb::Post, u"ReportUserJob"_s, makePath("/_matrix/client/v3/users/", userId, "/report"))
{
QJsonObject _dataJson;
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
setRequestData({_dataJson});
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
// TODO: Remove once libQuotient updates to Matrix API v1.14
class NeochatReportUserJob : public Quotient::BaseJob
{
public:
explicit NeochatReportUserJob(const QString &userId, const QString &reason);
};

View File

@@ -59,7 +59,7 @@ QList<ActionsModel::Action> actions{
Action{
u"shrug"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"¯\\\\\\_(ツ)\\_/¯ %1"_s.arg(message);
return u"¯\\\\_(ツ)_/¯ %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),

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