Compare commits

..

56 Commits

Author SHA1 Message Date
Carl Schwan
bc977c3fc6 Update apstream screenshots 2021-05-31 20:26:07 +02:00
Carl Schwan
c4c283c85a Update versioning to 1.2.0 2021-05-31 17:25:35 +02:00
Carl Schwan
56f49fabf7 Don't mark message as read when the current window is not visible
Fix #378

(cherry picked from commit 87d1fefae2)
2021-05-31 17:22:09 +02:00
Carl Schwan
fe407a3421 Mark all message as read when clicking on down button
Fix #379

(cherry picked from commit 6e5bca4928)
2021-05-31 17:22:03 +02:00
l10n daemon script
bbe539885e GIT_SILENT made messages (after extraction)
(cherry picked from commit 4d236a201b)
2021-05-31 16:58:08 +02:00
Carl Schwan
ff978b9586 Update Appstream 2021-05-31 16:57:50 +02:00
Noah Davis
885b75e35f Move TypingIndicator to the right side
So that it's less likely to cover message text.
2021-05-31 16:54:41 +02:00
Srevin Saju
cb81eaf26f feat: show the username and avatar again on date-change
when the clock hits 00:00 in the user's time zone, but in the
case of a continuous discussion, it is likely that "Today"
date-change marker would obstruct the conversation, and the
username and avatar would be missing.


(cherry picked from commit 3e78bff8a1)
2021-05-29 22:16:42 +00:00
Carl Schwan
22732b801b Don't steal focus in panel search field
(cherry picked from commit 807112fb19)
2021-05-30 00:06:58 +02:00
Carl Schwan
ae3e395b47 Reinitialize completion list after switching room
(cherry picked from commit e15e10d319)
2021-05-29 23:55:58 +02:00
Carl Schwan
96c91e2a35 Make sure we only add non empty name or display name in autocompletion
list

Otherwise this will breaks when replacing names later

(cherry picked from commit c7fd5cc511)
2021-05-29 23:15:26 +02:00
Carl Schwan
e36204bbd8 Open room when pressing Enter or Return
Fix #381

(cherry picked from commit b37152ff89)
2021-05-29 20:16:45 +02:00
Arnav Rawat
1804140ac0 Restore I-Beam cursor on hover
Should not cause issues with themes


(cherry picked from commit df0ad391ba)
2021-05-28 13:35:05 +00:00
Carl Schwan
5a28a93ab6 Fix reverse tabbing not working in autocompletion
Now call autocomplete() also for shift+tab

Fix #377

(cherry picked from commit 3329739d55)
2021-05-28 14:57:35 +02:00
Carl Schwan
76bd529c3c Fix size of replies in mesage delegate 2021-05-28 14:53:43 +02:00
Carl Schwan
293288a0b6 Fix completion when tabing users
This was caused by weird bindings. Now just access the userId value
directly.

Fix #324

(cherry picked from commit d9125148fe)
2021-05-28 14:27:44 +02:00
Carl Schwan
51b6593f96 Better read market handling: Mark room as read when scrolling to the
bottom

Fix #372

(cherry picked from commit db0f421811)
2021-05-28 14:07:19 +02:00
Carl Schwan
51e73568c4 Fix date being show too often
(cherry picked from commit 3d251b9b25)
2021-05-28 14:07:19 +02:00
Carl Schwan
e461e2098b Don't use SystemTray integration on GNOME and ElementaryOS
These platforms don't support it so hiding NeoChat in the tray in these
platforms is not a good idea and other a rather poor user experience.

(cherry picked from commit 13888401fa)
2021-05-28 14:07:19 +02:00
Hannah von Reth
79ceb45fae Fix Windows builds
(cherry picked from commit 92fcff1dce)
2021-05-28 14:07:19 +02:00
Nicolas Fella
0c292b34ff Remove minSdk version from AndroidManifest
(cherry picked from commit 41838d01df)
2021-05-28 14:07:19 +02:00
Nicolas Fella
db6640ba49 Fix Android ifdef
(cherry picked from commit af75cebba1)
2021-05-28 14:07:19 +02:00
Nicolas Fella
3827249f0c remove spurious QFileDialog include
(cherry picked from commit 1cec672c0a)
2021-05-28 14:07:19 +02:00
Nicolas Fella
f40a3daef4 remove spurious QMenu include
(cherry picked from commit bd5f6c9c9e)
2021-05-28 14:07:19 +02:00
Nicolas Fella
8d2608a230 Use QGuiApplication instead of QApplication where appropriate
(cherry picked from commit 6e04d343b7)
2021-05-28 14:07:19 +02:00
Nicolas Fella
3e5628def3 Don't find Widgets on Android
(cherry picked from commit 454e35433b)
2021-05-28 12:09:01 +00:00
Tobias Fella
3b3673fdff Fix multiple headers for the same sections
(cherry picked from commit 4dea02197c)
2021-05-28 12:08:32 +00:00
Adriaan de Groot
d81e4c417d CMake: various tidying-up
(cherry picked from commit 294f0c7e1a)
2021-05-28 12:07:48 +00:00
Carl Schwan
44d3f628d9 Fix username autocompletion 2021-05-28 14:02:03 +02:00
Carl Schwan
0db9c0454f pushReplace more
(cherry picked from commit 7cd9f788dd)
2021-05-28 12:05:59 +00:00
Noah Davis
e5c65a662e Use 3 dot typing indicator, clean up code a bit.
Move TypingIndicator.qml out of ChatBox folder.
It wasn't part of the ChatBox.

fixes #367 by eliding instead of wrapping text


(cherry picked from commit bbcf4239a4)
2021-05-27 14:18:05 +00:00
l10n daemon script
8913aa8a66 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"
2021-05-26 02:28:13 +00:00
l10n daemon script
5db3e14ae6 GIT_SILENT made messages (after extraction) 2021-05-26 01:52:33 +00:00
l10n daemon script
c5a3fc0431 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"
2021-05-25 02:21:15 +00:00
l10n daemon script
fc791d41fa GIT_SILENT made messages (after extraction) 2021-05-25 01:47:45 +00:00
Carl Schwan
127ad19109 Fix minor bugs
(cherry picked from commit d14674c2cd)
2021-05-24 14:55:28 +00:00
Carl Schwan
066ea4f8bd Make sure message are loaded when scrolling to the top
(cherry picked from commit 49c1736f7c)
2021-05-24 14:53:11 +00:00
Carl Schwan
cf60337b27 Make effects more visible
(cherry picked from commit db62f06de4)
2021-05-24 14:52:55 +00:00
l10n daemon script
98672cf870 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"
2021-05-24 02:13:42 +00:00
l10n daemon script
abd03299ec GIT_SILENT made messages (after extraction) 2021-05-24 01:39:11 +00:00
Carl Schwan
41b64f977c Fix creating broken direct chat for user with a direct chat already open
Just enter the existing room instead of trying to create a new one but
broken.

Fix !237


(cherry picked from commit 0dbb56ba1e)
2021-05-23 19:51:30 +00:00
Carl Schwan
ac75dd57c0 Minor optimization
(cherry picked from commit 7bdfdc0eec)
2021-05-23 16:33:13 +00:00
Carl Schwan
1d3d61ed77 Fix loading events when scrolling or opening a room for the first time
Fix #362


(cherry picked from commit bae7813f68)
2021-05-23 16:31:41 +00:00
Carl Schwan
1e047a8ff1 Fix mode without avatar
It seems that this mode didn't get much love when I added the bubbles so
it was quite broken. This patches removes the bubbles and fix the
alignment issues when using this mode.

We probably should rename it to compact mode in a follow up commit (but
not this one so we can backport it to the stable branch).


(cherry picked from commit dded804f00)
2021-05-23 16:30:55 +00:00
l10n daemon script
530b4c24a0 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"
2021-05-23 02:12:00 +00:00
l10n daemon script
ada7bcef65 GIT_SILENT made messages (after extraction) 2021-05-23 01:38:51 +00:00
Carl Schwan
ad4ca3ad9e Fix i18n
(cherry picked from commit dbb43addc8)
2021-05-22 16:24:24 +00:00
Tobias Fella
4103c44eb5 Don't hide redacted events
(cherry picked from commit 135b2e49fa)
2021-05-22 12:18:07 +00:00
Tobias Fella
dd4ed7539e Revert "Fix showing multiple deleted messages from the same author"
This reverts commit b48c9bdadc.


(cherry picked from commit 011f649cbf)
2021-05-22 12:17:45 +00:00
Tobias Fella
52ad911b2d Revert "Show deleted messages"
This reverts commit 116f883699.


(cherry picked from commit 36a2f5719f)
2021-05-22 12:17:20 +00:00
Tobias Fella
f09dff979e Fix banning users
(cherry picked from commit af6880b2ca)
2021-05-22 12:09:43 +00:00
Tobias Fella
0476398f91 Don't offer banning users that are already banned
(cherry picked from commit 48d1fa27cf)
2021-05-22 12:09:20 +00:00
Tobias Fella
41993bfe24 Don't offer to kick users that already left
(cherry picked from commit bd893adb34)
2021-05-21 22:42:45 +00:00
Tobias Fella
b3d90ebf82 Fix showing multiple deleted messages from the same author
(cherry picked from commit b48c9bdadc)
2021-05-21 22:42:15 +00:00
Tobias Fella
ef0a6e276c Show deleted messages
We used to show those, then a bug was fixed in the code that was
supposed to hide them.


(cherry picked from commit 116f883699)
2021-05-21 22:41:46 +00:00
Tobias Fella
a104968a29 Prioritize "low priority" over "direct chat" in roomList categories
There's no exact right or wrong here, but if a room was explicitly
marked as "low priority", we should honor this tag.

Fixes a slight inconsistency with the implementation in Element

Fixes #357


(cherry picked from commit 3ea783b370)
2021-05-21 19:12:00 +00:00
228 changed files with 3229 additions and 98646 deletions

View File

@@ -1,157 +0,0 @@
{
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
"nightly"
],
"desktop-file-name-suffix": " (Nightly)",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--filesystem=xdg-download",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [
{
"name": "kquickimageeditor",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/libraries/kquickimageeditor"
}
]
},
{
"name": "olm",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://gitlab.matrix.org/matrix-org/olm.git",
"tag": "3.2.10",
"x-checker-data": {
"type": "git",
"tag-pattern": "^([\\d.]+)$"
},
"commit": "9908862979147a71dc6abaecd521be526ae77be1"
}
]
},
{
"name": "libsecret",
"buildsystem": "meson",
"config-opts": [
"-Dmanpage=false",
"-Dvapi=false",
"-Dgtk_doc=false",
"-Dintrospection=false",
"-Dgcrypt=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz",
"sha256": "3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d",
"x-checker-data": {
"type": "gnome",
"name": "libsecret",
"stable-only": true
}
}
]
},
{
"name": "qtkeychain",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "archive",
"url": "https://github.com/frankosterfeld/qtkeychain/archive/v0.13.2.tar.gz",
"sha256": "20beeb32de7c4eb0af9039b21e18370faf847ac8697ab3045906076afbc4caa5",
"x-checker-data": {
"type": "anitya",
"project-id": 4138,
"stable-only": true,
"url-template": "https://github.com/frankosterfeld/qtkeychain/archive/v$version.tar.gz"
}
}
],
"config-opts": [
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DLIB_INSTALL_DIR=/app/lib",
"-DBUILD_TRANSLATIONS=NO"
]
},
{
"name": "libQuotient",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "dev"
}
],
"config-opts": [
"-DQuotient_ENABLE_E2EE=ON"
]
},
{
"name": "cmark",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/commonmark/cmark.git"
}
],
"config-opts": [
"-DCMARK_TESTS=OFF",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=/app"
],
"builddir": true
},
{
"name": "qcoro",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "archive",
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.4.0.tar.gz",
"sha256": "0e68b3f0ce7bf521ffbdd731464d2d60d8d7a39a749b551ed26855a1707d86d1",
"x-checker-data": {
"type": "anitya",
"project-id": 236236,
"stable-only": true,
"url-template": "https://github.com/danvratil/qcoro/archive/refs/tags/v$version.tar.gz"
}
}
]
},
{
"name": "neochat",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "dir",
"path": "."
}
],
"config-opts": [
"-DNEOCHAT_FLATPAK=ON"
]
}
]
}

2
.gitignore vendored
View File

@@ -5,5 +5,3 @@ build
neochat.kdev4
compile_commands.json
.cache/
.vscode/
kate.project.ctags.*

View File

@@ -1,12 +0,0 @@
# SPDX-FileCopyrightText: none
# SPDX-License-Identifier: CC0-1.0
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
# TODO enable once we can have qt6 libQuotient on the CI
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -1,28 +0,0 @@
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/sonnet': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'
Options:
require-passing-tests-on: [ 'Linux', 'Windows' ]

View File

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

View File

@@ -4,47 +4,43 @@
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.1)
project(NeoChat)
set(PROJECT_VERSION "22.09")
set(KF5_MIN_VERSION "5.91.0")
set(QT_MIN_VERSION "5.15.2")
set(KF5_MIN_VERSION "5.77.0")
set(QT_MIN_VERSION "5.15.0")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
include(FeatureSummary)
include(ECMSetupVersion)
include(KDEInstallDirs)
include(ECMFindQmlModule)
include(ECMQMLModules)
include(KDEClangFormat)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
ecm_setup_version(${PROJECT_VERSION}
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
cmake_policy(SET CMP0063 OLD)
ecm_setup_version(1.2.0
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
find_package(Qt5 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
@@ -58,8 +54,8 @@ set_package_properties(KF5Kirigami2 PROPERTIES
PURPOSE "Kirigami application UI framework"
)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
find_package(Qt5Keychain)
set_package_properties(Qt5Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
@@ -71,13 +67,11 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF5QQC2DesktopStyle ${KF5_MIN_VERSION} REQUIRED)
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.sonnet 1.0)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
@@ -87,7 +81,7 @@ endif()
find_package(Quotient 0.6)
set_package_properties(Quotient PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
DESCRIPTION "Qt wrapper arround Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
PURPOSE "Talk with matrix server"
)
@@ -111,27 +105,14 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro${QT_MAJOR_VERSION} COMPONENTS Core QUIET)
if(NOT QCoro${QT_MAJOR_VERSION}_FOUND)
find_package(QCoro REQUIRED)
endif()
qcoro_enable_coroutines()
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()
ki18n_install(po)
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@2/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@3/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/symbolic/apps)
install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
add_definitions(-DQT_NO_FOREACH)
@@ -139,14 +120,5 @@ add_subdirectory(src)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
if (NOT ANDROID)
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
endif()
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
# CI installs dependency headers to _install and _build, which break the reuse check
# Fixes the test by excluding this directory
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})

View File

@@ -1,11 +0,0 @@
Copyright (c) <year> <owner>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,446 +0,0 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
[This is the first released version of the library GPL. It is numbered 2 because
it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public Licenses are intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users.
This license, the Library General Public License, applies to some specially
designated Free Software Foundation software, and to any other libraries whose
authors decide to use it. You can use it for your libraries, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis or for
a fee, you must give the recipients all the rights that we gave you. You must
make sure that they, too, receive or can get the source code. If you link
a program with the library, you must provide complete object files to the
recipients so that they can relink them with the library, after making changes
to the library and recompiling it. And you must show them these terms so they
know their rights.
Our method of protecting your rights has two steps: (1) copyright the library,
and (2) offer you this license which gives you legal permission to copy, distribute
and/or modify the library.
Also, for each distributor's protection, we want to make certain that everyone
understands that there is no warranty for this free library. If the library
is modified by someone else and passed on, we want its recipients to know
that what they have is not the original version, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that companies distributing free software will individually
obtain patent licenses, thus in effect transforming the program into proprietary
software. To prevent this, we have made it clear that any patent must be licensed
for everyone's free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary GNU
General Public License, which was designed for utility programs. This license,
the GNU Library General Public License, applies to certain designated libraries.
This license is quite different from the ordinary one; be sure to read it
in full, and don't assume that anything in it is the same as in the ordinary
license.
The reason we have a separate public license for some libraries is that they
blur the distinction we usually make between modifying or adding to a program
and simply using it. Linking a program with a library, without changing the
library, is in some sense simply using the library, and is analogous to running
a utility program or application program. However, in a textual and legal
sense, the linked executable is a combined work, a derivative of the original
library, and the ordinary General Public License treats it as such.
Because of this blurred distinction, using the ordinary General Public License
for libraries did not effectively promote software sharing, because most developers
did not use the libraries. We concluded that weaker conditions might promote
sharing better.
However, unrestricted linking of non-free programs would deprive the users
of those programs of all benefit from the free status of the libraries themselves.
This Library General Public License is intended to permit developers of non-free
programs to use free libraries, while preserving your freedom as a user of
such programs to change the free libraries that are incorporated in them.
(We have not seen how to achieve this as regards changes in header files,
but we have achieved it as regards changes in the actual functions of the
Library.) The hope is that this will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and modification
follow. Pay close attention to the difference between a "work based on the
library" and a "work that uses the library". The former contains code derived
from the library, while the latter only works together with the library.
Note that it is possible for a library to be covered by the ordinary General
Public License rather than by this special one.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which contains a
notice placed by the copyright holder or other authorized party saying it
may be distributed under the terms of this Library General Public License
(also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared
so as to be conveniently linked with application programs (which use some
of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has
been distributed under these terms. A "work based on the Library" means either
the Library or any derivative work under copyright law: that is to say, a
work containing the Library or a portion of it, either verbatim or with modifications
and/or translated straightforwardly into another language. (Hereinafter, translation
is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications
to it. For a library, complete source code means all the source code for all
modules it contains, plus any associated interface definition files, plus
the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running a program
using the Library is not restricted, and output from such a program is covered
only if its contents constitute a work based on the Library (independent of
the use of the Library in a tool for writing it). Whether that is true depends
on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and disclaimer
of warranty; keep intact all the notices that refer to this License and to
the absence of any warranty; and distribute a copy of this License along with
the Library.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it,
thus forming a work based on the Library, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that
you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all
third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of
data to be supplied by an application program that uses the facility, other
than as an argument passed when the facility is invoked, then you must make
a good faith effort to ensure that, in the event an application does not supply
such function or table, the facility still operates, and performs whatever
part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose
that is entirely well-defined independent of the application. Therefore, Subsection
2d requires that any application-supplied function or table used by this function
must be optional: if the application does not supply it, the square root function
must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Library, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Library.
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may opt to apply the terms of the ordinary GNU General Public License
instead of this License to a given copy of the Library. To do this, you must
alter all the notices that refer to this License, so that they refer to the
ordinary GNU General Public License, version 2, instead of to this License.
(If a newer version than version 2 of the ordinary GNU General Public License
has appeared, then you can specify that version instead if you wish.) Do not
make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy,
so the ordinary GNU General Public License applies to all subsequent copies
and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library
into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of
it, under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you accompany it with the complete corresponding
machine-readable source code, which must be distributed under the terms of
Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated
place, then offering equivalent access to copy the source code from the same
place satisfies the requirement to distribute the source code, even though
third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but
is designed to work with the Library by being compiled or linked with it,
is called a "work that uses the Library". Such a work, in isolation, is not
a derivative work of the Library, and therefore falls outside the scope of
this License.
However, linking a "work that uses the Library" with the Library creates an
executable that is a derivative of the Library (because it contains portions
of the Library), rather than a "work that uses the library". The executable
is therefore covered by this License. Section 6 states terms for distribution
of such executables.
When a "work that uses the Library" uses material from a header file that
is part of the Library, the object code for the work may be a derivative work
of the Library even though the source code is not. Whether this is true is
especially significant if the work can be linked without the Library, or if
the work is itself a library. The threshold for this to be true is not precisely
defined by law.
If such an object file uses only numerical parameters, data structure layouts
and accessors, and small macros and small inline functions (ten lines or less
in length), then the use of the object file is unrestricted, regardless of
whether it is legally a derivative work. (Executables containing this object
code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute
the object code for the work under the terms of Section 6. Any executables
containing that work also fall under Section 6, whether or not they are linked
directly with the Library itself.
6. As an exception to the Sections above, you may also compile or link a "work
that uses the Library" with the Library to produce a work containing portions
of the Library, and distribute that work under terms of your choice, provided
that the terms permit modification of the work for the customer's own use
and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library
is used in it and that the Library and its use are covered by this License.
You must supply a copy of this License. If the work during execution displays
copyright notices, you must include the copyright notice for the Library among
them, as well as a reference directing the user to the copy of this License.
Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source
code for the Library including whatever changes were used in the work (which
must be distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete machine-readable "work
that uses the Library", as object code and/or source code, so that the user
can modify the Library and then relink to produce a modified executable containing
the modified Library. (It is understood that the user who changes the contents
of definitions files in the Library will not necessarily be able to recompile
the application to use the modified definitions.)
b) Accompany the work with a written offer, valid for at least three years,
to give the same user the materials specified in Subsection 6a, above, for
a charge no more than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy from a designated
place, offer equivalent access to copy the above specified materials from
the same place.
d) Verify that the user has already received a copy of these materials or
that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must
include any data and utility programs needed for reproducing the executable
from it. However, as a special exception, the source code distributed need
not include anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the operating
system on which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license restrictions of
other proprietary libraries that do not normally accompany the operating system.
Such a contradiction means you cannot use both them and the Library together
in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side
in a single library together with other library facilities not covered by
this License, and distribute such a combined library, provided that the separate
distribution of the work based on the Library and of the other library facilities
is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities. This must be distributed
under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of
it is a work based on the Library, and explaining where to find the accompanying
uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library
except as expressly provided under this License. Any attempt otherwise to
copy, modify, sublicense, link with, or distribute the Library is void, and
will automatically terminate your rights under this License. However, parties
who have received copies, or rights, from you under this License will not
have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Library or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Library
(or any work based on the Library), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library),
the recipient automatically receives a license from the original licensor
to copy, distribute, link with or modify the Library subject to these terms
and conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties to this License.
11. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Library at all. For example, if a
patent license would not permit royalty-free redistribution of the Library
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Library under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of
the Library General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Library does not specify a license version number, you may choose any version
ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs
whose distribution conditions are incompatible with these, write to the author
to ask for permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make exceptions
for this. Our decision will be guided by the two goals of preserving the free
status of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible
use to the public, we recommend making it free software that everyone can
redistribute and change. You can do so by permitting redistribution under
these terms (or, alternatively, under the terms of the ordinary General Public
License).
To apply these terms, attach the following notices to the library. It is safest
to attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the "copyright"
line and a pointer to where the full notice is found.
one line to give the library's name and an idea of what it does.
Copyright (C) year name of author
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Library General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more
details.
You should have received a copy of the GNU Library General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
signature of Ty Coon, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -1,175 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
one line to give the library's name and an idea of what it does.
Copyright (C) year name of author
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
signature of Ty Coon, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -1,12 +0,0 @@
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the license or (at your option) at any later version that is
accepted by the membership of KDE e.V. (or its successor
approved by the membership of KDE e.V.), which shall act as a
proxy as defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

View File

@@ -1,19 +0,0 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,4 +1,2 @@
#! /usr/bin/env bash
# SPDX-FileCopyrightText: None
# SPDX-License-Identifier: CC0-1.0
$XGETTEXT `find . \( -name \*.cpp -o -name \*.h -o -name \*.qml \)` -o $podir/neochat.pot

View File

@@ -1,8 +1,3 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
SPDX-License-Identifier: CC0-1.0
-->
# NeoChat
NeoChat is a client for Matrix, the decentralized communication protocol for instant
@@ -14,7 +9,7 @@ KConfig and KI18n.
## Get it
A stable release [is available](https://apps.kde.org/neochat) for download for Linux distributions.
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions.
Along with the stable release, a Flatpak version is available for the nightly
@@ -27,11 +22,11 @@ flatpak install kdeapps org.kde.neochat
```
A nightly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/NeoChat_Nightly_android-arm64/).
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/Neochat_android/).
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
![Timeline](https://invent.kde.org/websites/product-screenshots/-/raw/master/neochat/application.png)
## Features
@@ -55,7 +50,7 @@ We welcome contributions in this direction.
## Contact
You can reach the maintainers at [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org), if you are already on Matrix.
You can reach the maintainers at #neochat:kde.org, if you are already on Matrix.
Development happens in http://invent.kde.org/network/neochat (not in GitHub).
## Acknowledgement

View File

@@ -6,8 +6,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.neochat"
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:versionName="0.0.1"
android:versionCode="1604412458"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
@@ -21,7 +21,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.lib_name" android:value="neochat"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>

View File

@@ -1,96 +0,0 @@
/*
SPDX-FileCopyrightText: 2018-2020 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-FileCopyrightText: 2020 Gabriel Souza Franco <gabrielfrancosouza@gmail.com>
SPDX-License-Identifier: BSD-3-Clause
*/
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.4'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.application'
apply from: '../version.gradle'
def timestamp = (int)(new Date().getTime()/1000)
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
defaultConfig {
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
}
packagingOptions {
exclude 'lib/*/*RemoteObjects*'
exclude 'lib/*/*StateMachine*'
exclude 'lib/*/*_imageformats_qico_*'
exclude 'lib/*/*_imageformats_qicns_*'
exclude 'lib/*/*_imageformats_qtga_*'
exclude 'lib/*/*_imageformats_qtiff_*'
exclude 'lib/*/*_qmltooling_*'
}
aaptOptions {
// different syntax than above
// see https://android.googlesource.com/platform/frameworks/base/+/refs/heads/pie-release/tools/aapt2/util/Files.h#90
ignoreAssetsPattern '!<dir>ECM:!<dir>aclocal:!<dir>doc:!<dir>gtk-doc:!<dir>iso-codes:!<dir>man:!<dir>mime:!<dir>pkgconfig:!<dir>qlogging-categories5:!<file>iso_15924.mo:!<file>iso_3166-2.mo:!<file>iso_3166-3.mo:!<file>iso_4217.mo:!<file>iso_639-2.mo:!<file>iso_639-3.mo:!<file>iso_639-5.mo:!<file>kcodecs5_qt.qm:!<file>kde5_xml_mimetypes.qm'
}
}

View File

@@ -1,6 +0,0 @@
// SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: BSD-3-Clause
ext {
projectVersionFull = "@NEOCHAT_VERSION@"
}

View File

@@ -9,6 +9,6 @@ install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
${CMAKE_INSTALL_SYSCONFDIR}/fonts/conf.d/
)

View File

@@ -14,14 +14,14 @@ import NeoChat.Page 1.0
Loader {
id: root
property var attachmentMimetype: FileType.mimeTypeForUrl(chatBoxHelper.attachmentPath)
property var attachmentMimetype: FileType.mimeTypeForUrl(ChatBoxHelper.attachmentPath)
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
active: visible
sourceComponent: Component {
Pane {
id: attachmentPane
property string baseFileName: chatBoxHelper.attachmentPath.toString().substring(chatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, chatBoxHelper.attachmentPath.length)
property string baseFileName: ChatBoxHelper.attachmentPath.toString().substring(ChatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, ChatBoxHelper.attachmentPath.length)
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Item {
@@ -46,7 +46,7 @@ Loader {
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
smooth: height == preferredHeight && parent.height == parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? chatBoxHelper.attachmentPath : ""
source: hasImage ? ChatBoxHelper.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
@@ -162,14 +162,14 @@ Loader {
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: chatBoxHelper.attachmentPath
imagePath: ChatBoxHelper.attachmentPath
}
}
onClicked: {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
chatBoxHelper.attachmentPath = newPath;
ChatBoxHelper.attachmentPath = newPath;
});
}
ToolTip.text: text
@@ -180,7 +180,7 @@ Loader {
icon.name: "dialog-cancel"
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: chatBoxHelper.clearAttachment();
onClicked: ChatBoxHelper.clearAttachment();
ToolTip.text: text
ToolTip.visible: hovered
}

View File

@@ -5,11 +5,10 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Templates 2.15 as T
import Qt.labs.platform 1.1 as Platform
import QtQuick.Window 2.15
import org.kde.kirigami 2.18 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
ToolBar {
@@ -63,14 +62,10 @@ ToolBar {
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
+ inputField.topPadding + inputField.bottomPadding
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
FontMetrics {
id: fontMetrics
font: inputField.font
}
TextArea {
id: inputField
focus: true
@@ -86,7 +81,6 @@ ToolBar {
cursorShape: Qt.IBeamCursor
z: 1
}
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
topPadding: 0
@@ -97,22 +91,12 @@ ToolBar {
//property int lineHeight: contentHeight / lineCount
text: inputFieldText
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message")
placeholderText: currentRoom.usesEncryption ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : i18n("Write your message...")
verticalAlignment: TextEdit.AlignVCenter
horizontalAlignment: TextEdit.AlignLeft
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
readOnly: currentRoom.usesEncryption
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.SpellChecking.enabled: true
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
hoverEnabled: !Kirigami.Settings.tabletMode
selectByMouse: !Kirigami.Settings.tabletMode
ChatDocumentHandler {
id: documentHandler
@@ -123,21 +107,36 @@ ToolBar {
room: currentRoom ?? null
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
}
}
Timer {
id: repeatTimer
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
function sendMessage(event) {
if (isCompleting && completionMenu.count > 0) {
if (isCompleting) {
chatBar.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
isCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
currentRoom.sendTypingNotification(false)
chatBar.postMessage()
}
isCompleting = false;
}
Keys.onReturnPressed: { sendMessage(event) }
@@ -148,7 +147,11 @@ ToolBar {
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
replyPreviousUserMessage();
@@ -180,25 +183,6 @@ ToolBar {
chatBar.complete();
}
// yes, decrement goes up and increment goes down visually.
Keys.onUpPressed: (event) => {
if (chatBar.isCompleting) {
event.accepted = true
completionMenu.listView.decrementCurrentIndex()
autoAppeared = true;
}
event.accepted = false
}
Keys.onDownPressed: (event) => {
if (chatBar.isCompleting) {
event.accepted = true
completionMenu.listView.incrementCurrentIndex()
autoAppeared = true;
}
event.accepted = false
}
Keys.onTabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomDown();
@@ -228,26 +212,16 @@ ToolBar {
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
currentRoom.sendTypingNotification(true)
}
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
const completionInfo = documentHandler.getAutocompletionInfo();
if (completionInfo.type === ChatDocumentHandler.Ignore) {
if (completionInfo.keyword) {
// custom emojis
const idx = completionMenu.currentIndex;
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
completionMenu.currentIndex = idx;
}
return;
}
if (completionInfo.type === ChatDocumentHandler.None) {
isCompleting = false;
return;
@@ -255,36 +229,33 @@ ToolBar {
completionMenu.completionType = completionInfo.type
if (completionInfo.type === ChatDocumentHandler.User) {
completionMenu.model = currentRoom.getUsers(completionInfo.keyword, 10);
completionMenu.model = currentRoom.getUsers(completionInfo.keyword);
} else if (completionInfo.type === ChatDocumentHandler.Command) {
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
} else {
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
completionMenu.model = EmojiModel.filterModel(completionInfo.keyword);
}
if (completionMenu.model.length === 0) {
isCompleting = false;
return;
}
if (!isCompleting) {
isCompleting = true
autoAppeared = true;
completionMenu.endPosition = cursorPosition
}
isCompleting = true
autoAppeared = true;
completionMenu.endPosition = cursorPosition
}
}
}
Item {
visible: !chatBoxHelper.isReplying && (!chatBoxHelper.hasAttachment || uploadingBusySpinner.running)
visible: !ChatBoxHelper.isReplying && (!ChatBoxHelper.hasAttachment || uploadingBusySpinner.running)
implicitWidth: uploadButton.implicitWidth
implicitHeight: uploadButton.implicitHeight
ToolButton {
id: uploadButton
anchors.fill: parent
// Matrix does not allow sending attachments in replies
visible: !chatBoxHelper.isReplying && !chatBoxHelper.hasAttachment && !uploadingBusySpinner.running
visible: !ChatBoxHelper.isReplying && !ChatBoxHelper.hasAttachment && !uploadingBusySpinner.running
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: AbstractButton.IconOnly
@@ -296,7 +267,7 @@ ToolBar {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) { return }
chatBoxHelper.attachmentPath = path;
ChatBoxHelper.attachmentPath = path;
})
fileDialog.open()
}
@@ -370,25 +341,21 @@ ToolBar {
}
}
property CustomEmojiModel customEmojiModel: CustomEmojiModel {
connection: Controller.activeConnection
}
function pasteImage() {
let localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
if (!Clipboard.saveImage(localPath)) {
return;
}
chatBoxHelper.attachmentPath = localPath;
ChatBoxHelper.attachmentPath = localPath;
}
function postMessage() {
checkForFancyEffectsReason();
if (chatBoxHelper.hasAttachment) {
if (ChatBoxHelper.hasAttachment) {
// send attachment but don't reset the text
actionsHandler.postMessage("", chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, {}, this.customEmojiModel);
actionsHandler.postMessage("", ChatBoxHelper.attachmentPath,
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {});
currentRoom.markAllMessagesAsRead();
messageSent();
return;
@@ -400,8 +367,8 @@ ToolBar {
actionsHandler.postEdit(inputField.text);
} else {
// send normal message
actionsHandler.postMessage(inputField.text.trim(), chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
actionsHandler.postMessage(inputField.text.trim(), ChatBoxHelper.attachmentPath,
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted);
}
currentRoom.markAllMessagesAsRead();
inputField.clear();

View File

@@ -61,10 +61,6 @@ Item {
QQC2.Pane {
id: connectionPane
padding: fontMetrics.lineSpacing * 0.25
FontMetrics {
id: fontMetrics
font: networkLabel.font
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
@@ -73,7 +69,6 @@ Item {
visible: !Controller.isOnline
width: parent.width
QQC2.Label {
id: networkLabel
text: i18n("NeoChat is offline. Please check your network connection.")
}
anchors.bottom: emojiPickerLoaderSeparator.top
@@ -95,16 +90,9 @@ Item {
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: replySeparator.top
sourceComponent: QQC2.Pane {
topPadding: 0
bottomPadding: 0
rightPadding: 0
leftPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: EmojiPicker {
textArea: chatBar.textField
onChosen: addText(emoji)
}
sourceComponent: EmojiPicker{
textArea: chatBar.textField
onChosen: addText(emoji)
}
Behavior on height {
NumberAnimation {
@@ -127,8 +115,8 @@ Item {
ReplyPane {
id: replyPane
visible: chatBoxHelper.isReplying || chatBoxHelper.isEditing
user: chatBoxHelper.replyUser
visible: ChatBoxHelper.isReplying || ChatBoxHelper.isEditing
user: ChatBoxHelper.replyUser
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: attachmentSeparator.top
@@ -154,7 +142,7 @@ Item {
AttachmentPane {
id: attachmentPane
visible: chatBoxHelper.hasAttachment
visible: ChatBoxHelper.hasAttachment
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: chatBarSeparator.top
@@ -248,7 +236,7 @@ Item {
}
Connections {
target: chatBoxHelper
target: ChatBoxHelper
function onShouldClearText() {
root.inputFieldText = "";
@@ -277,7 +265,7 @@ Item {
}
function closeAll() {
chatBoxHelper.clear();
ChatBoxHelper.clear();
chatBar.emojiPaneOpened = false;
}
}

View File

@@ -10,14 +10,12 @@ import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Popup {
id: control
// Expose internal ListView properties.
property alias model: completionListView.model
property alias listView: completionListView
property alias currentIndex: completionListView.currentIndex
property alias currentItem: completionListView.currentItem
property alias count: completionListView.count
@@ -44,10 +42,9 @@ Popup {
completionListView.currentIndex = 0;
}
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 5)
contentItem: ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: completionListView
@@ -87,7 +84,6 @@ Popup {
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
}
labelItem.textFormat: Text.PlainText
text: modelData.displayName
onClicked: completeTriggered();
}
@@ -98,40 +94,23 @@ Popup {
Kirigami.BasicListItem {
id: emojiItem
width: ListView.view.width ?? implicitWidth
property string displayName: modelData.isCustom ? modelData.shortname : modelData.unicode
text: modelData.shortname
height: Kirigami.Units.gridUnit * 2
property string displayName: modelData.unicode
text: modelData.unicode + " " + modelData.shortname
leading: Image {
source: modelData.isCustom ? modelData.unicode : ""
width: height
sourceSize.width: width
sourceSize.height: height
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: ShimmerGradient { }
}
Label {
id: unicodeLabel
visible: !modelData.isCustom
font.family: 'emoji'
font.pixelSize: height - 2
text: modelData.unicode
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
}
leading: Label {
id: unicodeLabel
Layout.preferredHeight: Kirigami.Units.gridUnit
Layout.preferredWidth: textMetrics.tightBoundingRect.width
font.pointSize: Kirigami.Units.gridUnit * 0.75
text: modelData.unicode
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
TextMetrics {
id: textMetrics
text: modelData.unicode
font: unicodeLabel.font
}
onClicked: completeTriggered();
}
}

View File

@@ -12,7 +12,7 @@ import org.kde.neochat 1.0
Loader {
id: root
readonly property bool isEdit: chatBoxHelper.isEditing
readonly property bool isEdit: ChatBoxHelper.isEditing
property var user: null
property string avatarMediaUrl: user ? "image://mxc/" + user.avatarMediaId : ""
@@ -57,7 +57,7 @@ Loader {
elide: Text.ElideRight
text: {
let heading = "<b>%1</b>"
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
let userName = user ? "<font color=\""+ user.color +"\">" + user.displayName + "</font>" : ""
if (isEdit) {
heading = heading.arg(i18n("Editing message:")) + "<br/>"
} else {
@@ -71,10 +71,6 @@ Loader {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
TextArea {
id: textArea
leftPadding: 0
@@ -83,7 +79,7 @@ Loader {
bottomPadding: 0
text: {
const stylesheet = "<style> a{color:"+Kirigami.Theme.linkColor+";}.user-pill{}</style>";
const content = chatBoxHelper.isReplying ? chatBoxHelper.replyEventContent : chatBoxHelper.editContent;
const content = ChatBoxHelper.isReplying ? ChatBoxHelper.replyEventContent : ChatBoxHelper.editContent;
return stylesheet + content;
}
selectByMouse: true
@@ -91,7 +87,7 @@ Loader {
readOnly: true
wrapMode: Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
background: null
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
@@ -106,7 +102,7 @@ Loader {
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: {
chatBoxHelper.clear();
ChatBoxHelper.clearEditReply();
root.replyCancelled();
}
ToolTip.text: text

View File

@@ -10,16 +10,10 @@ import org.kde.neochat 1.0 as NeoChat
import NeoChat.Component 1.0
ColumnLayout {
id: _picker
property string emojiCategory: "history"
property var textArea
readonly property var emojiModel: NeoChat.EmojiModel
property NeoChat.CustomEmojiModel customModel: NeoChat.CustomEmojiModel {
connection: NeoChat.Controller.activeConnection
}
signal chosen(string emoji)
spacing: 0
@@ -35,7 +29,6 @@ ColumnLayout {
orientation: ListView.Horizontal
model: ListModel {
ListElement { label: "custom"; category: "custom" }
ListElement { label: "⌛️"; category: "history" }
ListElement { label: "😏"; category: "people" }
ListElement { label: "🌲"; category: "nature" }
@@ -48,23 +41,16 @@ ColumnLayout {
}
delegate: ItemDelegate {
id: del
required property string label
required property string category
width: contentItem.Layout.preferredWidth
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
level: del.label === "custom" ? 4 : 1
level: 1
Layout.preferredWidth: del.label === "custom" ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2
font.family: del.label === "custom" ? Kirigami.Theme.defaultFont.family : 'emoji'
text: del.label === "custom" ? i18n("Custom") : del.label
font.family: 'emoji'
text: label
}
Rectangle {
@@ -101,8 +87,6 @@ ColumnLayout {
model: {
switch (emojiCategory) {
case "custom":
return _picker.customModel
case "history":
return emojiModel.history
case "people":
@@ -134,32 +118,11 @@ ColumnLayout {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: 'emoji'
text: modelData.isCustom ? "" : modelData.unicode
}
Image {
visible: modelData.isCustom
source: visible ? modelData.unicode : ""
anchors.fill: parent
anchors.margins: 2
sourceSize.width: width
sourceSize.height: height
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: ShimmerGradient { }
}
text: modelData.unicode
}
onClicked: {
if (modelData.isCustom) {
chosen(modelData.shortname)
} else {
chosen(modelData.unicode)
}
chosen(modelData.unicode)
emojiModel.emojiUsed(modelData)
}
}

View File

@@ -3,306 +3,61 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
Popup {
ApplicationWindow {
id: root
property alias source: image.source
property string filename
property string blurhash: ""
property int imageWidth: -1
property int imageHeight: -1
property var modelData
property url localPath
parent: Overlay.overlay
closePolicy: Popup.CloseOnEscape
width: parent.width
height: parent.height
modal: true
padding: 0
background: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
ColumnLayout {
anchors.fill: parent
spacing: Kirigami.Units.largeSpacing
title: i18n("Image View - %1", filename)
Control {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
name: modelData.author.name ?? modelData.author.displayName
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
color: modelData.author.color
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
id: nameLabel
text: modelData.author.displayName
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
}
Label {
id: timeLabel
text: time.toLocaleString(Qt.locale(), Locale.ShortFormat)
}
}
Label {
id: imageLabel
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
text: modelData.display
font.weight: Font.Bold
elide: Text.ElideRight
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom in")
Accessible.name: text
icon.name: "zoom-in"
display: AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor + 0.25
if (image.scaleFactor > 3) {
image.scaleFactor = 3
}
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom out")
Accessible.name: text
icon.name: "zoom-out"
display: AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor - 0.25
if (image.scaleFactor < 0.25) {
image.scaleFactor = 0.25
}
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate left")
Accessible.name: text
icon.name: "image-rotate-left-symbolic"
display: AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle - 90
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate right")
Accessible.name: text
icon.name: "image-rotate-right-symbolic"
display: AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle + 90
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Save as")
Accessible.name: text
icon.name: "document-save"
display: AbstractButton.IconOnly
onClicked: {
var dialog = saveAsDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Close")
Accessible.name: text
icon.name: "dialog-close"
display: AbstractButton.IconOnly
onClicked: {
root.close()
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
}
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
}
Kirigami.Separator {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 1
}
}
BusyIndicator {
Layout.fillWidth: true
visible: image.status !== Image.Ready && root.blurhash === ""
running: visible
}
// Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area.
Item {
id: imageContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
clip: true
Image {
id: image
property var scaleFactor: 1
property int rotationAngle: 0
property var rotationInsensitiveWidth: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2)
property var rotationInsensitiveHeight: Math.min(root.imageHeight !== -1 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2)
anchors.centerIn: parent
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
fillMode: Image.PreserveAspectFit
clip: true
Behavior on width {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on height {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Image {
anchors.centerIn: parent
width: image.width
height: image.height
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
visible: root.blurhash !== "" && parent.status !== Image.Ready
}
transform: [
Rotation {
origin.x: image.width / 2
origin.y: image.height / 2
angle: image.rotationAngle
Behavior on angle {
RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
},
Scale {
origin.x: image.width / 2
origin.y: image.height / 2
xScale: image.scaleFactor
yScale: image.scaleFactor
Behavior on xScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on yScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
}
]
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author,
message: modelData.message,
eventId: modelData.eventId,
source: modelData.source,
file: root.parent,
mimeType: modelData.mimeType,
progressInfo: modelData.progressInfo,
plainMessage: modelData.message,
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
root.close()
}
}
}
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
Component {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
}
}
color: Kirigami.Theme.backgroundColor
background: AbstractButton {
onClicked: root.destroy()
}
onClosed: {
image.scaleFactor = 1
image.rotationAngle = 0
BusyIndicator {
visible: image.status !== Image.Ready
anchors.centerIn: parent
running: visible
}
AnimatedImage {
id: image
anchors.centerIn: parent
width: Math.min(sourceSize.width, root.width)
height: Math.min(sourceSize.height, root.height)
cache: false
fillMode: Image.PreserveAspectFit
source: localPath
}
Button {
anchors.top: parent.top
anchors.right: parent.right
text: i18n("Close")
icon.name: "dialog-close"
display: AbstractButton.IconOnly
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
onClicked: root.destroy()
}
}

View File

@@ -5,18 +5,16 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.LoadingPlaceholder {
QQC2.BusyIndicator {
property var showContinueButton: false
property var showBackButton: false
text: i18n("Synchronizing with your homeserver…")
icon.name: "cloud-download"
property string title: i18n("Loading")
anchors.centerIn: parent
explanation: i18n("Please wait. This might take a little while.")
}

View File

@@ -14,29 +14,24 @@ import NeoChat.Component 1.0
LoginStep {
id: login
showContinueButton: LoginHelper.homeserverReachable
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Welcome to NeoChat!")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
QQC2.Label {
text: i18n("To get started, enter your matrix ID:")
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if(acceptableInput) {
LoginHelper.matrixId = text
} else {
LoginHelper.matrixId = ""
}
}
@@ -49,13 +44,13 @@ LoginStep {
}
validator: RegularExpressionValidator {
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(\:[0-9]+)?$/
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
}
}
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
@@ -66,6 +61,5 @@ LoginStep {
}
}
enabled: LoginHelper.homeserverReachable
iconName: "go-next"
}
}

View File

@@ -25,7 +25,6 @@ LoginStep {
onTriggered: {
LoginHelper.login();
}
iconName: "go-next"
}
Connections {

View File

@@ -19,30 +19,20 @@ LoginStep {
Kirigami.FormLayout {
Connections {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
}
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
onSsoUrlChanged: {
Qt.openUrlExternally(LoginHelper.ssoUrl)
}
onConnected: processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
RowLayout {
QQC2.Button {
text: i18nc("@action:button", "Back")
onClicked: {
module.source = "qrc:/imports/NeoChat/Component/Login/Login.qml"
}
}
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
}
}

View File

@@ -1,108 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Layouts 1.10
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.14 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
QQC2.Popup {
id: _popup
Shortcut {
sequence: "Ctrl+K"
enabled: !Kirigami.Settings.hasPlatformMenuBar
onActivated: _popup.open()
}
onVisibleChanged: {
if (!visible) {
return
}
quickSearch.forceActiveFocus()
quickSearch.text = ""
}
anchors.centerIn: QQC2.Overlay.overlay
background: Kirigami.Card {}
height: 2 * Math.round(implicitHeight / 2)
padding: Kirigami.Units.largeSpacing * 2
contentItem: ColumnLayout {
spacing: Kirigami.Units.largeSpacing * 2
Kirigami.SearchField {
id: quickSearch
// TODO: get this broken property removed/disabled by default in Kirigami,
// we used to be able to expect that the text field wouldn't attempt to
// perform a mini-DDOS attack using signals.
autoAccept: false
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
Keys.onLeftPressed: cView.decrementCurrentIndex()
Keys.onRightPressed: cView.incrementCurrentIndex()
onAccepted: {
const item = cView.itemAtIndex(cView.currentIndex)
RoomManager.enterRoom(item.currentRoom)
_popup.close()
}
}
ListView {
id: cView
orientation: Qt.Horizontal
spacing: Kirigami.Units.largeSpacing
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
filterText: quickSearch.text
roomSortOrder: SortFilterRoomListModel.LastActivity
}
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.fillWidth: true
delegate: Kirigami.Avatar {
id: del
implicitHeight: Kirigami.Units.gridUnit * 3
implicitWidth: Kirigami.Units.gridUnit * 3
required property string avatar
required property var currentRoom
required property int index
// When an item is hovered set the currentIndex of listview to it so that it is highlighted
onHoveredChanged: {
if (!hovered) {
return
}
cView.currentIndex = index
}
actions.main: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
_popup.close()
}
}
source: avatar != "" ? "image://mxc/" + avatar : ""
}
}
}
modal: true
focus: true
}

View File

@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// Not to be confused with the Shimmer project.
// I like their gradiented GTK themes though.
import QtQuick 2.15
import org.kde.kirigami 2.15 as Kirigami
Gradient {
id: gradient
orientation: Gradient.Horizontal
property color color: Kirigami.Theme.textColor
property color translucent: Qt.rgba(color.r, color.g, color.b, 0.2)
property color bright: Qt.rgba(color.r, color.g, color.b, 0.3)
property real pos: 0.5
property real offset: 0.6
property SequentialAnimation ani: SequentialAnimation {
running: true
loops: Animation.Infinite
NumberAnimation {
from: -2.0
to: 2.0
duration: 700
target: gradient
properties: "pos"
}
PauseAnimation {
duration: 300
}
}
GradientStop { position: gradient.pos-gradient.offset; color: gradient.translucent }
GradientStop { position: gradient.pos; color: gradient.bright }
GradientStop { position: gradient.pos+gradient.offset; color: gradient.translucent }
}

View File

@@ -1,121 +1,61 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1 as Platform
import QtMultimedia 5.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: audioDelegate
Control {
id: root
onReplyClicked: ListView.view.goToEvent(eventID)
Layout.fillWidth: true
onOpenContextMenu: openFileContext(model, audioDelegate)
Audio {
id: audio
source: currentRoom.urlToMxcUrl(content.url)
autoLoad: false
}
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
onDownloadedChanged: audio.play()
contentItem: ColumnLayout {
RowLayout {
ToolButton {
icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
hoverComponent: hoverActions
innerObject: Control {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.contentMaxWidth
Audio {
id: audio
source: model.progressInfo.localPath
autoLoad: false
}
states: [
State {
name: "notDownloaded"
when: !model.progressInfo.completed && !model.progressInfo.active
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: currentRoom.downloadFile(model.eventId)
}
},
State {
name: "downloading"
when: model.progressInfo.active && !model.progressInfo.completed
PropertyChanges {
target: downloadBar
visible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-stop"
onClicked: {
currentRoom.cancelFileTransfer(model.eventId)
}
}
},
State {
name: "paused"
when: model.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: {
onClicked: {
if (audio.playbackState == Audio.PlayingState) {
audio.pause()
} else {
audio.play()
}
}
},
State {
name: "playing"
when: model.progressInfo.completed && audio.playbackState === Audio.PlayingState
PropertyChanges {
target: playButton
icon.name: "media-playback-pause"
onClicked: audio.pause()
}
}
]
contentItem: ColumnLayout {
RowLayout {
ToolButton {
id: playButton
}
Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
Label {
text: model.display
}
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
// Server doesn't support seeking, so use ProgressBar instead of Slider :(
ProgressBar {
id: downloadBar
visible: false
Layout.fillWidth: true
from: 0
to: model.content.info.size
value: model.progressInfo.progress
to: audio.duration
value: audio.position
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Slider {
from: 0
to: audio.duration
value: audio.position
onMoved: audio.seek(value)
}
Label {
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
Label {
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
}
}

View File

@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TimelineContainer {
id: encryptedDelegate
innerObject: TextEdit {
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
color: Kirigami.Theme.disabledTextColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
Layout.maximumWidth: encryptedDelegate.contentMaxWidth
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
}
}

View File

@@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "state"
delegate: StateDelegate {}
}
DelegateChoice {
roleValue: "emote"
delegate: MessageDelegate {
isEmote: true
}
}
DelegateChoice {
roleValue: "message"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: "notice"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: "image"
delegate: ImageDelegate {}
}
DelegateChoice {
roleValue: "sticker"
delegate: ImageDelegate {}
}
DelegateChoice {
roleValue: "audio"
delegate: AudioDelegate {}
}
DelegateChoice {
roleValue: "video"
delegate: VideoDelegate {}
}
DelegateChoice {
roleValue: "file"
delegate: FileDelegate {}
}
DelegateChoice {
roleValue: "encrypted"
delegate: EncryptedDelegate {}
}
DelegateChoice {
roleValue: "readMarker"
delegate: ReadMarkerDelegate {}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
}

View File

@@ -2,8 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
@@ -13,125 +14,70 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: fileDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, fileDelegate)
RowLayout {
id: root
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
onDownloadedChanged: if (downloaded && openOnFinished) {
openSavedFile();
}
ToolButton {
icon.name: progressInfo.completed ? "document-open" : "document-save"
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ColumnLayout {
Kirigami.Heading {
Layout.fillWidth: true
level: 4
text: model.display
wrapMode: Label.Wrap
}
Label {
Layout.fillWidth: true
text: !progressInfo.completed && progressInfo.active ? (Controller.formatByteSize(progressInfo.progress) + "/" + Controller.formatByteSize(progressInfo.total)) : Controller.formatByteSize(content.info ? content.info.size : 0)
color: Kirigami.Theme.disabledTextColor
wrapMode: Label.Wrap
}
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
function openSavedFile() {
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
function downloadAndOpen() {
if (downloaded) {
openSavedFile();
} else {
openOnFinished = true;
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/"
+ eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId));
}
}
innerObject: RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: fileDelegate.contentMaxWidth
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloaded"
when: progressInfo.completed
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: progressInfo.active
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: currentRoom.cancelFileTransfer(eventId)
}
},
State {
name: "raw"
when: true
PropertyChanges {
target: downloadButton
onClicked: fileDelegate.saveFileAs()
}
}
]
Kirigami.Icon {
id: ikon
source: model.fileMimetypeIcon
fallback: "unknown"
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
QQC2.Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
QQC2.Label {
id: sizeLabel
text: Controller.formatByteSize(content.info ? content.info.size : 0)
opacity: 0.7
Layout.fillWidth: true
}
}
QQC2.Button {
id: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@@ -4,23 +4,16 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: imageDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, imageDelegate)
Image {
id: img
property var content: model.content
readonly property bool isAnimated: contentType === "image/gif"
@@ -32,93 +25,69 @@ TimelineContainer {
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
property bool readonly: false
innerObject: Image {
id: img
source: "image://mxc/" + mediaId
Layout.maximumWidth: imageDelegate.contentMaxWidth
Layout.maximumHeight: imageDelegate.contentMaxWidth / imageDelegate.info.w * imageDelegate.info.h
Layout.preferredWidth: imageDelegate.info.w
Layout.preferredHeight: imageDelegate.info.h
source: model.mediaUrl
fillMode: Image.PreserveAspectFit
Image {
anchors.fill: parent
source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : ""
visible: parent.status !== Image.Ready
}
ToolTip.text: display
ToolTip.visible: hoverHandler.hovered
fillMode: Image.PreserveAspectFit
HoverHandler {
id: hoverHandler
enabled: img.readonly
}
ToolTip.text: model.display
ToolTip.visible: hoverHandler.hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Rectangle {
anchors.fill: parent
HoverHandler {
id: hoverHandler
}
visible: progressInfo.active && !downloaded
Rectangle {
anchors.fill: parent
color: "#BB000000"
visible: progressInfo.active && !downloaded
ProgressBar {
anchors.centerIn: parent
color: "#BB000000"
width: parent.width * 0.8
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onTapped: {
img.ToolTip.hide()
fullScreenImage.open()
}
}
FullScreenImage {
id: fullScreenImage
filename: eventId
source: mediaUrl
blurhash: model.content.info["xyz.amorgan.blurhash"]
imageWidth: content.info.w
imageHeight: content.info.h
modelData: model
}
function downloadAndOpen() {
if (downloaded) {
openSavedFile()
} else {
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}
function openSavedFile() {
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
function saveFileAs() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}
function openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
RowLayout {
id: row
property var links: model.display.match(/(\bhttps?:\/\/[^\s\<\>\"\']*[^\s\<\>\"\'])/g)
// don't show previews for room links or user mentions
.filter(link => !link.includes("https://matrix.to"))
// remove ending fullstops and commas
.map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link)
LinkPreviewer {
id: lp
url: links[0]
}
visible: lp.loaded && lp.title
Rectangle {
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
visible: lp.loaded && lp.title
color: Kirigami.Theme.highlightColor
}
Image {
visible: lp.imageSource
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
Layout.maximumWidth: Kirigami.Units.gridUnit * 5
source: lp.imageSource.replace("mxc://", "image://mxc/")
fillMode: Image.PreserveAspectFit
}
ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
Layout.fillWidth: true
level: 4
wrapMode: Text.Wrap
textFormat: Text.RichText
text: "<style>
a {
text-decoration: none;
}
</style>
<a href=\"" + links[0] + "\">" + lp.title.replace("&ndash;", "—") + "</a>"
visible: lp.loaded
onLinkActivated: RoomManager.openResource(link)
}
Label {
text: lp.description
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
Layout.fillWidth: true
wrapMode: Text.Wrap
visible: lp.loaded && lp.description
}
}
}

View File

@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TimelineContainer {
id: messageDelegate
property bool isEmote: false
onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument))
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: ColumnLayout {
Layout.maximumWidth: messageDelegate.contentMaxWidth
RichLabel {
id: label
isEmote: messageDelegate.isEmote
}
Loader {
id: linkPreviewLoader
Layout.fillWidth: true
height: active ? item.implicitHeight : 0
active: !currentRoom.usesEncryption && model.display && model.display.includes("http")
visible: active
sourceComponent: LinkPreviewDelegate {
anchors.verticalCenter: parent.verticalCenter
}
}
}
}

View File

@@ -29,7 +29,7 @@ Flow {
shadow.size: Kirigami.Units.smallSpacing
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
border.width: Kirigami.Units.devicePixelRatio
}

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
id: readMarkerDelegate
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
// extraWidth defines how the delegate can grow after the listView gets very wide
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width - Kirigami.Units.largeSpacing * 2 : Math.min(messageListView.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth)
width: delegateMaxWidth
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: Kirigami.Units.largeSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: readMarkerDelegate
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: readMarkerDelegate
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
}
}
]
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
opacity: 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
}
Timer {
id: makeMeDisapearTimer
interval: Kirigami.Units.humanMoment * 2
onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
}
ListView.onPooled: makeMeDisapearTimer.stop()
ListView.onAdd: {
const view = ListView.view;
if (view.atYEnd) {
makeMeDisapearTimer.start()
}
}
// When the read marker is visible and we are at the end of the list,
// start the makeMeDisapearTimer
Connections {
target: ListView.view
function onAtYEndChanged() {
makeMeDisapearTimer.start();
}
}
ListView.onRemove: {
const view = ListView.view;
if (view.atYEnd) {
// easy case just mark everything as read
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
return;
}
// mark the last visible index
const lastVisibleIdx = lastVisibleIndex();
if (lastVisibleIdx < index) {
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole)
}
}
}

View File

@@ -15,22 +15,21 @@ MouseArea {
id: replyButton
Layout.fillWidth: true
implicitHeight: replyName.implicitHeight + (loader.item ? loader.item.height : 0) + Kirigami.Units.largeSpacing
implicitWidth: Math.min(contentMaxWidth, Math.max((loader.item ? loader.item.width : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
implicitWidth: Math.min(bubbleMaxWidth, Math.max((loader.item ? loader.item.width + Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
Component.onCompleted: {
parent.Layout.fillWidth = true;
parent.Layout.preferredWidth = Qt.binding(function() { return implicitWidth; })
parent.Layout.maximumWidth = Qt.binding(function() { return contentMaxWidth + Kirigami.Units.largeSpacing * 2; })
parent.Layout.maximumWidth = Qt.binding(function() { return bubbleMaxWidth + Kirigami.Units.largeSpacing * 2; })
}
Rectangle {
id: replyLeftBorder
width: Kirigami.Units.smallSpacing
height: parent.height
x: Config.compactLayout ? Kirigami.Units.largeSpacing : 0
color: Kirigami.Theme.highlightColor
}
Kirigami.Avatar {
id: replyAvatar
id: avatatReply
anchors.left: replyLeftBorder.right
anchors.leftMargin: Kirigami.Units.smallSpacing
width: visible ? Kirigami.Units.gridUnit : 0
@@ -47,12 +46,12 @@ MouseArea {
QQC2.Label {
id: replyName
anchors {
left: replyAvatar.right
left: avatatReply.right
leftMargin: Kirigami.Units.smallSpacing
right: parent.right
rightMargin: Kirigami.Units.smallSpacing
}
text: currentRoom.htmlSafeMemberName(reply.author.id)
text: reply.author.displayName
color: reply.author.color
elide: Text.ElideRight
}
@@ -75,12 +74,13 @@ MouseArea {
Component {
id: textComponent
RichLabel {
TextDelegate {
id: replyText
textMessage: reply.display
textFormat: Text.RichText
width: Math.min(implicitWidth, contentMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, bubbleMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + avatatReply.width
}
}
@@ -94,9 +94,9 @@ MouseArea {
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
source: "image://mxc/" + mediaId
width: contentMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - replyAvatar.width
width: bubbleMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - avatatReply.width
height: reply.content.info.h / reply.content.info.w * width
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
x: Kirigami.Units.smallSpacing * 3 + avatatReply.width
}
}
}

View File

@@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.neochat 1.0
import org.kde.kirigami 2.15 as Kirigami
TextEdit {
id: contentLabel
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
readonly property var hasSpoiler: /data-mx-spoiler/g
property bool isEmote: false
/* Turn all links which aren't already in <a> tags into <a> hyperlinks */
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g
property string textMessage: model.display.includes("http")
? model.display.replace(linkRegex, function() {
if (arguments[1]) {
return arguments[0];
} else {
var l = arguments[2];
if ([".", ","].includes(l[l.length-1])) {
var link = l.substring(0, l.length-1);
var leftover = l[l.length-1];
return "<a href=\"" + link + "\">" + link + "</a>" + leftover;
}
return "<a href=\"" + l + "\">" + l + "</a>";
}
})
: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
Layout.fillWidth: true
persistentSelection: true
text: "<style>
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
" + (!spoilerRevealed ? "
[data-mx-spoiler] a {
color: transparent;
background: " + Kirigami.Theme.textColor + ";
}
[data-mx-spoiler] {
color: transparent;
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: {
spoilerRevealed = true
RoomManager.openResource(link)
}
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
} else {
applicationWindow().hoverLinkIndicator.text = "";
}
HoverHandler {
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TapHandler {
enabled: !parent.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true
}
}

View File

@@ -9,8 +9,6 @@ import org.kde.kirigami 2.15 as Kirigami
Kirigami.Heading {
level: 4
text: model.showSection ? section : ""
color: Kirigami.Theme.disabledTextColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
topPadding: Kirigami.Units.largeSpacing * 2
bottomPadding: Kirigami.Units.smallSpacing

View File

@@ -7,97 +7,39 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Control {
id: stateDelegate
// extraWidth defines how the delegate can grow after the listView gets very wide
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
RowLayout {
id: row
width: delegateMaxWidth
// anchors.leftMargin: Kirigami.Units.largeSpacing
// anchors.rightMargin: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignTop
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: stateDelegate
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: stateDelegate
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
name: author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
]
height: sectionDelegate.height + rowLayout.height
SectionDelegate {
id: sectionDelegate
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
visible: model.showSection
height: visible ? implicitHeight : 0
}
RowLayout {
id: rowLayout
height: label.contentHeight
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
anchors.rightMargin: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignTop
name: author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
}
Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.preferredHeight: icon.height
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${currentRoom.htmlSafeMemberName(author.id)}</a> ${aggregateDisplay}`
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
Label {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: "<style>a {text-decoration: none;}</style><a href=\"https://matrix.to/#/" + author.id + "\" style='color: " + author.color + "'>" + author.displayName + "</a> " + display
onLinkActivated: Qt.openUrlExternally(link)
}
}

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.neochat 1.0
import org.kde.kirigami 2.15 as Kirigami
TextEdit {
id: contentLabel
Layout.margins: Kirigami.Units.largeSpacing
Layout.topMargin: 0
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
property bool isEmote: false
property string textMessage: model.display
text: "<style>
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
color: Kirigami.Theme.textColor
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
Layout.fillWidth: true
onLinkActivated: RoomManager.openResource(link)
onHoveredLinkChanged: if (hoveredLink.length > 0) {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
} else {
applicationWindow().hoverLinkIndicator.text = "";
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}

View File

@@ -4,6 +4,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.15 as Kirigami
@@ -12,58 +13,23 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
QQC2.ItemDelegate {
id: timelineContainer
id: messageDelegate
default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed
property bool isLoaded
property bool isEmote: false
property bool cardBackground: true
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
property bool isTemporaryHighlighted: false
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
signal openContextMenu
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
// extraWidth defines this as the excess after a certain ListView width, capped to a max value
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth)
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight &&
model.author.isLocalUser && !Config.compactLayout
readonly property int bubbleMaxWidth: !Config.showAvatarInTimeline ? width : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4, Kirigami.Units.gridUnit * 20)
signal saveFileAs()
signal openExternally()
signal replyClicked(string eventID)
Component.onCompleted: {
if (model.isReply && model.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0)))
}
}
topPadding: 0
bottomPadding: 0
topInset: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing))
background: Rectangle {
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
background: null
property Item hoverComponent
// show hover actions
@@ -76,39 +42,13 @@ QQC2.ItemDelegate {
// updates the global hover component to point to this delegate, and update its position
function updateHoverComponent() {
if (hoverComponent) {
hoverComponent.delegate = timelineContainer
hoverComponent.bubble = bubble
hoverComponent.updateFunction = updateHoverComponent;
hoverComponent.event = model
}
}
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: timelineContainer
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: timelineContainer
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
height: sectionDelegate.height + Math.max(avatar.height, bubble.implicitHeight) + loader.height + (model.showAuthor ? Kirigami.Units.smallSpacing : 0) - (Config.showAvatarInTimeline ? 0 : Kirigami.Units.largeSpacing)
SectionDelegate {
id: sectionDelegate
@@ -121,25 +61,18 @@ QQC2.ItemDelegate {
Kirigami.Avatar {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0
height: width
padding: Kirigami.Units.smallSpacing
topInset: Kirigami.Units.smallSpacing
bottomInset: Kirigami.Units.smallSpacing
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
sourceSize.width: width
sourceSize.height: width
anchors {
top: sectionDelegate.bottom
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
topMargin: model.showAuthor ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.smallSpacing
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
leftMargin: Kirigami.Units.largeSpacing
}
visible: model.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
visible: model.showAuthor && Config.showAvatarInTimeline
name: model.author.name ?? model.author.displayName
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color
@@ -161,67 +94,45 @@ QQC2.ItemDelegate {
QQC2.Control {
id: bubble
topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
topPadding: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
hoverEnabled: true
anchors {
top: avatar.top
left: avatar.right
leftMargin: Kirigami.Units.smallSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
right: Config.showAvatarInTimeline ? undefined : parent.right
rightMargin: Config.showAvatarInTimeline ? undefined : Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout ? timelineContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize
// as setting anchors to undefined did not work reliably
states: [
State {
name: "userMessageOnRight"
AnchorChanges {
target: bubble
anchors.left: undefined
anchors.right: parent.right
}
},
State {
name: "userMessageOnLeft"
AnchorChanges {
target: bubble
anchors.left: avatar.right
anchors.right: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
contentItem: ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
RowLayout {
spacing: 0
Item {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor && !isEmote
Layout.fillWidth: true
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.preferredWidth: nameLabel.implicitWidth + timeLabel.implicitWidth + Kirigami.Units.largeSpacing
Layout.maximumWidth: bubbleMaxWidth
implicitHeight: visible ? nameLabel.implicitHeight : 0
QQC2.Label {
id: nameLabel
topInset: 0
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
visible: model.showAuthor && !isEmote
anchors.left: rowLayout.left
anchors.right: timeLabel.left
anchors.rightMargin: Kirigami.Units.smallSpacing
text: visible ? author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
elide: Text.ElideRight
wrapMode: Text.Wrap
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
@@ -238,16 +149,10 @@ QQC2.ItemDelegate {
}
QQC2.Label {
id: timeLabel
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
anchors.right: rowLayout.right
visible: model.showAuthor && !isEmote
text: visible ? time.toLocaleTimeString(Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
}
}
}
Loader {
@@ -255,6 +160,7 @@ QQC2.ItemDelegate {
active: model.reply !== undefined
source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml'
visible: active
Layout.bottomMargin: Kirigami.Units.smallSpacing
Connections {
target: replyLoader.item
@@ -265,30 +171,14 @@ QQC2.ItemDelegate {
}
}
background: Item {
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
color: {
if (model.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (timelineContainer.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
}
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: timelineContainer.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
Behavior on color {
ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
background: Kirigami.ShadowedRectangle {
visible: cardBackground && Config.showAvatarInTimeline
color: model.isHighlighted ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: Kirigami.Units.devicePixelRatio
}
}
@@ -298,7 +188,7 @@ QQC2.ItemDelegate {
left: bubble.left
right: parent.right
top: bubble.bottom
topMargin: active && Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
topMargin: active && Config.showAvatarInTimeline ? Kirigami.Units.smallSpacing : 0
}
height: active ? item.implicitHeight : 0
//Layout.bottomMargin: readMarker ? Kirigami.Units.smallSpacing : 0
@@ -306,14 +196,4 @@ QQC2.ItemDelegate {
visible: active
sourceComponent: ReactionDelegate { }
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: timelineContainer.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: timelineContainer.openContextMenu()
}
}

View File

@@ -4,6 +4,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import QtMultimedia 5.15
import Qt.labs.platform 1.1 as Platform
@@ -14,19 +15,13 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: videoDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
Video {
id: vid
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
property bool supportStreaming: true
readonly property int maxWidth: 1000 // TODO messageListView.width
onOpenContextMenu: openFileContext(model, vid)
onDownloadedChanged: {
if (downloaded) {
@@ -39,94 +34,102 @@ TimelineContainer {
}
}
innerObject: Video {
id: vid
Layout.maximumWidth: videoDelegate.contentMaxWidth
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
readonly property int maxWidth: 1000 // TODO messageListView.width
Layout.preferredWidth: (model.content.info.w === undefined || model.content.info.w > videoDelegate.maxWidth) ? videoDelegate.maxWidth : content.info.w
Layout.preferredHeight: model.content.info.w === undefined ? (videoDelegate.maxWidth * 3 / 4) : (model.content.info.w > videoDelegate.maxWidth ? (model.content.info.h / model.content.info.w * videoDelegate.maxWidth) : model.content.info.h)
Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w
Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h
loops: MediaPlayer.Infinite
loops: MediaPlayer.Infinite
fillMode: VideoOutput.PreserveAspectFit
fillMode: VideoOutput.PreserveAspectFit
onDurationChanged: {
if (!duration) {
vid.supportStreaming = false;
}
Component.onCompleted: {
if (downloaded) {
source = progressInfo.localPath
} else {
source = currentRoom.urlToMxcUrl(content.url)
}
}
onErrorChanged: {
if (error != MediaPlayer.NoError) {
vid.supportStreaming = false;
}
onDurationChanged: {
if (!duration) {
supportStreaming = false;
}
}
Image {
anchors.fill: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
source: model.content.thumbnailMediaId ? "image://mxc/" + model.content.thumbnailMediaId : ""
fillMode: Image.PreserveAspectFit
onErrorChanged: {
if (error != MediaPlayer.NoError) {
supportStreaming = false;
}
}
Label {
Image {
readonly property bool isThumbnail: content.info.thumbnail_info && content.thumbnailMediaId
readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
anchors.fill: parent
visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError)
source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "")
sourceSize.width: info.w
sourceSize.height: info.h
fillMode: Image.PreserveAspectFit
}
Label {
anchors.centerIn: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white"
text: i18n("Video")
font.pixelSize: 16
padding: 8
background: Rectangle {
radius: Kirigami.Units.smallSpacing
color: "black"
opacity: 0.3
}
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white"
text: i18n("Video")
font.pixelSize: 16
width: parent.width * 0.8
padding: 8
background: Rectangle {
radius: Kirigami.Units.smallSpacing
color: "black"
opacity: 0.3
}
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !videoDelegate.downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (vid.supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
videoDelegate.downloadAndPlay()
vid.play()
}
} else {
downloadAndPlay()
}
}
function downloadAndPlay() {
if (vid.downloaded) {
if (downloaded) {
playSavedFile()
} else {
playOnFinished = true

View File

@@ -1,6 +1,6 @@
module NeoChat.Component.Timeline
RichLabel 1.0 RichLabel.qml
TimelineContainer 1.0 TimelineContainer.qml
TextDelegate 1.0 TextDelegate.qml
StateDelegate 1.0 StateDelegate.qml
SectionDelegate 1.0 SectionDelegate.qml
ImageDelegate 1.0 ImageDelegate.qml
@@ -8,8 +8,3 @@ FileDelegate 1.0 FileDelegate.qml
VideoDelegate 1.0 VideoDelegate.qml
ReactionDelegate 1.0 ReactionDelegate.qml
AudioDelegate 1.0 AudioDelegate.qml
EncryptedDelegate 1.0 EncryptedDelegate.qml
EventDelegate 1.0 EventDelegate.qml
MessageDelegate 1.0 MessageDelegate.qml
ReadMarkerDelegate 1.0 ReadMarkerDelegate.qml
LinkPreviewDelegate 1.0 LinkPreviewDelegate.qml

View File

@@ -90,7 +90,6 @@ Loader {
id: typingLabel
elide: Text.ElideRight
text: root.labelText
textFormat: Text.PlainText
}
}

View File

@@ -3,5 +3,3 @@ FullScreenImage 1.0 FullScreenImage.qml
ChatTextInput 1.0 ChatTextInput.qml
FancyEffectsContainer 1.0 FancyEffectsContainer.qml
TypingPane 1.0 TypingPane.qml
QuickSwitcher 1.0 QuickSwitcher.qml
ShimmerGradient 1.0 ShimmerGradient.qml

View File

@@ -14,7 +14,9 @@ Kirigami.OverlaySheet {
parent: applicationWindow().overlay
title: i18n("Create a Room")
header: Kirigami.Heading {
text: i18n("Create a Room")
}
contentItem: Kirigami.FormLayout {
TextField {

View File

@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: emojiItem
property string emoji
property string description
QQC2.Label {
id: emojiLabel
x: 0
y: 0
width: parent.width
height: parent.height * 0.75
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: emojiItem.emoji
font.family: "emoji"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4
}
QQC2.Label {
x: 0
y: parent.height * 0.75
width: parent.width
height: parent.height * 0.25
text: emojiItem.description
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Row {
id: emojiRow
property alias model: repeater.model
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
id: repeater
delegate: EmojiItem {
emoji: modelData.emoji
description: modelData.description
width: emojiRow.height
height: width
}
}
}

View File

@@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: emojiSas
required property var model
signal accept()
signal reject()
visible: dialog.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Confirm the emoji below are displayed on both devices, in the same order.")
}
EmojiRow {
anchors.horizontalCenter: parent.horizontalCenter
height: Kirigami.Units.gridUnit * 4
model: emojiSas.model.slice(0, 4)
}
EmojiRow {
anchors.horizontalCenter: parent.horizontalCenter
height: Kirigami.Units.gridUnit * 4
model: emojiSas.model.slice(4, 7)
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They match")
icon.name: "dialog-ok"
onClicked: emojiSas.accept()
}
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They don't match")
icon.name: "dialog-cancel"
onClicked: emojiSas.reject()
}
}
}

View File

@@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: dialog
title: i18n("Session Verification")
required property var session
Item {
anchors.fill: parent
VerificationCanceled {
visible: dialog.session.state === KeyVerificationSession.CANCELED
anchors.centerIn: parent
reason: dialog.session.error
}
EmojiSas {
anchors.centerIn: parent
visible: dialog.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
model: dialog.session.sasEmojis
onReject: dialog.session.cancelVerification(KeyVerificationSession.MISMATCHED_SAS)
onAccept: dialog.session.sendMac()
}
Message {
visible: dialog.session.state === KeyVerificationSession.WAITINGFORREADY
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Waiting for device to accept verification.")
}
Message {
visible: dialog.session.state === KeyVerificationSession.INCOMING
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Incoming key verification request from device **%1**", dialog.session.remoteDeviceId)
}
Message {
visible: dialog.session.state === KeyVerificationSession.WAITINGFORMAC
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Waiting for other party to verify.")
}
Kirigami.BasicListItem {
id: emojiVerification
text: "Emoji Verification"
visible: dialog.session.state === KeyVerificationSession.READY
subtitle: i18n("Compare a set of emoji on both devices")
onClicked: {
dialog.session.sendStartSas()
}
}
Message {
visible: dialog.session.state === KeyVerificationSession.DONE
anchors.centerIn: parent
text: i18n("Successfully verified device **%1**", dialog.session.remoteDeviceId)
icon: "security-high"
}
}
footer: QQC2.ToolBar {
visible: dialog.session.state === KeyVerificationSession.INCOMING
QQC2.DialogButtonBox {
anchors.fill: parent
Item { Layout.fillWidth: true }
QQC2.Button {
text: i18n("Accept")
icon.name: "dialog-ok"
onClicked: dialog.session.sendReady()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
QQC2.Button {
text: i18n("Decline")
icon.name: "dialog-cancel"
onClicked: dialog.session.cancelVerification("m.user", "Declined")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.CancelRole
}
}
}
}

View File

@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: message
required property string icon
required property string text
anchors.centerIn: parent
Kirigami.Icon {
width: Kirigami.Units.iconSizes.enormous
height: width
anchors.horizontalCenter: parent.horizontalCenter
source: message.icon
}
QQC2.Label {
text: message.text
textFormat: Text.MarkdownText
}
}

View File

@@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Message {
id: verificationCanceled
required property int reason
anchors.centerIn: parent
icon: "security-low"
text: {
switch(verificationCanceled.reason) {
case KeyVerificationSession.NONE:
return i18n("The session verification was canceled for unknown reason.");
case KeyVerificationSession.TIMEOUT:
return i18n("The session verification timed out.");
case KeyVerificationSession.REMOTE_TIMEOUT:
return i18n("The session verification timed out for remote party.");
case KeyVerificationSession.USER:
return i18n("You canceled the session verification.");
case KeyVerificationSession.REMOTE_USER:
return i18n("The remote party canceled the session verification.");
case KeyVerificationSession.UNEXPECTED_MESSAGE:
return i18n("The session verification was canceled because we received an unexpected message.");
case KeyVerificationSession.REMOTE_UNEXPECTED_MESSAGE:
return i18n("The remote party canceled the session verification because it received an unexpected message.");
case KeyVerificationSession.UNKNOWN_TRANSACTION:
return i18n("The session verification was canceled because it received a message for an unknown session.");
case KeyVerificationSession.REMOTE_UNKNOWN_TRANSACTION:
return i18n("The remote party canceled the session verification because it received a message for an unknown session.");
case KeyVerificationSession.UNKNOWN_METHOD:
return i18n("The session verification was canceled because NeoChat is unable to handle this verification method.");
case KeyVerificationSession.REMOTE_UNKNOWN_METHOD:
return i18n("The remote party canceled the session verification because it is unable to handle this verification method.");
case KeyVerificationSession.KEY_MISMATCH:
return i18n("The session verification was canceled because the keys are incorrect.");
case KeyVerificationSession.REMOTE_KEY_MISMATCH:
return i18n("The remote party canceled the session verification because the keys are incorrect.");
case KeyVerificationSession.USER_MISMATCH:
return i18n("The session verification was canceled because it verifies an unexpected user.");
case KeyVerificationSession.REMOTE_USER_MISMATCH:
return i18n("The remote party canceled the session verification because it verifies an unexpected user.");
case KeyVerificationSession.INVALID_MESSAGE:
return i18n("The session verification was canceled because we received an invalid message.");
case KeyVerificationSession.REMOTE_INVALID_MESSAGE:
return i18n("The remote party canceled the session verification because it received an invalid message.");
case KeyVerificationSession.SESSION_ACCEPTED:
return i18n("The session was accepted on a different device"); //TODO this should not be visible
case KeyVerificationSession.REMOTE_SESSION_ACCEPTED:
return i18n("The session was accepted on a different device"); //TODO neither should this
case KeyVerificationSession.MISMATCHED_COMMITMENT:
return i18n("The session verification was canceled because of a mismatched key.");
case KeyVerificationSession.REMOTE_MISMATCHED_COMMITMENT:
return i18n("The remote party canceled the session verification because of a mismatched key.");
case KeyVerificationSession.MISMATCHED_SAS:
return i18n("The session verification was canceled because the keys do not match.");
case KeyVerificationSession.REMOTE_MISMATCHED_SAS:
return i18n("The remote party canceled the session verification because the keys do not match.");
default:
return i18n("The session verification was canceled due to an unknown error.");
}
}
}

View File

@@ -1,7 +0,0 @@
module NeoChat.Dialog.KeyVerification
KeyVerificationDialog 1.0 KeyVerificationDialog.qml
Message 1.0 Message.qml
VerificationCanceled 1.0 VerificationCanceled.qml
EmojiItem 1.0 EmojiItem.qml
EmojiRow 1.0 EmojiRow.qml
EmojiSas 1.0 EmojiSas.qml

View File

@@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.OverlaySheet {
id: root
property var room
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
readonly property bool canChangeName: room.canSendState("m.room.name")
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
parent: applicationWindow().overlay
header: Kirigami.Heading {
text: i18nc("%1 is the room name", "Room Settings - %1", room.displayName)
elide: Text.ElideRight
}
contentItem: ColumnLayout {
RowLayout {
Layout.fillWidth: true
spacing: 16
Kirigami.Avatar {
Layout.preferredWidth: 72
Layout.preferredHeight: 72
Layout.alignment: Qt.AlignTop
name: room.name
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
MouseArea {
anchors.fill: parent
enabled: canChangeAvatar
onClicked: {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
room.changeAvatar(path)
})
fileDialog.open()
}
}
}
Kirigami.FormLayout {
Layout.fillWidth: true
TextField {
id: roomNameField
text: room.name
Kirigami.FormData.label: i18n("Room Name:")
enabled: canChangeName
}
TextArea {
id: roomTopicField
Layout.fillWidth: true
text: room.topic
Kirigami.FormData.label: i18n("Room topic:")
enabled: canChangeTopic
}
Button {
Layout.alignment: Qt.AlignRight
visible: canChangeName || canChangeTopic
text: i18n("Save")
highlighted: true
onClicked: {
if (room.name != roomNameField.text) {
room.setName(roomNameField.text)
}
if (room.topic != roomTopicField.text) {
room.setTopic(roomTopicField.text)
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: canonicalAliasComboBox.visible || altAlias.visible
}
ComboBox {
id: canonicalAliasComboBox
visible: room.aliases && room.aliases.length
Kirigami.FormData.label: i18n("Canonical Alias:")
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
enabled: canChangeCanonicalAlias
model: room.aliases
currentIndex: room.aliases.indexOf(room.canonicalAlias)
onCurrentIndexChanged: {
if (room.canonicalAlias != room.aliases[currentIndex]) {
room.setCanonicalAlias(room.aliases[currentIndex])
}
}
}
RowLayout {
id: altAlias
Kirigami.FormData.label: i18n("Alt Aliases")
Layout.fillWidth: true
visible: room.altAliases && room.altAliases.length
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: room.altAliases
delegate: RowLayout {
Layout.maximumWidth: parent.width
Label {
text: modelData
color: Kirigami.Theme.disabledColor
}
ToolButton {
icon.name: ""
onClicked: room.removeLocalAlias(modelData)
}
}
}
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: next.visible || prev.visible
}
Control {
id: next
Layout.fillWidth: true
visible: room.predecessorId && room.connection.room(room.predecessorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room continues another conversation.")
actions: Kirigami.Action {
text: i18n("See older messages...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
root.close()
}
}
}
}
Control {
id: prev
Layout.fillWidth: true
visible: room.successorId && room.connection.room(room.successorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room has been replaced.")
actions: Kirigami.Action {
text: i18n("See new room...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
root.close()
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
}
}

View File

@@ -14,8 +14,6 @@ import NeoChat.Component 1.0
Kirigami.OverlaySheet {
id: root
signal closed()
property var room
property var user
@@ -29,7 +27,13 @@ Kirigami.OverlaySheet {
rightPadding: 0
topPadding: 0
title: i18nc("@title:menu Account detail dialog", "Account detail")
header: Kirigami.Heading {
id: heading
text: i18nc("@title:menu Account detail dialog", "Account detail")
elide: Text.ElideRight
QQC2.ToolTip.visible: truncated && hovered
QQC2.ToolTip.text: text
}
contentItem: ColumnLayout {
spacing: 0
@@ -54,7 +58,7 @@ Kirigami.OverlaySheet {
onClicked: {
if (avatarMediaId) {
fullScreenImage.createObject(parent, {filename: displayName, source: room.urlToMxcUrl(avatarUrl)}).showFullScreen()
fullScreenImage.createObject(parent, {"filename": displayName, "localPath": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
}
}
}
@@ -70,7 +74,7 @@ Kirigami.OverlaySheet {
elide: Text.ElideRight
wrapMode: Text.NoWrap
text: room.htmlSafeMemberName(user.id)
text: displayName
}
QQC2.Label {
@@ -125,32 +129,6 @@ Kirigami.OverlaySheet {
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser && room.canSendState("ban") && room.isUserBanned(user.id)
action: Kirigami.Action {
text: i18n("Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
room.unban(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
visible: user === room.localUser || room.canSendState("redact")
action: Kirigami.Action {
text: i18n("Delete recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
room.deleteMessagesByUser(user.id)
}
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser
action: Kirigami.Action {
text: i18n("Open a private chat")
icon.name: "document-send"
@@ -166,11 +144,5 @@ Kirigami.OverlaySheet {
FullScreenImage {}
}
}
onSheetOpenChanged: {
if (!sheetOpen) {
closed()
}
}
}

View File

@@ -1,4 +1,5 @@
module NeoChat.Dialog
RoomSettingsDialog 1.0 RoomSettingsDialog.qml
UserDetailDialog 1.0 UserDetailDialog.qml
LoginDialog 1.0 LoginDialog.qml
CreateRoomDialog 1.0 CreateRoomDialog.qml
@@ -7,4 +8,3 @@ OpenFileDialog 1.0 OpenFileDialog.qml
ImageClipboardDialog 1.0 ImageClipboardDialog.qml
StartChatDialog 1.0 StartChatDialog.qml
EmojiDialog 1.0 EmojiDialog.qml
KeyVerificationDialog 1.0 KeyVerificationDialog.qml

View File

@@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import Qt.labs.platform 1.1 as Labs
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.10
import org.kde.kirigami 2.15 as Kirigami
Labs.Menu {
id: editMenu
required property Item field
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.canUndo
text: i18nc("text editing menu action", "Undo")
shortcut: StandardKey.Undo
onTriggered: {
editMenu.field.undo()
editMenu.close()
}
}
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.canRedo
text: i18nc("text editing menu action", "Redo")
shortcut: StandardKey.Redo
onTriggered: {
editMenu.field.undo()
editMenu.close()
}
}
Labs.MenuSeparator {
}
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.selectedText
text: i18nc("text editing menu action", "Cut")
shortcut: StandardKey.Cut
onTriggered: {
editMenu.field.cut()
editMenu.close()
}
}
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.selectedText
text: i18nc("text editing menu action", "Copy")
shortcut: StandardKey.Copy
onTriggered: {
editMenu.field.copy()
editMenu.close()
}
}
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.canPaste
text: i18nc("text editing menu action", "Paste")
shortcut: StandardKey.Paste
onTriggered: {
editMenu.field.paste()
editMenu.close()
}
}
Labs.MenuItem {
enabled: editMenu.field !== null && editMenu.field.selectedText !== ""
text: i18nc("text editing menu action", "Delete")
shortcut: ""
onTriggered: {
editMenu.field.remove(editMenu.field.selectionStart, editMenu.field.selectionEnd)
editMenu.close()
}
}
Labs.MenuSeparator {
}
Labs.MenuItem {
enabled: editMenu.field !== null
text: i18nc("text editing menu action", "Select All")
shortcut: StandardKey.SelectAll
onTriggered: {
editMenu.field.selectAll()
editMenu.close()
}
}
}

View File

@@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Labs
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.10
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Page 1.0
import NeoChat.Panel 1.0
Labs.MenuBar {
Labs.Menu {
title: i18nc("menu", "NeoChat")
// TODO: make about page its own thing so we can go to it instead of settings where it's currently at
// Labs.MenuItem {
// text: i18nc("menu", "About NeoChat")
// }
Labs.MenuItem {
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat...")
text: i18nc("menu", "Configure NeoChat...")
shortcut: StandardKey.Preferences
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml", {}, {
title: i18n("Configure")
})
}
Labs.MenuItem {
text: i18nc("menu", "Quit NeoChat")
shortcut: StandardKey.Quit
onTriggered: Qt.quit()
}
}
Labs.Menu {
title: i18nc("menu", "File")
Labs.MenuItem {
text: i18nc("menu", "New Private Chat…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {connection: Controller.activeConnection})
}
Labs.MenuItem {
text: i18nc("menu", "New Group…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
shortcut: StandardKey.New
onTriggered: {
const dialog = createRoomDialog.createObject(root.overlay)
dialog.open()
}
}
Labs.MenuItem {
text: i18nc("menu", "Browse Chats…")
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {connection: Controller.activeConnection})
}
}
EditMenu {
title: i18nc("menu", "Edit")
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
}
Labs.Menu {
title: i18nc("menu", "View")
Labs.MenuItem {
text: i18nc("menu item that opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Open Quick Switcher")
shortcut: "Ctrl+K"
onTriggered: quickView.item.open()
}
}
Labs.Menu {
title: i18nc("menu", "Window")
// Labs.MenuItem {
// text: settings.userWantsSidebars ? i18nc("menu", "Hide Sidebar") : i18nc("menu", "Show Sidebar")
// onTriggered: settings.userWantsSidebars = !settings.userWantsSidebars
// }
Labs.MenuItem {
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
}
}
// TODO: offline help system (https://invent.kde.org/network/neochat/-/issues/411)
Labs.Menu {
title: i18nc("menu", "Help")
Labs.MenuItem {
text: i18nc("menu", "Matrix FAQ")
onTriggered: UrlHelper.openUrl("https://matrix.org/faq/")
}
}
}

View File

@@ -4,9 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Page 1.0
@@ -14,197 +11,41 @@ import NeoChat.Page 1.0
/**
* Context menu when clicking on a room in the room list
*/
Loader {
Menu {
id: root
property var room
signal closed()
Component {
id: regularMenu
Menu {
MenuItem {
id: newWindow
text: i18n("Open in New Window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
MenuSeparator {
visible: newWindow.visible
}
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
MenuItem {
text: i18n("Mark as Read")
onTriggered: room.markAllMessagesAsRead()
}
MenuItem {
text: i18nc("@action:inmenu", "Copy Address to Clipboard")
onTriggered: if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
Clipboard.saveText(room.canonicalAlias)
}
}
Menu {
title: i18n("Notification State")
MenuItem {
text: i18n("Follow Global Setting")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Default
}
}
MenuItem {
text: i18nc("As in 'notify for all messages'","All")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.All
}
}
MenuItem {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'","@Mentions and Keywords")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
MenuItem {
text: i18nc("As in 'do not notify for any messages'","Off")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Mute
}
}
}
MenuItem {
text: i18n("Room Settings")
onTriggered: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}
MenuSeparator {}
MenuItem {
text: i18n("Leave Room")
onTriggered: RoomManager.leaveRoom(room)
}
onClosed: {
root.closed()
destroy()
}
}
MenuItem {
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
}
Component {
id: mobileMenu
MenuSeparator {}
Kirigami.OverlayDrawer {
id: drawer
height: popupContent.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
parent: applicationWindow().overlay
ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
Kirigami.Heading {
level: 5
Layout.fillWidth: true
text: room.displayName
wrapMode: Text.WordWrap
}
ToolButton {
checked: room.isFavourite
checkable: true
icon.name: 'favorite'
Accessible.name: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
ToolButton {
icon.name: 'settings-configure'
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}
}
Kirigami.BasicListItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
onClicked: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
Kirigami.BasicListItem {
text: i18n("Mark as Read")
onClicked: room.markAllMessagesAsRead()
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
Kirigami.BasicListItem {
text: i18n("Leave Room")
onClicked: RoomManager.leaveRoom(room)
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
onClosed: root.closed()
}
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
asynchronous: true
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
function open() {
active = true;
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
onStatusChanged: if (status == Loader.Ready) {
if (Kirigami.Settings.isMobile) {
item.open();
} else {
item.popup();
}
MenuItem {
text: i18n("Mark as Read")
onTriggered: room.markAllMessagesAsRead()
}
MenuSeparator {}
MenuItem {
text: i18n("Leave Room")
onTriggered: RoomManager.leaveRoom(room)
}
onClosed: destroy()
}

View File

@@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as Controls
import org.kde.kirigami 2.14 as Kirigami
/**
* Action that allows an user to share data with other apps and service
* installed on their computer. The goal of this high level API is to
* adapte itself for each platform and adopt the native component.
*
* TODO add Android support
*/
Kirigami.Action {
id: shareAction
iconName: "emblem-shared-symbolic"
text: i18n("Share")
tooltip: i18n("Share the selected media")
property var doBeforeSharing: () => {}
visible: false
/**
* This property holds the input data for purpose.
*
* @code{.qml}
* Purpose.ShareAction {
* inputData: {
* 'urls': ['file://home/notroot/Pictures/mypicture.png'],
* 'mimeType': ['image/png']
* }
* }
* @endcode
*/
property var inputData: ({})
property Instantiator _instantiator: Instantiator {
Component.onCompleted: {
const purposeModel = Qt.createQmlObject('import org.kde.purpose 1.0 as Purpose;
Purpose.PurposeAlternativesModel {
pluginType: "Export"
}', shareAction._instantiator);
purposeModel.inputData = Qt.binding(function() {
return shareAction.inputData;
});
_instantiator.model = purposeModel;
shareAction.visible = true;
}
delegate: Kirigami.Action {
property int index
text: model.display
icon.name: model.iconName
onTriggered: {
doBeforeSharing();
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/ShareDialog.qml', {
title: shareAction.tooltip,
index: index,
model: shareAction._instantiator.model
})
}
}
onObjectAdded: {
object.index = index;
shareAction.children.push(object)
}
onObjectRemoved: shareAction.children = Array.from(shareAction.children).filter(obj => obj.pluginId !== object.pluginId)
}
}

View File

@@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import org.kde.kirigami 2.14 as Kirigami
Kirigami.Action {
property var inputData: ({})
property var doBeforeSharing: () => {}
visible: false
}

View File

@@ -1,69 +0,0 @@
/*
* SPDX-FileCopyrightText: 2017 Atul Sharma <atulsharma406@gmail.com>
* SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as Controls
import org.kde.purpose 1.0 as Purpose
import org.kde.notification 1.0
import org.kde.kirigami 2.14 as Kirigami
Kirigami.Page {
id: window
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
property alias index: jobView.index
property alias model: jobView.model
Controls.Action {
shortcut: 'Escape'
onTriggered: window.closeDialog()
}
Notification {
id: sharingFailed
eventId: "sharingFailed"
text: i18n("Sharing failed")
urgency: Notification.NormalUrgency
}
Notification {
id: sharingSuccess
eventId: "sharingSuccess"
flags: Notification.Persistent
}
Component.onCompleted: jobView.start()
contentItem: Purpose.JobView {
id: jobView
onStateChanged: {
if (state === Purpose.PurposeJobController.Finished) {
if (jobView.job.output.url !== "") {
// Show share url
// TODO no needed anymore in purpose > 5.90
sharingSuccess.text = i18n("Shared url for image is <a href='%1'>%1</a>", jobView.output.url);
sharingSuccess.sendEvent();
Clipboard.saveText(jobView.output.url);
}
window.closeDialog()
} else if (state === Purpose.PurposeJobController.Error) {
// Show failure notification
sharingFailed.sendEvent();
window.closeDialog()
} else if (state === Purpose.PurposeJobController.Cancelled) {
// Do nothing
window.closeDialog()
}
}
}
}

View File

@@ -14,11 +14,8 @@ import NeoChat.Menu 1.0
MessageDelegateContextMenu {
id: root
signal closeFullscreen
required property var file
required property var progressInfo
required property string mimeType
property list<Kirigami.Action> actions: [
Kirigami.Action {
@@ -26,13 +23,13 @@ MessageDelegateContextMenu {
icon.name: "document-open"
onTriggered: {
if (file.downloaded) {
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
}
} else {
file.onDownloadedChanged.connect(function() {
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
}
});
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
@@ -52,8 +49,7 @@ MessageDelegateContextMenu {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
chatBoxHelper.replyToMessage(eventId, message, author);
root.closeFullscreen()
ChatBoxHelper.replyToMessage(eventId, message, author);
}
},
Kirigami.Action {
@@ -63,50 +59,13 @@ MessageDelegateContextMenu {
icon.color: "red"
onTriggered: {
currentRoom.redactEvent(eventId);
root.closeFullscreen()
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: author.id !== currentRoom.localUser.id
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/imports/NeoChat/Menu/Timeline/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: root.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
root.closeFullscreen()
}
}
]
property list<Kirigami.Action> nestedActions: [
ShareAction {
id: shareAction
inputData: {
'urls': [],
'mimeType': [mimeType]
}
property string filename: StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId);
doBeforeSharing: () => {
currentRoom.downloadFile(eventId, filename)
}
Component.onCompleted: {
shareAction.inputData = {
urls: [filename],
mimeType: [mimeType]
};
messageSourceSheet.createObject(root, {'sourceText': root.source}).open();
}
}
]

View File

@@ -19,22 +19,18 @@ Loader {
property string eventType: ""
property string formattedBody: ""
required property string source
property string selectedText: ""
required property string plainMessage
property list<Kirigami.Action> nestedActions
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Edit")
icon.name: "document-edit"
onTriggered: chatBoxHelper.edit(message, formattedBody, eventId);
onTriggered: ChatBoxHelper.edit(message, formattedBody, eventId);
visible: eventType.length > 0 && author.id === Controller.activeConnection.localUserId && (eventType === "emote" || eventType === "message")
},
Kirigami.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: chatBoxHelper.replyToMessage(eventId, message, author);
onTriggered: ChatBoxHelper.replyToMessage(eventId, message, author);
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
@@ -46,27 +42,13 @@ Loader {
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.plainMessage : loadRoot.selectedText)
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: author.id !== currentRoom.localUser.id
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/imports/NeoChat/Menu/Timeline/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
onTriggered: Clipboard.saveText(message)
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: loadRoot.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
messageSourceSheet.createObject(page, {'sourceText': loadRoot.source}).open();
}
}
]
@@ -75,77 +57,104 @@ Loader {
id: regularMenu
QQC2.Menu {
id: menu
Instantiator {
model: loadRoot.nestedActions
delegate: QQC2.Menu {
id: menuItem
visible: modelData.visible
title: modelData.text
Instantiator {
model: modelData.children
delegate: QQC2.MenuItem {
text: modelData.text
icon.name: modelData.icon.name
onTriggered: modelData.trigger()
}
onObjectAdded: {
menuItem.insertItem(0, object)
}
}
}
onObjectAdded: {
object.visible = false;
menu.addMenu(object)
}
}
Repeater {
model: loadRoot.actions
QQC2.MenuItem {
id: menuItem
visible: modelData.visible
action: modelData
onClicked: loadRoot.item.close();
}
}
QQC2.Menu {
id: webshortcutmenu
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
property bool isVisible: webshortcutmodel.enabled
Component.onCompleted: {
webshortcutmenu.parent.visible = isVisible
}
onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible
Instantiator {
model: WebShortcutModel {
id: webshortcutmodel
selectedText: loadRoot.selectedText ? loadRoot.selectedText : loadRoot.plainMessage
onOpenUrl: RoomManager.visitNonMatrix(url)
}
/*
Kirigami.OverlaySheet {
id: root
parent: applicationWindow().overlay
leftPadding: 0
rightPadding: 0
header: Kirigami.Heading {
text: i18nc("@title:menu Message detail dialog", "Message detail")
}
contentItem: ColumnLayout {
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
name: author.displayName
color: author.color
}
delegate: QQC2.MenuItem {
text: model.display
icon.name: model.decoration
onTriggered: webshortcutmodel.trigger(model.edit)
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.displayName
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
wrapMode: Text.WordWrap
onLinkActivated: RoomManager.openResource(link);
}
}
onObjectAdded: webshortcutmenu.insertItem(0, object)
}
QQC2.MenuSeparator {}
QQC2.MenuItem {
text: i18n("Configure Web Shortcuts...")
icon.name: "configure"
onTriggered: webshortcutmodel.configureWebShortcuts()
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: QQC2.Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 16
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData)
loadRoot.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
}
}*/
}
Component {
id: mobileMenu
Kirigami.OverlayDrawer {
id: drawer
height: stackView.implicitHeight
height: popupContent.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
@@ -155,146 +164,85 @@ Loader {
parent: applicationWindow().overlay
QQC2.StackView {
id: stackView
ColumnLayout {
id: popupContent
width: parent.width
implicitHeight: currentItem.implicitHeight
Component {
id: nestedActionsComponent
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
id: actionLayout
property string title: ""
property list<Kirigami.Action> actions
width: parent.width
spacing: 0
RowLayout {
QQC2.ToolButton {
icon.name: 'draw-arrow-back'
onClicked: stackView.pop()
}
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: actionLayout.title
wrapMode: Text.WordWrap
}
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.displayName
wrapMode: Text.WordWrap
}
Repeater {
id: listViewAction
model: actionLayout.actions
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
onLinkActivated: RoomManager.openResource(link);
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
loadRoot.item.close();
}
}
}
}
initialItem: ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: currentRoom.htmlSafeMemberName(author.id)
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
onLinkActivated: RoomManager.openResource(link);
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
loadRoot.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: loadRoot.actions
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
Repeater {
model: loadRoot.nestedActions
Kirigami.BasicListItem {
action: modelData
visible: modelData.visible
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
onClicked: {
stackView.push(nestedActionsComponent, {
title: modelData.text,
actions: modelData.children
});
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: loadRoot.actions
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
}

View File

@@ -5,45 +5,20 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import org.kde.syntaxhighlighting 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
Kirigami.OverlaySheet {
property string sourceText
topPadding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
header: Kirigami.Heading {
text: i18n("Message Source")
}
title: i18n("Message Source")
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
TextArea {
id: sourceTextArea
text: sourceText
readOnly: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
}
SyntaxHighlighter {
textEdit: sourceTextArea
definition: "JSON"
repository: Repository
}
}
TextArea {
text: sourceText
readOnly: true
wrapMode: Text.WordWrap
}
}

View File

@@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
Kirigami.Page {
id: reportSheet
property var room
property string eventId
title: i18n("Report Message")
QQC2.TextArea {
id: reason
placeholderText: i18n("Reason for reporting this message")
anchors.fill: parent
wrapMode: TextEdit.Wrap
}
footer: QQC2.ToolBar {
QQC2.DialogButtonBox {
anchors.fill: parent
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
reportSheet.room.reportEvent(eventId, reason.text)
reportSheet.closeDialog()
}
}
QQC2.Button {
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: reportSheet.closeDialog()
}
}
}
}

View File

@@ -2,4 +2,3 @@ module NeoChat.Menu.Timeline
MessageDelegateContextMenu 1.0 MessageDelegateContextMenu.qml
FileDelegateContextMenu 1.0 FileDelegateContextMenu.qml
MessageSourceSheet 1.0 MessageSourceSheet.qml
ReportSheet 1.0 ReportSheet.qml

View File

@@ -1,6 +1,2 @@
module NeoChat.Menu
RoomListContextMenu 1.0 RoomListContextMenu.qml
GlobalMenu 1.0 GlobalMenu.qml
EditMenu 1.0 EditMenu.qml
ShareAction 1.0 ShareAction.qml
ShareDialog 1.0 ShareDialog.qml

View File

@@ -0,0 +1,196 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
title: i18n("Accounts")
ListView {
model: AccountListModel { }
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
action: Kirigami.Action {
onTriggered: Controller.activeConnection = model.connection
}
Kirigami.BasicListItem {
anchors.top: parent.top
anchors.bottom: parent.bottom
text: model.user.displayName
subtitle: model.user.id
icon: model.user.avatarMediaId ? ("image://mxc/" + model.user.avatarMediaId) : "im-user"
onClicked: {
Controller.activeConnection = model.connection
pageStack.layers.pop()
}
}
actions: [
Kirigami.Action {
text: i18n("Edit this account")
iconName: "document-edit"
onTriggered: {
userEditSheet.connection = model.connection
userEditSheet.open()
}
},
Kirigami.Action {
text: i18n("Logout")
iconName: "im-kick-user"
onTriggered: {
Controller.logout(model.connection, true)
if(Controller.accountCount === 1)
pageStack.layers.pop()
}
}
]
}
}
Connections {
target: Controller
function onConnectionAdded() {
if (pageStack.layers.depth > 2)
pageStack.layers.pop()
}
function onPasswordStatus(status) {
if(status == Controller.Success)
showPassiveNotification(i18n("Password changed successfully"))
else if(status == Controller.Wrong)
showPassiveNotification(i18n("Wrong password entered"))
else
showPassiveNotification(i18n("Unknown problem while trying to change password"))
}
}
actions.main: Kirigami.Action {
text: i18n("Add an account")
iconName: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
}
Component {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
}
}
Kirigami.OverlaySheet {
id: userEditSheet
property var connection
header: Kirigami.Heading {
text: i18n("Edit Account")
}
Kirigami.FormLayout {
anchors.top: passwordsMessage.bottom
RowLayout {
Kirigami.Avatar {
id: avatar
source: userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
MouseArea {
id: mouseArea
anchors.fill: parent
property var fileDialog: null;
onClicked: {
if (fileDialog != null) {
return;
}
fileDialog = openFileDialog.createObject(Controls.ApplicationWindow.Overlay)
fileDialog.chosen.connect(function(receivedSource) {
mouseArea.fileDialog = null;
if (!receivedSource) {
return;
}
parent.source = receivedSource;
});
fileDialog.onRejected.connect(function() {
mouseArea.fileDialog = null;
});
fileDialog.open();
}
}
}
Controls.Button {
visible: avatar.source.length !== 0
icon.name: "edit-clear"
onClicked: avatar.source = ""
}
Kirigami.FormData.label: i18n("Avatar:")
}
Controls.TextField {
id: name
text: userEditSheet.connection.localUser.displayName
Kirigami.FormData.label: i18n("Name:")
}
Controls.TextField {
id: currentPassword
Kirigami.FormData.label: i18n("Current Password:")
echoMode: TextInput.Password
}
Controls.TextField {
id: newPassword
Kirigami.FormData.label: i18n("New Password:")
echoMode: TextInput.Password
}
Controls.TextField {
id: confirmPassword
Kirigami.FormData.label: i18n("Confirm new Password:")
echoMode: TextInput.Password
}
RowLayout {
Controls.Button {
text: i18n("Save")
onClicked: {
if(!Controller.setAvatar(userEditSheet.connection, avatar.source))
showPassiveNotification("The Avatar could not be set")
if(userEditSheet.connection.localUser.displayName !== name.text)
userEditSheet.connection.localUser.rename(name.text)
if(currentPassword.text !== "" && newPassword.text !== "" && confirmPassword.text !== "") {
if(newPassword.text === confirmPassword.text) {
Controller.changePassword(userEditSheet.connection, currentPassword.text, newPassword.text)
} else {
showPassiveNotification(i18n("Passwords do not match"))
return
}
}
userEditSheet.close()
currentPassword.text = ""
newPassword.text = ""
confirmPassword.text = ""
}
}
Controls.Button {
text: i18n("Cancel")
onClicked: {
userEditSheet.close()
avatar.source = userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
currentPassword.text = ""
newPassword.text = ""
confirmPassword.text = ""
}
}
}
}
}
}

View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18n("Devices")
ListView {
model: DevicesModel {
id: devices
}
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
Kirigami.BasicListItem {
anchors.top: parent.top
anchors.bottom: parent.bottom
text: model.displayName
subtitle: model.id
icon: "network-connect"
}
actions: [
Kirigami.Action {
text: i18n("Edit device name")
iconName: "document-edit"
onTriggered: {
renameSheet.index = model.index
renameSheet.name = model.displayName
renameSheet.open()
}
},
Kirigami.Action {
text: i18n("Logout device")
iconName: "edit-delete-remove"
onTriggered: {
passwordSheet.index = index
passwordSheet.open()
}
}
]
}
}
Kirigami.OverlaySheet {
id: passwordSheet
property var index
header: Kirigami.Heading {
text: i18n("Remove device")
}
Kirigami.FormLayout {
Controls.TextField {
id: passwordField
Kirigami.FormData.label: i18n("Password:")
echoMode: TextInput.Password
}
Controls.Button {
text: i18n("Confirm")
onClicked: {
devices.logout(passwordSheet.index, passwordField.text)
passwordField.text = ""
passwordSheet.close()
}
}
}
}
Kirigami.OverlaySheet {
id: renameSheet
property int index
property string name
header: Kirigami.Heading {
text: i18n("Edit device")
}
Kirigami.FormLayout {
Controls.TextField {
id: nameField
Kirigami.FormData.label: i18n("Name:")
text: renameSheet.name
}
Controls.Button {
text: i18n("Save")
onClicked: {
devices.setName(renameSheet.index, nameField.text)
renameSheet.close()
}
}
}
}
}

View File

@@ -4,6 +4,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.15 as Kirigami
@@ -20,14 +21,14 @@ Kirigami.Page {
title: i18n("Edit")
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
function crop() {
const ratioX = editImage.paintedWidth / editImage.nativeWidth;
const ratioY = editImage.paintedHeight / editImage.nativeHeight;
rootEditorView.resizing = false
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
imageDoc.crop(resizeRectangle.insideX / ratioX, resizeRectangle.insideY / ratioY, resizeRectangle.insideWidth / ratioX, resizeRectangle.insideHeight / ratioY);
}
actions {
@@ -57,11 +58,8 @@ Kirigami.Page {
KQuickImageEditor.ImageItem {
contentItem: KQuickImageEditor.ImageItem {
id: editImage
// Assigning this to the contentItem and setting the padding causes weird positioning issues
anchors.fill: parent
anchors.margins: Kirigami.Units.gridUnit
fillMode: KQuickImageEditor.ImageItem.PreserveAspectFit
image: imageDoc.image
@@ -78,41 +76,12 @@ Kirigami.Page {
Shortcut {
sequence: StandardKey.SaveAs
onActivated: saveAsAction.trigger();
}
} anchors.fill: parent
KQuickImageEditor.ImageDocument {
id: imageDoc
path: rootEditorView.imagePath
}
KQuickImageEditor.SelectionTool {
id: selectionTool
visible: rootEditorView.resizing
width: editImage.paintedWidth
height: editImage.paintedHeight
x: editImage.horizontalPadding
y: editImage.verticalPadding
KQuickImageEditor.CropBackground {
anchors.fill: parent
z: -1
insideX: selectionTool.selectionX
insideY: selectionTool.selectionY
insideWidth: selectionTool.selectionWidth
insideHeight: selectionTool.selectionHeight
}
Connections {
target: selectionTool.selectionArea
function onDoubleClicked() {
rootEditorView.crop()
}
}
}
onImageChanged: {
selectionTool.selectionX = 0
selectionTool.selectionY = 0
selectionTool.selectionWidth = Qt.binding(() => selectionTool.width)
selectionTool.selectionHeight = Qt.binding(() => selectionTool.height)
}
}
header: QQC2.ToolBar {
@@ -176,4 +145,22 @@ Kirigami.Page {
showCloseButton: true
visible: false
}
KQuickImageEditor.ResizeRectangle {
id: resizeRectangle
visible: rootEditorView.resizing
width: editImage.paintedWidth
height: editImage.paintedHeight
x: editImage.horizontalPadding
y: editImage.verticalPadding
insideX: 100
insideY: 100
insideWidth: 100
insideHeight: 100
onAcceptSize: rootEditorView.crop();
}
}

View File

@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.12 as Kirigami
Kirigami.Page {
Kirigami.LoadingPlaceholder {
id: loadingIndicator
title: i18n("Loading")
QQC2.BusyIndicator {
anchors.centerIn: parent
}
}

View File

@@ -15,101 +15,9 @@ import NeoChat.Component 1.0
import NeoChat.Menu 1.0
Kirigami.ScrollablePage {
header: ColumnLayout {
visible: !page.collapsedMode
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
ListView {
id: spaceList
property string activeSpaceId: ''
orientation: Qt.Horizontal
spacing: Kirigami.Units.largeSpacing
clip:true
visible: spaceList.count > 0
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.fillWidth: true
model: SortFilterSpaceListModel {
id: sortFilterSpaceListModel
sourceModel: RoomListModel {
id: spaceListModel
connection: Controller.activeConnection
}
}
header: QQC2.Control {
contentItem: QQC2.RoundButton {
id: homeButton
flat: true
padding: Kirigami.Units.gridUnit / 2
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
display: QQC2.AbstractButton.IconOnly
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip {
text: homeButton.text
}
}
}
delegate: QQC2.Control {
required property string avatar
required property var currentRoom
required property int index
required property string id
implicitWidth: ListView.view.headerItem.implicitWidth
implicitHeight: ListView.view.headerItem.implicitHeight
contentItem: Kirigami.Avatar {
actions.main: Kirigami.Action {
onTriggered: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceId = id;
}
}
QQC2.ToolTip {
text: currentRoom.displayName
}
source: avatar !== "" ? "image://mxc/" + avatar : ""
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
id: page
title: i18n("Rooms")
property var enteredRoom
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
verticalScrollBarPolicy: collapsedMode ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
onCollapsedModeChanged: if (collapsedMode) {
sortFilterRoomListModel.filterText = "";
}
Connections {
target: RoomManager
function onCurrentRoomChanged() {
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
}
}
function goToNextRoom() {
do {
@@ -125,57 +33,21 @@ Kirigami.ScrollablePage {
listView.currentItem.action.trigger();
}
titleDelegate: collapsedMode ? empty : searchField
title: i18n("Rooms")
Component {
id: empty
Item {}
}
Component {
id: searchField
Kirigami.SearchField {
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.fillHeight: true
Layout.fillWidth: true
onTextChanged: sortFilterRoomListModel.filterText = text
KeyNavigation.tab: listView
}
titleDelegate: Kirigami.SearchField {
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.fillHeight: true
Layout.fillWidth: true
onTextChanged: sortFilterRoomListModel.filterText = text
KeyNavigation.tab: listView
}
ListView {
id: listView
activeFocusOnTab: true
clip: accountList.count > 1
header: QQC2.ItemDelegate {
visible: page.collapsedMode
action: Kirigami.Action {
id: enterRoomAction
onTriggered: quickView.item.open();
}
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: visible ? page.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
Kirigami.Icon {
anchors.centerIn: parent
width: 22
height: 22
source: "search"
}
Kirigami.Separator {
width: parent.width
anchors.bottom: parent.bottom
}
}
Layout.fillWidth: true
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
@@ -192,7 +64,6 @@ Kirigami.ScrollablePage {
}
}
ItemSelectionModel {
id: itemSelection
model: roomListModel
@@ -216,7 +87,6 @@ Kirigami.ScrollablePage {
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
section.delegate: Kirigami.ListSectionHeader {
id: sectionHeader
height: implicitHeight
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
@@ -227,14 +97,11 @@ Kirigami.ScrollablePage {
level: 3
text: roomListModel.categoryName(section)
Layout.fillWidth: true
elide: Text.ElideRight
visible: !page.collapsedMode
}
Kirigami.Icon {
source: page.collapsedMode ? roomListModel.categoryIconName(section) : (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignHCenter
}
}
}
@@ -242,134 +109,87 @@ Kirigami.ScrollablePage {
reuseItems: true
currentIndex: -1 // we don't want any room highlighted by default
delegate: page.collapsedMode ? collapsedModeListComponent : normalModeListComponent
delegate: Kirigami.BasicListItem {
id: roomListItem
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
highlighted: listView.currentIndex === index
focus: true
icon: undefined
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
itemSelection.setCurrentIndex(sortFilterRoomListModel.mapToSource(
sortFilterRoomListModel.index(index, 0)), ItemSelectionModel.SelectCurrent)
}
}
Keys.onEnterPressed: enterRoomAction.trigger()
Keys.onReturnPressed: enterRoomAction.trigger()
bold: unreadCount > 0
label: name ?? ""
subtitle: {
let txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
if (txt.length) {
return txt
}
return " "
}
Component {
id: collapsedModeListComponent
leading: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInTimeline
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
QQC2.ItemDelegate {
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
trailing: RowLayout {
QQC2.Label {
text: notificationCount
visible: notificationCount > 0
padding: Kirigami.Units.smallSpacing
color: highlightCount > 0 ? "white" : Kirigami.Theme.textColor
Layout.minimumWidth: height
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
radius: height / 2
}
}
Keys.onEnterPressed: enterRoomAction.trigger()
Keys.onReturnPressed: enterRoomAction.trigger()
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: ListView.view.width
height: ListView.view.width
QQC2.Button {
id: configButton
visible: roomListItem.hovered || Kirigami.Settings.isMobile
Accessible.name: i18n("Configure room")
contentItem: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
QQC2.ToolTip {
enabled: text.length !== 0
text: name ?? ""
action: Kirigami.Action {
id: optionAction
icon.name: "configure"
onTriggered: {
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
configButton.visible = true
configButton.down = true
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.popup()
}
}
}
}
}
Component {
id: roomListContextMenu
RoomListContextMenu {}
}
Component {
id: normalModeListComponent
Kirigami.BasicListItem {
id: roomListItem
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
highlighted: listView.currentIndex === index
focus: true
icon: undefined
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
}
}
Keys.onEnterPressed: enterRoomAction.trigger()
Keys.onReturnPressed: enterRoomAction.trigger()
bold: unreadCount > 0
label: name ?? ""
labelItem.textFormat: Text.PlainText
subtitle: subtitleText
subtitleItem.textFormat: Text.PlainText
onPressAndHold: {
createRoomListContextMenu()
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
leading: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInTimeline
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
trailing: RowLayout {
QQC2.Label {
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
padding: Kirigami.Units.smallSpacing
color: Kirigami.Theme.textColor
Layout.minimumWidth: height
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
id: optionAction
icon.name: "configure"
onTriggered: {
createRoomListContextMenu()
}
}
}
}
function createRoomListContextMenu() {
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
configButton.visible = true
configButton.down = true
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.open()
}
}
}
}
footer: QQC2.ToolBar {
visible: accountList.count > 1 && !collapsedMode
visible: accountList.count > 1
height: visible ? implicitHeight : 0
leftPadding: 0
rightPadding: 0
@@ -379,15 +199,16 @@ Kirigami.ScrollablePage {
spacing: 0
Repeater {
id: accountList
model: AccountRegistry
model: AccountListModel {
id: accountListModel
}
delegate: Kirigami.BasicListItem {
checkable: true
checked: Controller.activeConnection && Controller.activeConnection.localUserId === model.connection.localUserId
checked: Controller.activeConnection && Controller.activeConnection.localUser.id === model.user.id
onClicked: Controller.activeConnection = model.connection
Layout.fillWidth: true
Layout.fillHeight: true
text: model.connection.localUserId
subtitle: model.connection.localUser.accountLabel
text: model.user.id
}
}
}

View File

@@ -8,7 +8,7 @@ import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1 as Platform
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
@@ -23,56 +23,17 @@ Kirigami.ScrollablePage {
/// It's not readonly because of the seperate window view.
property var currentRoom: RoomManager.currentRoom
property bool loading: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
/// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false;
/// Disable cancel shortcut. Used by the seperate window since it provide its own
/// cancel implementation.
property bool disableCancelShortcut: false
title: currentRoom.htmlSafeDisplayName
KeyNavigation.left: pageStack.get(0)
Connections {
target: RoomManager
function onCurrentRoomChanged() {
if(!RoomManager.currentRoom) {
if(pageStack.lastItem == page) {
pageStack.pop()
}
} else if (page.currentRoom.isInvite) {
page.currentRoom.clearInvitationNotification();
}
}
}
title: currentRoom.displayName
signal switchRoomUp()
signal switchRoomDown()
onCurrentRoomChanged: {
hasScrolledUpBefore = false;
chatBoxHelper.clearEditReply()
}
Connections {
target: messageEventModel
function onRowsInserted() {
markReadIfVisibleTimer.restart()
}
}
Timer {
id: markReadIfVisibleTimer
interval: 1000
onTriggered: {
if (loading || !currentRoom.readMarkerLoaded || !applicationWindow().active) {
restart()
} else {
markReadIfVisible()
}
}
ChatBoxHelper.clearEditReply()
}
ActionsHandler {
@@ -81,20 +42,10 @@ Kirigami.ScrollablePage {
connection: Controller.activeConnection
}
ChatBoxHelper {
id: chatBoxHelper
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: applicationWindow().pageStack.get(0).forceActiveFocus()
enabled: !page.disableCancelShortcut
}
Connections {
target: Controller.activeConnection
function onJoinedRoom(room, invited) {
if(page.currentRoom.id === invited.id) {
function onJoinedRoom(room) {
if(room.id === invitation.id) {
RoomManager.enterRoom(room);
}
}
@@ -102,26 +53,28 @@ Kirigami.ScrollablePage {
Connections {
target: actionsHandler
function onShowMessage(messageType, message) {
onShowMessage: {
page.header.contentItem.text = message;
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
page.header.visible = true;
page.header.contentItem.visible = true;
}
}
header: QQC2.Control {
height: visible ? implicitHeight : 0
visible: false
visible: contentItem.visible
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
showCloseButton: true
visible: true
visible: false
}
}
Kirigami.PlaceholderMessage {
id: invitation
property var id
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
@@ -130,7 +83,7 @@ Kirigami.ScrollablePage {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: RoomManager.leaveRoom(page.currentRoom);
onClicked: RoomManager.leave(page.currentRoom);
}
QQC2.Button {
@@ -139,15 +92,22 @@ Kirigami.ScrollablePage {
onClicked: {
currentRoom.acceptInvitation();
invitation.id = currentRoom.id
currentRoom = null
}
}
}
}
Kirigami.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
visible: loading
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
text: i18n("Loading")
QQC2.BusyIndicator {
running: loadingIndicator.visible
Layout.alignment: Qt.AlignHCenter
}
}
focus: true
@@ -166,9 +126,9 @@ Kirigami.ScrollablePage {
Keys.onPressed: {
if (event.key === Qt.Key_PageDown && (event.modifiers & Qt.ControlModifier)) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
switchRoomUp();
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
switchRoomDown();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true;
chatBox.addText(event.text);
@@ -177,15 +137,6 @@ Kirigami.ScrollablePage {
}
}
Connections {
target: currentRoom
function onPositiveMessage(message) {
page.header.contentItem.text = message;
page.header.contentItem.type = Kirigami.MessageType.Positive;
page.header.visible = true;
}
}
// hover actions on a delegate, activated in TimelineContainer.qml
Connections {
target: page.flickable
@@ -195,24 +146,95 @@ Kirigami.ScrollablePage {
}
}
CollapseStateProxyModel {
id: collapseStateProxyModel
sourceModel: sortedMessageEventModel
Item {
id: hoverActions
property var event
property bool showEdit: event && (event.author.id === Controller.activeConnection.localUserId && (event.eventType === "emote" || event.eventType === "message"))
property var bubble
property var hovered: bubble && bubble.hovered
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
onVisibleDelayedChanged: if (visibleDelayed) {
visible = true;
} else {
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
// See https://invent.kde.org/network/neochat/-/issues/333
hoverActionsTimer.restart();
}
Timer {
id: hoverActionsTimer
interval: 200
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
}
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0)) : 0
y: bubble ? bubble.mapToItem(page, 0, -Kirigami.Units.largeSpacing - hoverActions.childHeight * 1.5).y : 0
visible: false
property var updateFunction
property alias childWidth: hoverActionsRow.width
property alias childHeight: hoverActionsRow.height
RowLayout {
id: hoverActionsRow
z: 4
spacing: 0
HoverHandler {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
QQC2.Button {
QQC2.ToolTip.text: i18n("React")
QQC2.ToolTip.visible: hovered
icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open();
EmojiDialog {
id: emojiDialog
onReact: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.focusInputField();
}
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: hoverActions.showEdit
icon.name: "document-edit"
onClicked: {
if (hoverActions.showEdit) {
ChatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
}
chatBox.focusInputField();
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered
icon.name: "mail-replied-symbolic"
onClicked: {
ChatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
chatBox.focusInputField();
}
}
}
}
ListView {
id: messageListView
pixelAligned: true
visible: !invitation.visible
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool isLoaded: page.width * page.height > 10
spacing: 0
spacing: Kirigami.Units.smallSpacing
reuseItems: true
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
model: !isLoaded ? undefined : collapseStateProxyModel
model: !isLoaded ? undefined : sortedMessageEventModel
MessageEventModel {
id: messageEventModel
@@ -238,7 +260,7 @@ Kirigami.ScrollablePage {
}
onAtYEndChanged: if (atYEnd && hasScrolledUpBefore) {
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
hasScrolledUpBefore = false;
@@ -270,7 +292,7 @@ Kirigami.ScrollablePage {
fileDialog.chosen.connect(function(path) {
if (!path) return
chatBoxHelper.attachmentPath = path;
ChatBoxHelper.attachmentPath = path;
})
fileDialog.open()
@@ -292,7 +314,7 @@ Kirigami.ScrollablePage {
if (!Clipboard.saveImage(localPath)) {
return;
}
chatBoxHelper.attachmentPath = localPath;
ChatBoxHelper.attachmentPath = localPath;
attachDialog.close();
}
}
@@ -312,7 +334,297 @@ Kirigami.ScrollablePage {
sourceModel: messageEventModel
}
delegate: EventDelegate {}
delegate: DelegateChooser {
id: timelineDelegateChooser
role: "eventType"
property bool delegateLoaded: true
ListView.onPooled: delegateLoaded = false
ListView.onReused: delegateLoaded = true
DelegateChoice {
roleValue: "state"
delegate: QQC2.Control {
leftPadding: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
topPadding: 0
bottomPadding: 0
contentItem: StateDelegate { }
implicitWidth: messageListView.width - Kirigami.Units.largeSpacing
}
}
DelegateChoice {
roleValue: "emote"
delegate: TimelineContainer {
id: emoteContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
isEmote: true
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: TextDelegate {
isEmote: true
Layout.fillWidth: !Config.showAvatarInTimeline
Layout.maximumWidth: emoteContainer.bubbleMaxWidth
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
}
}
}
}
DelegateChoice {
roleValue: "message"
delegate: TimelineContainer {
id: messageContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: TextDelegate {
Layout.fillWidth: !Config.showAvatarInTimeline
Layout.maximumWidth: messageContainer.bubbleMaxWidth
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
}
}
}
}
DelegateChoice {
roleValue: "notice"
delegate: TimelineContainer {
id: noticeContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
innerObject: TextDelegate {
Layout.fillWidth: !Config.showAvatarInTimeline
Layout.maximumWidth: noticeContainer.bubbleMaxWidth
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
}
}
}
DelegateChoice {
roleValue: "image"
delegate: TimelineContainer {
id: imageContainer
isLoaded: timelineDelegateChooser.delegateLoaded
width: messageListView.width
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: ImageDelegate {
Layout.preferredWidth: Kirigami.Units.gridUnit * 15
Layout.maximumWidth: imageContainer.bubbleMaxWidth
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.preferredHeight: info.h / info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
onTapped: {
fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
}
}
}
}
}
DelegateChoice {
roleValue: "sticker"
delegate: TimelineContainer {
isLoaded: timelineDelegateChooser.delegateLoaded
width: messageListView.width
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
cardBackground: false
innerObject: ImageDelegate {
readonly: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 10
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.preferredHeight: info.h / info.w * width
}
}
}
DelegateChoice {
roleValue: "audio"
delegate: TimelineContainer {
id: audioContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: AudioDelegate {
Layout.fillWidth: true
Layout.maximumWidth: audioContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
}
DelegateChoice {
roleValue: "video"
delegate: TimelineContainer {
id: videoContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: VideoDelegate {
Layout.fillWidth: true
Layout.maximumWidth: videoContainer.bubbleMaxWidth
Layout.preferredHeight: content.info.h / content.info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
}
DelegateChoice {
roleValue: "file"
delegate: TimelineContainer {
id: fileContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
innerObject: FileDelegate {
Layout.fillWidth: true
Layout.maximumWidth: fileContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
}
DelegateChoice {
roleValue: "readMarker"
delegate: QQC2.ItemDelegate {
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
width: ListView.view.width - Kirigami.Units.gridUnit
x: Kirigami.Units.gridUnit / 2
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
opacity: 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: Kirigami.Units.devicePixelRatio
}
Timer {
id: makeMeDisapearTimer
interval: Kirigami.Units.humanMoment * 2
onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
}
ListView.onPooled: makeMeDisapearTimer.stop()
ListView.onAdd: {
const view = ListView.view;
if (view.atYEnd) {
makeMeDisapearTimer.start()
}
}
// When the read marker is visible and we are at the end of the list,
// start the makeMeDisapearTimer
Connections {
target: ListView.view
function onAtYEndChanged() {
makeMeDisapearTimer.start();
}
}
ListView.onRemove: {
const view = ListView.view;
if (view.atYEnd) {
// easy case just mark everything as read
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
return;
}
// mark the last visible index
const lastVisibleIdx = lastVisibleIndex();
if (lastVisibleIdx < index) {
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole)
}
}
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
}
QQC2.RoundButton {
anchors.right: parent.right
@@ -327,7 +639,7 @@ Kirigami.ScrollablePage {
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action {
onTriggered: {
messageListView.goToEvent(currentRoom.readMarkerEventId)
goToEvent(currentRoom.readMarkerEventId)
}
icon.name: "go-up"
}
@@ -361,13 +673,19 @@ Kirigami.ScrollablePage {
}
Component.onCompleted: {
if (currentRoom) {
if (currentRoom.timelineSize < 20) {
currentRoom.getPreviousContent(50);
}
}
positionViewAtBeginning();
}
DropArea {
id: dropAreaFile
anchors.fill: parent
onDropped: chatBoxHelper.attachmentPath = drop.urls[0]
onDropped: ChatBoxHelper.attachmentPath = drop.urls[0]
}
QQC2.Pane {
@@ -390,6 +708,12 @@ Kirigami.ScrollablePage {
MessageDelegateContextMenu {}
}
Component {
id: messageSourceSheet
MessageSourceSheet {}
}
Component {
id: fileDelegateContextMenu
@@ -416,7 +740,7 @@ Kirigami.ScrollablePage {
currentRoom.usersTyping.length,
currentRoom.usersTyping.map(user => user.displayName).join(", ")
) : ""
anchors.left: parent.left
anchors.right: parent.right
height: visible ? implicitHeight : 0
Behavior on height {
NumberAnimation {
@@ -429,105 +753,6 @@ Kirigami.ScrollablePage {
}
headerPositioning: ListView.OverlayHeader
function goToEvent(eventID) {
const index = eventToIndex(eventID)
messageListView.positionViewAtIndex(index, ListView.Center)
itemAtIndex(index).isTemporaryHighlighted = true
}
Item {
id: hoverActions
property var event: null
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
property bool showEdit: event && (userMsg && (event.eventType === "emote" || event.eventType === "message"))
property var delegate: null
property var bubble: null
property var hovered: bubble && bubble.hovered
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
onVisibleDelayedChanged: if (visibleDelayed) {
visible = true;
} else {
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
// See https://invent.kde.org/network/neochat/-/issues/333
hoverActionsTimer.restart();
}
Timer {
id: hoverActionsTimer
interval: 200
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
}
property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0)
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 + (delegate.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 : 0 ): 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
visible: false
property var updateFunction
property alias childWidth: hoverActionsRow.width
property alias childHeight: hoverActionsRow.height
RowLayout {
id: hoverActionsRow
z: 4
spacing: 0
HoverHandler {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
Kirigami.Icon {
source: "security-high"
width: height
height: parent.height
visible: hoverActions.event.verified
HoverHandler {
id: hover
}
QQC2.ToolTip.text: i18n("This message was sent from a verified device")
QQC2.ToolTip.visible: hover.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
QQC2.ToolTip.text: i18n("React")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open();
EmojiDialog {
id: emojiDialog
onReact: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.focusInputField();
}
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
visible: hoverActions.showEdit
icon.name: "document-edit"
onClicked: {
if (hoverActions.showEdit) {
chatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
}
chatBox.focusInputField();
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "mail-replied-symbolic"
onClicked: {
chatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
chatBox.focusInputField();
}
}
}
}
}
@@ -542,14 +767,14 @@ Kirigami.ScrollablePage {
onEditLastUserMessage: {
const targetMessage = messageEventModel.getLastLocalUserMessageEventId();
if (targetMessage) {
chatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], targetMessage["event_id"]);
ChatBoxHelper.edit(targetMessage["body"], targetMessage["body"], targetMessage["event_id"]);
chatBox.focusInputField();
}
}
onReplyPreviousUserMessage: {
const replyResponse = messageEventModel.getLatestMessageFromIndex(0);
if (replyResponse && replyResponse["event_id"]) {
chatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["message"], replyResponse["sender_id"]);
ChatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["event"], replyResponse["sender_id"]);
}
}
}
@@ -593,7 +818,7 @@ Kirigami.ScrollablePage {
function warning(title, message) {
page.header.contentItem.text = `${title}<br />${message}`;
page.header.contentItem.type = Kirigami.MessageType.Warning;
page.header.visible = true;
page.header.contentItem.visible = true;
}
function showUserDetail(user) {
@@ -609,6 +834,10 @@ Kirigami.ScrollablePage {
messageListView.positionViewAtIndex(0, ListView.End)
}
function goToEvent(eventID) {
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
function eventToIndex(eventID) {
const index = messageEventModel.eventIDToIndex(eventID)
if (index === -1)
@@ -638,40 +867,28 @@ Kirigami.ScrollablePage {
return index;
}
// Mark all messages as read if all unread messages are visible to the user
function markReadIfVisible() {
let readMarkerRow = eventToIndex(currentRoom.readMarkerEventId)
if (readMarkerRow > 0 && readMarkerRow < firstVisibleIndex()) {
currentRoom.markAllMessagesAsRead()
}
}
/// Open message context dialog for file and videos
function openFileContext(event, file) {
function openFileContext(author, message, eventId, source, progressInfo, file) {
const contextMenu = fileDelegateContextMenu.createObject(page, {
author: event.author,
message: event.message,
eventId: event.eventId,
source: event.source,
author: author,
message: message,
eventId: eventId,
source: source,
file: file,
mimeType: event.mimeType,
progressInfo: event.progressInfo,
plainMessage: event.message,
progressInfo: progressInfo,
});
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(event, selectedText, plainMessage) {
function openMessageContext(author, message, eventId, source, eventType, formattedBody) {
const contextMenu = messageDelegateContextMenu.createObject(page, {
selectedText: selectedText,
author: event.author,
message: event.display,
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.eventType,
plainMessage: plainMessage,
author: author,
message: message,
eventId: eventId,
formattedBody: formattedBody,
source: source,
eventType: eventType
});
contextMenu.open();
}

View File

@@ -12,14 +12,8 @@ Kirigami.ApplicationWindow {
required property var currentRoom
minimumWidth: Kirigami.Units.gridUnit * 10
minimumHeight: Kirigami.Units.gridUnit * 15
Shortcut {
sequence: StandardKey.Cancel
onActivated: window.close()
}
pageStack.initialPage: RoomPage {
visible: true
currentRoom: window.currentRoom
disableCancelShortcut: true
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18n("Settings")
Kirigami.FormLayout {
QQC2.CheckBox {
Kirigami.FormData.label: i18n("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
visible: Controller.supportSystemTray
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.
Kirigami.FormData.label: i18n("Notifications and events:")
text: i18n("Show notifications")
checked: Config.showNotifications
onToggled: {
Config.showNotifications = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show leave and join events")
checked: Config.showLeaveJoinEvent
onToggled: {
Config.showLeaveJoinEvent = checked
Config.save()
}
}
QQC2.RadioButton {
Kirigami.FormData.label: i18n("Rooms and private chats:")
text: i18n("Separated")
checked: !Config.mergeRoomList
onToggled: {
Config.mergeRoomList = false
Config.save()
}
}
QQC2.RadioButton {
text: i18n("Intermixed")
checked: Config.mergeRoomList
onToggled: {
Config.mergeRoomList = true
Config.save()
}
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Timeline:")
text: i18n("Show User Avatar")
checked: Config.showAvatarInTimeline
onToggled: {
Config.showAvatarInTimeline = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show Fancy Effects")
checked: Config.showFancyEffects
onToggled: {
Config.showFancyEffects = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Use s/text/replacement syntax to edit your last message")
checked: Config.allowQuickEdit
onToggled: {
Config.allowQuickEdit = checked
Config.save()
}
}
}
}

View File

@@ -5,7 +5,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
@@ -30,96 +30,18 @@ Kirigami.ScrollablePage {
Connections {
target: LoginHelper
function onErrorOccured(message) {
onErrorOccured: {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
}
}
Connections {
target: Controller
function onInitiated() {
pageStack.layers.pop();
}
}
ColumnLayout {
Item {
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
Layout.alignment: Qt.AlignHCenter
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
id: swapper
states: [
State {
when: !LoginHelper.homeserverReachable
name: "idle"
PropertyChanges {
target: icon
opacity: 1
}
PropertyChanges {
target: avi
opacity: 0
}
},
State {
when: LoginHelper.homeserverReachable
name: "showAvi"
PropertyChanges {
target: icon
opacity: 0
}
PropertyChanges {
target: avi
opacity: 1
}
}
]
transitions: [
Transition {
to: "showAvi"
SequentialAnimation {
NumberAnimation { target: icon; properties: "opacity";}
NumberAnimation { target: avi; properties: "opacity";}
}
},
Transition {
from: "showAvi"
SequentialAnimation {
NumberAnimation { target: avi; properties: "opacity";}
NumberAnimation { target: icon; properties: "opacity";}
}
}
]
Kirigami.Icon {
id: icon
source: "org.kde.neochat"
anchors.fill: parent
implicitWidth: height
}
ColumnLayout {
id: avi
opacity: 0
anchors.fill: parent
Kirigami.Avatar {
source: LoginHelper.loginAvatar
name: LoginHelper.loginName
Layout.fillHeight: true
implicitWidth: height
Layout.alignment: Qt.AlignHCenter
}
Controls.Label {
text: LoginHelper.loginName
font.pointSize: 24
Layout.alignment: Qt.AlignHCenter
}
}
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
Controls.Label {
Layout.fillWidth: true
@@ -137,7 +59,6 @@ Kirigami.ScrollablePage {
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
@@ -150,26 +71,16 @@ Kirigami.ScrollablePage {
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
icon.name: "go-back"
}
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
opacity: welcomePage.currentStep.showContinueButton ? 1 : 0
Behavior on opacity { NumberAnimation {} }
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
Kirigami.LoadingPlaceholder {
icon.name: "online"
opacity: LoginHelper.testing ? 1 : 0
text: i18n("Connecting to your homeserver...")
Behavior on opacity { NumberAnimation {} }
Layout.alignment: Qt.AlignHCenter
}
Connections {
target: currentStep

View File

@@ -5,5 +5,6 @@ RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml
ImageEditorPage 1.0 ImageEditorPage.qml

View File

@@ -18,195 +18,132 @@ Kirigami.OverlayDrawer {
id: roomDrawer
readonly property var room: RoomManager.currentRoom
width: actualWidth
readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
property int actualWidth: {
if (Config.roomDrawerWidth === -1) {
return Kirigami.Units.gridUnit * 20;
} else {
return Config.roomDrawerWidth
}
}
MouseArea {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: undefined
width: 2
z: 500
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
enabled: true
visible: true
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
onReleased: {
Config.roomDrawerWidth = roomDrawer.actualWidth;
Config.save();
}
property real _lastX: -1
onPositionChanged: {
if (_lastX === -1) {
return;
}
if (Qt.application.layoutDirection === Qt.RightToLeft) {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x))
} else {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x))
}
}
}
enabled: true
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
// If modal has been changed and the drawer is closed automatically then dim on popup open will have been switched off in main.qml so switch it back on after the animation completes.
// This is to avoid dim being active for a split second when the drawer is switched to modal which looks terrible.
onAnimatingChanged: if (dim === false) dim = undefined
topPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Loader {
id: loader
active: roomDrawer.drawerOpen
sourceComponent: ColumnLayout {
id: columnLayout
property alias userSearchText: userListSearchField.text
property alias highlightedUser: userListView.currentIndex
anchors.fill: parent
spacing: 0
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
topPadding: Kirigami.Units.smallSpacing / 2;
bottomPadding: Kirigami.Units.smallSpacing / 2;
rightPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing
RowLayout {
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
spacing: 0
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
level: 1
}
ToolButton {
id: inviteButton
Layout.alignment: Qt.AlignRight
icon.name: "list-add-user"
text: i18n("Invite user to room")
display: AbstractButton.IconOnly
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {room: room})
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
roomDrawer.close();
}
ToolTip {
text: inviteButton.text
}
}
ToolButton {
id: favouriteButton
Item {
// HACK otherwise rating item is not right aligned
Layout.fillWidth: true
}
ToolButton {
Layout.alignment: Qt.AlignRight
icon.name: room && room.isFavourite ? "rating" : "rating-unrated"
checkable: true
checked: room && room.isFavourite
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
display: AbstractButton.IconOnly
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
ToolTip {
text: favouriteButton.text
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
}
}
ToolButton {
id: settingsButton
Layout.alignment: Qt.AlignRight
icon.name: 'settings-configure'
text: i18n("Room settings")
display: AbstractButton.IconOnly
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
onClicked: {
roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
if (!wideScreen) {
roomDrawer.close();
}
}
ToolTip {
text: settingsButton.text
text: i18n("Room settings")
}
}
}
}
ColumnLayout {
Control {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
RowLayout {
padding: Kirigami.Units.largeSpacing
contentItem: ColumnLayout {
id: infoLayout
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.name : i18n("No name")
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
Kirigami.Heading {
text: i18n("Room information")
level: 3
}
ColumnLayout {
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
Layout.margins: Kirigami.Units.largeSpacing
Kirigami.Heading {
Layout.fillWidth: true
level: 1
type: Kirigami.Heading.Type.Primary
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
textFormat: Text.PlainText
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.name : i18n("No name")
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
}
TextEdit {
ColumnLayout {
Layout.fillWidth: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
readOnly: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
Layout.alignment: Qt.AlignVCenter
spacing: 0
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
font.bold: true
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
}
Label {
Layout.fillWidth: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
}
}
}
TextEdit {
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(https:\/\/[^ ]*)/
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
TextEdit {
Layout.maximumWidth: Kirigami.Units.gridUnit * 13
Layout.preferredWidth: Kirigami.Units.gridUnit * 13
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /\(https:\/\/[^ ]*\)/
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
}
@@ -214,31 +151,18 @@ Kirigami.OverlayDrawer {
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
Label {
Layout.alignment: Qt.AlignRight
text: room ? i18np("%1 Member", "%1 Members", room.joinedCount) : i18n("No Member Count")
text: room ? i18np("%1 Member", "%1 Members", room.totalMemberCount) : i18n("No Member Count")
}
}
Control {
Layout.fillWidth: true
// Note need to set padding individually to guarantee it will always work
// see note - https://doc.qt.io/qt-6/qml-qtquick-controls2-control.html#padding-prop
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
Pane {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
contentItem: Kirigami.SearchField {
id: userListSearchField
onAccepted: sortedMessageEventModel.filterString = text;
}
}
@@ -247,12 +171,11 @@ Kirigami.OverlayDrawer {
Layout.fillWidth: true
Layout.fillHeight: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: userListView
clip: true
headerPositioning: ListView.OverlayHeader
boundsBehavior: Flickable.DragOverBounds
activeFocusOnTab: true
model: KSortFilterProxyModel {
@@ -264,56 +187,60 @@ Kirigami.OverlayDrawer {
sortRole: "perm"
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
delegate: Kirigami.BasicListItem {
id: userListItem
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
z: 1
label: name
onClicked: {
const popup = userDetailDialog.createObject(ApplicationWindow.overlay, {room: room, user: user, displayName: name, avatarMediaId: avatar})
popup.closed.connect(function() {
userListItem.highlighted = false
})
if (roomDrawer.modal) {
roomDrawer.close()
contentItem: RowLayout {
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
Layout.preferredHeight: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
visible: Config.showAvatarInTimeline
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
source: avatar ? ("image://mxc/" + avatar) : ""
name: name
}
popup.open()
}
leading: Kirigami.Avatar {
implicitWidth: height
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
source: avatar ? ("image://mxc/" + avatar) : ""
name: name
}
Label {
Layout.fillWidth: true
trailing: Label {
visible: perm != UserType.Member
text: name
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
default:
return "";
Label {
visible: perm != UserType.Member
text: {
if (perm == UserType.Owner) {
return i18n("Owner")
}
if (perm == UserType.Admin) {
return i18n("Admin")
}
if (perm == UserType.Moderator) {
return i18n("Mod")
}
if (perm == UserType.Muted) {
return i18n("Muted")
}
return ""
}
color: Kirigami.Theme.disabledTextColor
font.pixelSize: 12
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
action: Kirigami.Action {
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open()
}
}
}
@@ -322,15 +249,17 @@ Kirigami.OverlayDrawer {
}
onRoomChanged: {
if (loader.active) {
loader.item.userSearchText = ""
loader.item.highlightedUser = -1
}
if (room == null) {
close()
}
}
Component {
id: roomSettingDialog
RoomSettingsDialog {}
}
Component {
id: userDetailDialog

View File

@@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
id: root
property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
text: i18n("General")
icon.name: "settings-configure"
page: Qt.resolvedUrl("General.qml")
initialProperties: {
return {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Security")
icon.name: "security-low"
page: Qt.resolvedUrl("Security.qml")
initialProperties: {
return {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Notifications")
icon.name: "notifications"
page: Qt.resolvedUrl("PushNotification.qml")
initialProperties: {
return {
room: root.room
}
}
}
]
}

View File

@@ -1,202 +0,0 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
property var room
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
readonly property bool canChangeName: room.canSendState("m.room.name")
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
title: i18n("General")
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
Kirigami.Avatar {
Layout.bottomMargin: Kirigami.Units.largeSpacing
name: room.name
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
RoundButton {
anchors.right: parent.right
anchors.bottom: parent.bottom
height: Kirigami.Units.gridUnits
width: Kirigami.Units.gridUnits
icon.name: 'cloud-upload'
Accessible.name: i18n("Update avatar")
enabled: canChangeAvatar
onClicked: {
const fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
room.changeAvatar(path)
})
fileDialog.open()
}
}
}
TextField {
id: roomNameField
text: room.name
Kirigami.FormData.label: i18n("Room Name:")
enabled: canChangeName
}
TextArea {
id: roomTopicField
Layout.fillWidth: true
text: room.topic
Kirigami.FormData.label: i18n("Room topic:")
enabled: canChangeTopic
}
Kirigami.Separator {
Layout.fillWidth: true
visible: canonicalAliasComboBox.visible || altAlias.visible
}
ComboBox {
id: canonicalAliasComboBox
visible: room.aliases && room.aliases.length
Kirigami.FormData.label: i18n("Canonical Alias:")
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
enabled: canChangeCanonicalAlias
model: room.aliases
currentIndex: room.aliases.indexOf(room.canonicalAlias)
onCurrentIndexChanged: {
if (room.canonicalAlias != room.aliases[currentIndex]) {
room.setCanonicalAlias(room.aliases[currentIndex])
}
}
}
RowLayout {
id: altAlias
Kirigami.FormData.label: i18n("Other Aliases:")
Layout.fillWidth: true
visible: room.altAliases && room.altAliases.length
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: room.altAliases
delegate: RowLayout {
Layout.maximumWidth: parent.width
Label {
text: modelData
}
ToolButton {
icon.name: ""
onClicked: room.removeLocalAlias(modelData)
}
}
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: next.visible || prev.visible
}
Control {
id: next
Layout.fillWidth: true
visible: room.predecessorId && room.connection.room(room.predecessorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room continues another conversation.")
actions: Kirigami.Action {
text: i18n("See older messages...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
root.close()
}
}
}
}
Control {
id: prev
Layout.fillWidth: true
visible: room.successorId && room.connection.room(room.successorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room has been replaced.")
actions: Kirigami.Action {
text: i18n("See new room...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
root.close()
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
}
footer: ToolBar {
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
enabled: room.name !== roomNameField.text || room.topic !== roomTopicField.text
text: i18n("Apply")
onClicked: {
if (room.name != roomNameField.text) {
room.setName(roomNameField.text)
}
if (room.topic != roomTopicField.text) {
room.setTopic(roomTopicField.text)
}
}
}
}
}
}

View File

@@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
property var room
title: i18nc('@title:window', 'Notifications')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.RadioButton {
text: i18n("Follow global setting")
Kirigami.FormData.label: i18n("Room notifications setting:")
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Default
}
}
QQC2.RadioButton {
text: i18nc("As in 'notify for all messages'","All")
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.All
}
}
QQC2.RadioButton {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'","@Mentions and Keywords")
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
QQC2.RadioButton {
text: i18nc("As in 'do not notify for any messages'","Off")
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Mute
}
}
}
}
}

View File

@@ -1,69 +0,0 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
property var room
title: i18n("Security")
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
RadioButton {
text: i18nc("@option:check", "Private (invite only)")
Kirigami.FormData.label: i18nc("@option:check", "Access:")
checked: room.joinRule === "invite"
enabled: false
}
Label {
text: i18n("Only invited people can join.")
font: Kirigami.Theme.smallFont
}
RadioButton {
text: i18nc("@option:check", "Space members")
checked: room.joinRule === "restricted"
enabled: false
}
Label {
text: i18n("Anyone in a space can find and join.")
font: Kirigami.Theme.smallFont
}
RadioButton {
text: i18nc("@option:check", "Public")
checked: room.joinRule === "public"
enabled: false
}
Label {
text: i18nc("@option:check", "Anyone can find and join.") + room.joinRule
font: Kirigami.Theme.smallFont
}
}
}
footer: ToolBar {
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
enabled: false
text: i18n("Apply")
}
}
}
}

View File

@@ -1,12 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.AboutPage {
title: i18nc("@title:window", "About NeoChat")
aboutData: Controller.aboutData
}

View File

@@ -1,130 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
title: i18n("Edit Account")
property var connection
ColumnLayout {
Kirigami.FormLayout {
RowLayout {
Kirigami.Avatar {
id: avatar
source: root.connection && root.connection.localUser.avatarMediaId ? ("image://mxc/" + root.connection.localUser.avatarMediaId) : ""
MouseArea {
id: mouseArea
anchors.fill: parent
property var fileDialog: null;
onClicked: {
if (fileDialog != null) {
return;
}
fileDialog = openFileDialog.createObject(Controls.ApplicationWindow.Overlay)
fileDialog.chosen.connect(function(receivedSource) {
mouseArea.fileDialog = null;
if (!receivedSource) {
return;
}
parent.source = receivedSource;
});
fileDialog.onRejected.connect(function() {
mouseArea.fileDialog = null;
});
fileDialog.open();
}
}
}
Controls.Button {
visible: avatar.source.toString().length !== 0
icon.name: "edit-clear"
onClicked: avatar.source = ""
}
Kirigami.FormData.label: i18n("Avatar:")
}
Controls.TextField {
id: name
text: root.connection ? root.connection.localUser.displayName : ""
Kirigami.FormData.label: i18n("Name:")
}
Controls.TextField {
id: accountLabel
text: root.connection ? root.connection.localUser.accountLabel : ""
Kirigami.FormData.label: i18n("Label:")
}
Controls.TextField {
id: currentPassword
Kirigami.FormData.label: i18n("Current Password:")
enabled: roto.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: newPassword
Kirigami.FormData.label: i18n("New Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: confirmPassword
Kirigami.FormData.label: i18n("Confirm new Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
}
}
footer: RowLayout {
Item {
Layout.fillWidth: true
}
Controls.Button {
text: i18n("Save")
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
onClicked: {
if (!Controller.setAvatar(root.connection, avatar.source)) {
showPassiveNotification("The Avatar could not be set");
}
if (root.connection.localUser.displayName !== name.text) {
root.connection.localUser.rename(name.text);
}
if (root.connection.localUser.accountLabel !== accountLabel.text) {
root.connection.localUser.setAccountLabel(accountLabel.text);
}
if(currentPassword.text !== "" && newPassword.text !== "" && confirmPassword.text !== "") {
if(newPassword.text === confirmPassword.text) {
Controller.changePassword(root.connection, currentPassword.text, newPassword.text);
} else {
showPassiveNotification(i18n("Passwords do not match"));
return;
}
}
root.closeDialog();
}
}
Controls.Button {
text: i18n("Cancel")
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
onClicked: root.closeDialog();
}
}
}

View File

@@ -1,113 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
title: i18n("Accounts")
actions.main: Kirigami.Action {
text: i18n("Add an account")
icon.name: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
visible: !pageSettingStack.wideMode
}
ListView {
model: AccountRegistry
anchors.fill: parent
delegate: Kirigami.BasicListItem {
text: model.connection.localUser.displayName
labelItem.textFormat: Text.PlainText
subtitle: model.connection.localUserId
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
onClicked: {
Controller.activeConnection = model.connection;
pageStack.layers.pop();
}
trailing: RowLayout {
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
Controls.ToolTip {
text: parent.action.text
}
action: Kirigami.Action {
text: i18n("Edit this account")
iconName: "document-edit"
onTriggered: pageSettingStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
connection: model.connection
}, {
title: i18n("Account editor")
});
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
Controls.ToolTip {
text: parent.action.text
}
action: Kirigami.Action {
text: i18n("Logout")
iconName: "im-kick-user"
onTriggered: {
Controller.logout(model.connection, true);
if (Controller.accountCount === 1) {
pageStack.layers.pop();
}
}
}
}
}
}
}
footer: Controls.ToolBar {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.ActionToolBar {
alignment: Qt.AlignRight
rightPadding: Kirigami.Units.smallSpacing
width: parent.width
flat: false
actions: Kirigami.Action {
text: i18n("Add an account")
icon.name: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
}
}
}
Connections {
target: Controller
function onConnectionAdded() {
if (pageStack.layers.depth > 2)
pageStack.layers.pop()
}
function onPasswordStatus(status) {
if (status === Controller.Success) {
showPassiveNotification(i18n("Password changed successfully"));
} else if (status === Controller.Wrong) {
showPassiveNotification(i18n("Wrong password entered"));
} else {
showPassiveNotification(i18n("Unknown problem while trying to change password"));
}
}
}
property Component openFileDialog: Component {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
}
}
}

View File

@@ -1,257 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Settings 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "Appearance")
ColumnLayout {
RowLayout {
Layout.alignment: Qt.AlignCenter
spacing: Kirigami.Units.gridUnit * 2
QQC2.ButtonGroup { id: themeGroup }
ThemeRadioButton {
innerObject: [
RowLayout {
Layout.fillWidth: true
Kirigami.Avatar {
color: "#4a5bcc"
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
}
QQC2.Control {
Layout.fillWidth: true
contentItem: ColumnLayout {
QQC2.Label {
Layout.fillWidth: true
font.weight: Font.Bold
font.pixelSize: 7
text: "Paul Müller"
color: "#4a5bcc"
wrapMode: Text.Wrap
}
QQC2.Label {
Layout.fillWidth: true
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta mauris, quis finibus sem suscipit tincidunt."
wrapMode: Text.Wrap
font.pixelSize: 7
}
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
}
}
},
RowLayout {
Layout.fillWidth: true
Kirigami.Avatar {
color: "#9f244b"
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
}
QQC2.Control {
Layout.fillWidth: true
contentItem: ColumnLayout {
QQC2.Label {
Layout.fillWidth: true
font.weight: Font.Bold
font.pixelSize: 7
text: "Jean Paul"
color: "#9f244b"
wrapMode: Text.Wrap
}
QQC2.Label {
Layout.fillWidth: true
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta , quis sem suscipit tincidunt."
wrapMode: Text.Wrap
font.pixelSize: 7
}
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
}
}
}
]
text: i18n("Bubbles")
checked: !Config.compactLayout
QQC2.ButtonGroup.group: themeGroup
enabled: !Config.isCompactLayoutImmutable
onToggled: {
Config.compactLayout = !checked;
Config.save();
}
}
ThemeRadioButton {
innerObject: [
RowLayout {
Layout.fillWidth: true
Kirigami.Avatar {
color: "#4a5bcc"
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
}
ColumnLayout {
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
font.weight: Font.Bold
font.pixelSize: 7
text: "Paul Müller"
color: "#4a5bcc"
wrapMode: Text.Wrap
}
QQC2.Label {
Layout.fillWidth: true
text: "Lorem ipsum dolor sit amet, consectetur elit. Vivamus facilisis porta mauris, finibus sem suscipit tincidunt."
wrapMode: Text.Wrap
font.pixelSize: 7
}
}
},
RowLayout {
Layout.fillWidth: true
Kirigami.Avatar {
color: "#9f244b"
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
}
ColumnLayout {
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
font.weight: Font.Bold
font.pixelSize: 7
text: "Jean Paul"
color: "#9f244b"
wrapMode: Text.Wrap
}
QQC2.Label {
Layout.fillWidth: true
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta mauris, quis finibus sem suscipit tincidunt."
wrapMode: Text.Wrap
font.pixelSize: 7
}
}
}
]
text: i18n("Compact")
checked: Config.compactLayout
QQC2.ButtonGroup.group: themeGroup
enabled: !Config.isCompactLayoutImmutable
onToggled: {
Config.compactLayout = checked;
Config.save();
}
}
}
Kirigami.FormLayout {
Layout.maximumWidth: parent.width
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Show Avatar:")
text: i18n("In Chat")
checked: Config.showAvatarInTimeline
onToggled: {
Config.showAvatarInTimeline = checked
Config.save()
}
enabled: !Config.isShowAvatarInTimelineImmutable
}
QQC2.CheckBox {
text: i18n("In Sidebar")
checked: Config.showAvatarInRoomDrawer
enabled: !Config.isShowAvatarInRoomDrawerImmutable
onToggled: {
Config.showAvatarInRoomDrawer = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show Fancy Effects")
checked: Config.showFancyEffects
enabled: !Config.isShowFancyEffectsImmutable
onToggled: {
Config.showFancyEffects = checked;
Config.save();
}
}
Loader {
visible: item !== null
Kirigami.FormData.label: item ? i18n("Theme:") : ""
source: "qrc:/imports/NeoChat/Settings/ColorScheme.qml"
}
QQC2.CheckBox {
visible: Controller.hasWindowSystem
text: i18n("Use transparent chat page")
enabled: !Config.compactLayout && !Config.isBlurImmutable
checked: Config.blur
onToggled: {
Config.blur = checked;
Config.save();
}
}
RowLayout {
visible: Controller.hasWindowSystem && Config.blur
enabled: !Config.isTransparancyImmutable
Kirigami.FormData.label: i18n("Transparency:")
QQC2.Slider {
enabled: !Config.compactLayout && Config.blur
from: 0
to: 1
stepSize: 0.05
value: Config.transparency
onMoved: {
Config.transparency = value;
Config.save();
}
HoverHandler { id: sliderHover }
QQC2.ToolTip.visible: sliderHover.hovered && !enabled
QQC2.ToolTip.text: i18n("Only enabled if the transparent chat page is enabled.")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
text: Math.round(Config.transparency * 100) + "%"
}
}
QQC2.CheckBox {
text: i18n("Show your messages on the right")
checked: Config.showLocalMessagesOnRight
enabled: !Config.isShowLocalMessagesOnRightImmutable && !Config.compactLayout
onToggled: {
Config.showLocalMessagesOnRight = checked
Config.save()
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Settings 1.0
QQC2.ComboBox {
textRole: "display"
model: ColorSchemer.model
Component.onCompleted: currentIndex = ColorSchemer.indexForScheme(Config.colorScheme);
onActivated: {
ColorSchemer.apply(currentIndex);
Config.colorScheme = ColorSchemer.nameForIndex(currentIndex);
Config.save();
}
}

View File

@@ -1,114 +0,0 @@
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18n("Devices")
ListView {
model: DevicesModel {
id: devices
}
anchors.fill: parent
Kirigami.LoadingPlaceholder {
visible: parent.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
}
delegate: Kirigami.BasicListItem {
text: model.displayName
subtitle: model.id
icon: "network-connect"
trailing: RowLayout {
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Edit device name")
iconName: "document-edit"
onTriggered: {
renameSheet.index = model.index
renameSheet.name = model.displayName
renameSheet.open()
}
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
visible: Controller.encryptionSupported
action: Kirigami.Action {
text: i18n("Verify device")
iconName: "security-low-symbolic"
onTriggered: {
devices.connection.startKeyVerificationSession(model.id)
}
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Logout device")
iconName: "edit-delete-remove"
onTriggered: {
passwordSheet.index = index
passwordSheet.open()
}
}
}
}
}
}
Kirigami.OverlaySheet {
id: passwordSheet
property var index
title: i18n("Remove device")
Kirigami.FormLayout {
Controls.TextField {
id: passwordField
Kirigami.FormData.label: i18n("Password:")
echoMode: TextInput.Password
}
Controls.Button {
text: i18n("Confirm")
onClicked: {
devices.logout(passwordSheet.index, passwordField.text)
passwordField.text = ""
passwordSheet.close()
}
}
}
}
Kirigami.OverlaySheet {
id: renameSheet
property int index
property string name
title: i18n("Edit device")
Kirigami.FormLayout {
Controls.TextField {
id: nameField
Kirigami.FormData.label: i18n("Name:")
text: renameSheet.name
}
Controls.Button {
text: i18n("Save")
onClicked: {
devices.setName(renameSheet.index, nameField.text)
renameSheet.close()
}
}
}
}
}

View File

@@ -1,122 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Settings 1.0
import NeoChat.Component 1.0 as Components
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "Custom Emojis")
ListView {
anchors.fill: parent
model: CustomEmojiModel {
id: emojiModel
connection: Controller.activeConnection
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("No custom inline stickers found")
visible: parent.model.count === 0
}
delegate: Kirigami.BasicListItem {
id: del
required property string name
required property url imageURL
text: name
reserveSpaceForSubtitle: true
leading: Image {
width: height
sourceSize.width: width
sourceSize.height: height
source: imageURL
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: Components.ShimmerGradient { }
}
}
trailing: QQC2.ToolButton {
width: height
icon.name: "delete"
onClicked: emojiModel.removeEmoji(del.name)
}
}
}
footer: QQC2.ToolBar {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.ActionToolBar {
id: emojiCreator
alignment: Qt.AlignRight
rightPadding: Kirigami.Units.smallSpacing
width: parent.width
flat: false
property string name
actions: [
Kirigami.Action {
displayComponent: QQC2.TextField {
id: emojiField
placeholderText: i18n("new_emoji_name_here")
validator: RegularExpressionValidator {
regularExpression: /[a-zA-Z_0-9]*/
}
onTextChanged: emojiCreator.name = text
}
},
Kirigami.Action {
text: i18n("Add Emoji...")
enabled: emojiCreator.name.length > 0
property var fileDialog: null
icon.name: 'list-add'
onTriggered: {
if (this.fileDialog !== null) {
return;
}
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
this.fileDialog.chosen.connect((url) => {
emojiModel.addEmoji(emojiCreator.name, url)
this.fileDialog = null
})
this.fileDialog.onRejected.connect(() => {
this.fileDialog = null
})
this.fileDialog.open()
}
}
]
}
}
Component {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
}
}
}

View File

@@ -1,136 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "General")
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.CheckBox {
Kirigami.FormData.label: i18n("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
visible: Controller.supportSystemTray
enabled: !Config.isSystemTrayImmutable
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Minimize to system tray on startup")
checked: Config.minimizeToSystemTrayOnStartup
visible: Controller.supportSystemTray && !Kirigami.Settings.isMobile
enabled: Config.systemTray && !Config.isMinimizeToSystemTrayOnStartupImmutable
onToggled: {
Config.minimizeToSystemTrayOnStartup = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.
Kirigami.FormData.label: i18n("Notifications and events:")
text: i18n("Show notifications")
checked: Config.showNotifications
enabled: !Config.isShowNotificationsImmutable
onToggled: {
Config.showNotifications = checked
Config.save()
NotificationsManager.globalNotificationsEnabled = checked
}
}
QQC2.CheckBox {
text: i18n("Show leave and join events")
checked: Config.showLeaveJoinEvent
enabled: !Config.isShowLeaveJoinEventImmutable
onToggled: {
Config.showLeaveJoinEvent = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show name change events")
checked: Config.showRename
enabled: !Config.isShowRenameImmutable
onToggled: {
Config.showRename = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show avatar update events")
checked: Config.showAvatarUpdate
enabled: !Config.isShowAvatarUpdateImmutable
onToggled: {
Config.showAvatarUpdate = checked
Config.save()
}
}
QQC2.RadioButton {
Kirigami.FormData.label: i18n("Rooms and private chats:")
text: i18n("Separated")
checked: !Config.mergeRoomList
enabled: !Config.isMergeRoomListImmutable
onToggled: {
Config.mergeRoomList = false
Config.save()
}
}
QQC2.RadioButton {
text: i18n("Intermixed")
checked: Config.mergeRoomList
enabled: !Config.isMergeRoomListImmutable
onToggled: {
Config.mergeRoomList = true
Config.save()
}
}
QQC2.CheckBox {
id: quickEditCheckbox
Layout.maximumWidth: parent.width
text: i18n("Use s/text/replacement syntax to edit your last message")
checked: Config.allowQuickEdit
enabled: !Config.isAllowQuickEditImmutable
onToggled: {
Config.allowQuickEdit = checked
Config.save()
}
// TODO KF5.97 remove this line
Component.onCompleted: this.contentItem.wrap = QQC2.Label.Wrap
}
QQC2.CheckBox {
text: i18n("Send Typing Notifications")
checked: Config.typingNotifications
enabled: !Config.isTypingNotificationsImmutable
onToggled: {
Config.typingNotifications = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Automatically hide/unhide the room information when resizing the window")
Layout.maximumWidth: parent.width
checked: Config.autoRoomInfoDrawer
enabled: !Config.isAutoRoomInfoDrawerImmutable
onToggled: {
Config.autoRoomInfoDrawer = checked
Config.save()
}
// TODO KF5.97 remove this line
Component.onCompleted: this.contentItem.wrap = QQC2.Label.Wrap
}
}
}
}

View File

@@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
text: i18n("General")
icon.name: "org.kde.neochat"
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Appearance")
icon.name: "preferences-desktop-theme-global"
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Accounts")
icon.name: "preferences-system-users"
page: Qt.resolvedUrl("AccountsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Custom Emojis")
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},
Kirigami.SettingAction {
text: i18n("Spell Checking")
iconName: "tools-check-spelling"
page: Qt.resolvedUrl("SonnetConfigPage.qml")
},
Kirigami.SettingAction {
text: i18n("Devices")
iconName: "network-connect"
page: Qt.resolvedUrl("DevicesPage.qml")
},
Kirigami.SettingAction {
text: i18n("About NeoChat")
icon.name: "help-about"
page: Qt.resolvedUrl("About.qml")
}
]
}

View File

@@ -1,344 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
import QtQml 2.15
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.sonnet 1.0 as Sonnet
Kirigami.Page {
id: page
/**
* This property holds whether the setting on that page are automatically
* applied or whether the user can apply then manually. By default, false.
*/
property bool instantApply: false
/**
* This property holds whether the ListViews inside the page should get
* extra padding and a background. By default, use the Kirigami.ApplicationWindow
* wideMode value.
*/
property bool wideMode: QQC2.ApplicationWindow.window.wideMode ?? QQC2.ApplicationWindow.window.width > Kirigami.Units.gridUnit * 40
/**
* Signal emmited when the user decide to discard it's change and close the
* setting page.
*
* For example when using the ConfigPage inside Kirigami PageRow:
*
* \code
* Sonnet.ConfigPage {
* onClose: applicationWindow().pageStack.pop();
* }
* \endcode
*/
signal close()
function onBackRequested(event) {
if (settings.modified) {
applyDialog.open();
event.accepted = true;
}
if (dialog) {
dialog.close();
}
}
title: i18nc("@window:title", "Spellchecking")
QQC2.Dialog {
id: applyDialog
title: qsTr("Apply Settings")
contentItem: QQC2.Label {
text: qsTr("The settings of the current module have changed.<br /> Do you want to apply the changes or discard them?")
}
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel | QQC2.Dialog.Discard
onAccepted: {
settings.save();
applyDialog.close();
page.close();
}
onDiscarded: {
applyDialog.close();
page.close();
}
onRejected: applyDialog.close();
}
onWideModeChanged: scroll.background.visible = wideMode;
leftPadding: wideMode ? Kirigami.Units.gridUnit : 0
topPadding: wideMode ? Kirigami.Units.gridUnit : 0
bottomPadding: wideMode ? Kirigami.Units.gridUnit : 0
rightPadding: wideMode ? Kirigami.Units.gridUnit : 0
property var dialog: null
Sonnet.Settings {
id: settings
}
ColumnLayout {
anchors.fill: parent
Kirigami.FormLayout {
Layout.fillWidth: true
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
QQC2.ComboBox {
Kirigami.FormData.label: i18n("Selected default language:")
model: settings.dictionaryModel
textRole: "display"
valueRole: "languageCode"
Component.onCompleted: currentIndex = indexOfValue(settings.defaultLanguage);
onActivated: {
settings.defaultLanguage = currentValue;
}
}
QQC2.Button {
text: i18n("Open Personal Dictionary")
onClicked: if (!dialog) {
if (Kirigami.Settings.isMobile) {
dialog = mobileSheet.createObject(page, {settings: settings});
dialog.open();
} else {
dialog = desktopSheet.createObject(page, {settings: settings})
dialog.show();
}
} else {
if (Kirigami.Settings.isMobile) {
dialog.open();
} else {
dialog.show();
}
}
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Options:")
checked: settings.checkerEnabledByDefault
text: i18n("Enable automatic spell checking")
onCheckedChanged: {
settings.checkerEnabledByDefault = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
checked: settings.skipUppercase
text: i18n("Ignore uppercase words")
onCheckedChanged: {
settings.skipUppercase = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
checked: settings.skipRunTogether
text: i18n("Ignore hyphenated words")
onCheckedChanged: {
settings.skipRunTogether = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
id: autodetectLanguageCheckbox
checked: settings.autodetectLanguage
text: i18n("Detect language automatically")
onCheckedChanged: {
settings.autodetectLanguage = checked;
if (instantApply) {
settings.save();
}
}
}
}
Kirigami.Heading {
level: 2
text: i18n("Spell checking languages")
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
}
QQC2.Label {
text: i18n("%1 will provide spell checking and suggestions for the languages listed here when autodetection is enabled.", Qt.application.displayName)
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
}
QQC2.ScrollView {
id: scroll
Layout.fillWidth: true
Layout.fillHeight: true
enabled: autodetectLanguageCheckbox.checked
Component.onCompleted: background.visible = wideMode
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
clip: true
model: settings.dictionaryModel
delegate: Kirigami.CheckableListItem {
label: model.display
action: Kirigami.Action {
onTriggered: model.checked = checked
}
checked: model.checked
trailing: Kirigami.Icon {
source: "favorite"
visible: model.isDefault
HoverHandler {
id: hover
}
QQC2.ToolTip {
visible: hover.hovered
text: qsTr("Default Language")
}
}
}
}
}
}
component SheetHeader : RowLayout {
QQC2.TextField {
id: dictionaryField
Layout.fillWidth: true
placeholderText: i18n("Add a new word to your personal dictionary…")
}
QQC2.Button {
text: i18nc("@action:button", "Add word")
icon.name: "list-add"
enabled: dictionaryField.text.length > 0
onClicked: {
add(dictionaryField.text);
dictionaryField.clear();
if (instantApply) {
settings.save();
}
}
Layout.rightMargin: Kirigami.Units.largeSpacing
}
}
Component {
id: desktopSheet
QQC2.ApplicationWindow {
id: window
required property Sonnet.Settings settings
title: i18n("Spell checking dictionary")
width: Kirigami.Units.gridUnit * 20
height: Kirigami.Units.gridUnit * 20
flags: Qt.Dialog | Qt.WindowCloseButtonHint
header: Kirigami.AbstractApplicationHeader {
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
contentItem: SheetHeader {
anchors.fill: parent
}
}
QQC2.ScrollView {
anchors.fill: parent
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
model: settings.currentIgnoreList
delegate: Kirigami.BasicListItem {
label: model.modelData
trailing: QQC2.ToolButton {
icon.name: "delete"
onClicked: {
remove(modelData)
if (instantApply) {
settings.save();
}
}
QQC2.ToolTip {
text: i18n("Delete word")
}
}
}
}
}
}
}
Component {
id: mobileSheet
Kirigami.OverlaySheet {
required property Sonnet.Settings settings
id: dictionarySheet
header: SheetHeader {}
ListView {
implicitWidth: Kirigami.Units.gridUnit * 15
model: settings.currentIgnoreList
delegate: Kirigami.BasicListItem {
label: model.modelData
trailing: QQC2.ToolButton {
icon.name: "delete"
onClicked: {
remove(modelData)
if (instantApply) {
settings.save();
}
}
QQC2.ToolTip {
text: i18n("Delete word")
}
}
}
}
}
}
footer: QQC2.ToolBar {
visible: !instantApply
height: visible ? implicitHeight : 0
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18n("Apply")
enabled: settings.modified
onClicked: settings.save();
}
}
}
function add(word) {
const dictionary = settings.currentIgnoreList;
dictionary.push(word);
settings.currentIgnoreList = dictionary;
}
function remove(word) {
settings.currentIgnoreList = settings.currentIgnoreList.filter(function (value, _, _) {
return value !== word;
});
}
}

View File

@@ -1,65 +0,0 @@
// Copyright 2021 Marco Martin <mart@kde.org>
// Copyright 2018 Furkan Tokac <furkantokac34@gmail.com>
// Copyright 2019 Nate Graham <nate@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
QQC2.RadioButton {
id: delegate
implicitWidth: contentItem.implicitWidth
implicitHeight: contentItem.implicitHeight
property alias innerObject: contentLayout.children
contentItem: ColumnLayout {
Kirigami.ShadowedRectangle {
implicitWidth: implicitHeight * 1.6
implicitHeight: Kirigami.Units.gridUnit * 6
radius: Kirigami.Units.smallSpacing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
shadow.xOffset: 0
shadow.yOffset: 2
shadow.size: 10
shadow.color: Qt.rgba(0, 0, 0, 0.3)
color: {
if (delegate.checked) {
return Kirigami.Theme.highlightColor;
} else if (delegate.hovered) {
// Match appearance of hovered list items
return Qt.rgba(Kirigami.Theme.highlightColor.r,
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b,
0.5);
} else {
return Kirigami.Theme.backgroundColor;
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
clip: true
}
}
QQC2.Label {
id: label
Layout.fillWidth: true
text: delegate.text
horizontalAlignment: Text.AlignHCenter
}
}
indicator: Item {}
background: Item {}
}

View File

@@ -1,4 +0,0 @@
module NeoChat.Settings
ThemeRadioButton 1.0 ThemeRadioButton.qml
SettingsPage 1.0 SettingsPage.qml
SonnetConfigPage 1.0 SonnetConfigPage.qml

94
neochat.notifyrc Normal file
View File

@@ -0,0 +1,94 @@
[Global]
IconName=org.kde.neochat
Name=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
Name[es]=NeoChat
Name[fi]=NeoChat
Name[ia]=Neochat
Name[it]=NeoChat
Name[ko]=NeoChat
Name[nl]=NeoChat
Name[nn]=NeoChat
Name[pl]=NeoChat
Name[sv]=NeoChat
Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
DesktopEntry=org.kde.neochat
Comment=A client for matrix, the decentralized communication protocol
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat
Comment[ca@valencia]=Un client per al Matrix, el protocol de comunicacions descentralitzat
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[ia]=Un cliente per matrix, le protocollo de communication decentralisate
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldings­protokollen.
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado
Comment[ro]=Client pentru Matrix, protocolul de comunicare descentralizată
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
Comment[zh_CN]=分布式通讯协议 Matrix 的客户端
[Event/message]
Name=New message
Name[ca]=Missatge nou
Name[ca@valencia]=Missatge nou
Name[cs]=Nová zpráva
Name[de]=Neue Nachricht
Name[en_GB]=New message
Name[es]=Nuevo mensaje
Name[eu]=Mezu berria
Name[fi]=Uusi viesti
Name[fr]=Nouveau message
Name[hu]=Új üzenet
Name[ia]=Nove message
Name[it]=Nuovo messaggio
Name[ko]=새 메시지
Name[nl]=Nieuw bericht
Name[nn]=Ny melding
Name[pl]=Nowa wiadomość
Name[pt]=Nova mensagem
Name[pt_BR]=Nova mensagem
Name[ro]=Mesaj nou
Name[sk]=Nová správa
Name[sl]=Novo sporočilo
Name[sv]=Nytt meddelande
Name[uk]=Нове повідомлення
Name[x-test]=xxNew messagexx
Name[zh_CN]=新消息
Comment=There is a new message
Comment[ca]=Hi ha un missatge nou
Comment[ca@valencia]=Hi ha un missatge nou
Comment[de]=Es ist eine neue Nachricht vorhanden
Comment[en_GB]=There is a new message
Comment[es]=Hay un mensaje nuevo
Comment[eu]=Mezu berri bat dago
Comment[fi]=Saapui uusi viesti
Comment[fr]=Il y a un nouveau message
Comment[hu]=Új üzenet érkezett
Comment[ia]=Isto es un nove message
Comment[it]=È presente un nuovo messaggio
Comment[ko]=새 메시지가 있음
Comment[nl]=Er is een nieuw bericht
Comment[nn]=Du har ei ny melding
Comment[pl]=Dostępna jest nowa wiadomość
Comment[pt]=Tem uma mensagem nova
Comment[pt_BR]=Existe uma nova mensagem
Comment[ro]=Este un mesaj nou
Comment[sk]=Je nová správa
Comment[sl]=Prišlo je novo sporočilo
Comment[sv]=Det finns ett nytt meddelande
Comment[uk]=Надійшло нове повідомлення
Comment[x-test]=xxThere is a new messagexx
Comment[zh_CN]=有新消息
Action=Popup

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