Compare commits

..

1 Commits

294 changed files with 60451 additions and 131561 deletions

View File

@@ -22,12 +22,6 @@
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [
{
"name": "kirigamiaddons",
"config-opts": [ "-DBUILD_TESTING=OFF" ],
"buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
},
{
"name": "kquickimageeditor",
"buildsystem": "cmake-ninja",
@@ -106,8 +100,7 @@
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "dev",
"disable-submodules": true
"branch": "dev"
}
],
"config-opts": [
@@ -136,8 +129,8 @@
"sources": [
{
"type": "archive",
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.7.0.tar.gz",
"sha256": "23ef0217926e67c8d2eb861cf91617da2f7d8d5a9ae6c62321b21448b1669210",
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.4.0.tar.gz",
"sha256": "0e68b3f0ce7bf521ffbdd731464d2d60d8d7a39a749b551ed26855a1707d86d1",
"x-checker-data": {
"type": "anitya",
"project-id": 236236,

2
.gitignore vendored
View File

@@ -7,5 +7,3 @@ compile_commands.json
.cache/
.vscode/
kate.project.ctags.*
*.user
.flatpak-builder/

View File

@@ -13,21 +13,16 @@ Dependencies:
'frameworks/kitemmodels': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
'libraries/kirigami-addons': '@latest'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- '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', 'FreeBSD', 'Windows' ]
require-passing-tests-on: [ 'Linux', 'Windows' ]

View File

@@ -6,11 +6,15 @@ Files: 128-logo.png icons/* logo.png org.kde.neochat.svg org.kde.neochat.tray.sv
Copyright: 2020 Carson Black <uhhadd@gmail.com>
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
Files: qtquickcontrols2.conf
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: CC0-1.0
Files: android/res/drawable/splash.xml
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: .gitignore
Files: */qmldir .gitignore
Copyright: None
License: CC0-1.0
@@ -18,7 +22,7 @@ Files: .gitlab/issue_templates/bug.md
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: CC0-1.0
Files: src/res.qrc src/res_android.qrc src/res_desktop.qrc
Files: res.qrc res_android.qrc res_desktop.qrc
Copyright: None
License: CC0-1.0
@@ -27,14 +31,15 @@ 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>
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>
Copyright: 2020-2021 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: src/qml/Component/confetti.png src/qml/Component/glowdot.png
Files: imports/NeoChat/Component/confetti.png imports/NeoChat/Component/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "22.11")
set(PROJECT_VERSION "22.06")
set(KF5_MIN_VERSION "5.91.0")
set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.2")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -25,15 +25,12 @@ include(FeatureSummary)
include(ECMSetupVersion)
include(KDEInstallDirs)
include(ECMFindQmlModule)
include(KDEClangFormat)
include(KDECMakeSettings)
include(ECMAddTests)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
@@ -49,7 +46,7 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons)
set_package_properties(KF5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -58,7 +55,6 @@ set_package_properties(KF5Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF5KirigamiAddons 0.6 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
@@ -78,6 +74,7 @@ else()
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.sonnet 1.0)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
@@ -112,16 +109,13 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro${QT_MAJOR_VERSION} COMPONENTS Core QUIET)
if(NOT QCoro${QT_MAJOR_VERSION}_FOUND)
find_package(QCoro REQUIRED)
endif()
qcoro_enable_coroutines()
find_package(KF5DocTools ${KF5_MIN_VERSION})
set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
@@ -140,24 +134,14 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
endif()
if(KF5DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()
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})
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)
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

View File

@@ -1,170 +0,0 @@
Creative Commons Attribution-ShareAlike 4.0 International
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensors permission is not necessary for any reasonfor example, because of any applicable exception or limitation to copyrightthen that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
Creative Commons Attribution-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
A. Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. Additional offer from the Licensor Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapters License You apply.
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapters License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -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,10 +0,0 @@
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
enable_testing()
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test Quotient
TEST_NAME neochatroomtest
)

View File

@@ -1,148 +0,0 @@
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include "neochatroom.h"
#include <connection.h>
#include <quotient_common.h>
#include <syncdata.h>
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class NeoChatRoomTest : public QObject {
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void subtitleTextTest();
void eventTest();
};
void NeoChatRoomTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1235
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
}
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
}
void NeoChatRoomTest::eventTest()
{
QCOMPARE(room->timelineSize(), 1);
}
QTEST_MAIN(NeoChatRoomTest)
#include "neochatroomtest.moc"

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

@@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % English "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title>NeoChat User's Manual</title>
<author><firstname>Carl</firstname><surname>Schwan</surname>
<contrib>NeoChat man page.</contrib>
<email>carl@carlschwan.eu</email></author>
<date>2022-11-01</date>
<releaseinfo>22.09</releaseinfo>
<productname>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command>neochat</command>
</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>neochat</refname>
<refpurpose>Client for interacting with the matrix messaging protocol</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis>
<command>neochat</command>
<arg choice="opt"><replaceable>URI</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title>Description</title>
<para>
<command>neochat</command> is a chat application for the matrix protocol
that work on both desktop and mobile.
</para>
</refsect1>
<refsect1 id="options"><title>Options</title>
<variablelist>
<varlistentry>
<term><option>URI</option></term>
<listitem>
<para>
The matrix uri for an user or a room. e.g matrix:u/user:example.org and
matrix:r/root:example.org. This will makes NeoChat try to open the given
room or conversation.
</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title>Reporting bugs</title>
<para>You can report bugs and feature requests at <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General">https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink></para>
</refsect1>
<refsect1>
<title>See Also</title>
<simplelist>
<member>
A list of frequently asked questions about Matrix <ulink url="https://matrix.org/faq/">https://matrix.org/faq/</ulink>
</member>
<member>kf5options(7)</member>
<member>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"><title>Copyright</title>
<para>Copyright &copy; 2020-2022 Tobias Fella </para>
<para>Copyright &copy; 2020-2022 Carl Schwan </para>
<para>License: GNU General Public Version 3 or later &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html">https://www.gnu.org/licenses/gpl-3.0.html</ulink>&gt;</para>
</refsect1>
</refentry>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -4,24 +4,24 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Page 1.0
Loader {
id: attachmentPaneLoader
id: root
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath)
property var attachmentMimetype: FileType.mimeTypeForUrl(chatBoxHelper.attachmentPath)
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
active: visible
sourceComponent: Component {
QQC2.Pane {
Pane {
id: attachmentPane
property string baseFileName: chatBoxHelper.attachmentPath.toString().substring(chatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, chatBoxHelper.attachmentPath.length)
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Item {
@@ -45,8 +45,8 @@ Loader {
width: Math.min(implicitWidth, attachmentPane.availableWidth)
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
smooth: height == preferredHeight && parent.height == parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? chatBoxHelper.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
@@ -72,7 +72,7 @@ Loader {
}
}
QQC2.BusyIndicator {
BusyIndicator {
id: imageBusyIndicator
anchors {
horizontalCenter: parent.horizontalCenter
@@ -98,7 +98,7 @@ Loader {
source: attachmentMimetype.iconName
}
QQC2.Label {
Label {
id: fileLabel
text: baseFileName
}
@@ -115,7 +115,7 @@ Loader {
// Using a toolbar to get a button spacing consistent with what the QQC2 style normally has
// Also has some accessibility info
QQC2.ToolBar {
ToolBar {
id: toolBar
width: parent.width
anchors.top: parent.top
@@ -130,7 +130,7 @@ Loader {
contentItem: RowLayout {
spacing: parent.spacing
QQC2.Label {
Label {
Layout.leftMargin: -attachmentPane.leftPadding
Layout.topMargin: -attachmentPane.topPadding
leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding
@@ -152,37 +152,37 @@ Loader {
Item {
Layout.fillWidth: true
}
QQC2.ToolButton {
Button {
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
display: AbstractButton.IconOnly
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: attachmentPaneLoader.attachmentPath
imagePath: chatBoxHelper.attachmentPath
}
}
onClicked: {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
attachmentPaneLoader.attachmentPath = newPath;
chatBoxHelper.attachmentPath = newPath;
});
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
ToolTip.text: text
ToolTip.visible: hovered
}
QQC2.ToolButton {
Button {
id: cancelAttachmentButton
icon.name: "dialog-close"
text: i18n("Cancel sending Image")
display: QQC2.AbstractButton.IconOnly
onClicked: currentRoom.chatBoxAttachmentPath = "";
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
icon.name: "dialog-cancel"
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: chatBoxHelper.clearAttachment();
ToolTip.text: text
ToolTip.visible: hovered
}
}
background: null

View File

@@ -0,0 +1,420 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
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.neochat 1.0
ToolBar {
id: chatBar
property string replyEventId: ""
property string editEventId: ""
property alias inputFieldText: inputField.text
property alias textField: inputField
property alias emojiPaneOpened: emojiButton.checked
// store each user we autoComplete here, this will be helpful later to generate
// the matrix.to links.
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
property var userAutocompleted: ({})
signal closeAllTriggered()
signal inputFieldForceActiveFocusTriggered()
signal messageSent()
signal pasteImageTriggered()
signal editLastUserMessage()
signal replyPreviousUserMessage()
property alias isCompleting: completionMenu.visible
onInputFieldForceActiveFocusTriggered: {
inputField.forceActiveFocus();
// set the cursor to the end of the text
inputField.cursorPosition = inputField.length;
}
position: ToolBar.Footer
Kirigami.Theme.colorSet: Kirigami.Theme.View
// Using a custom background because some styles like Material
// or Fusion might have ugly colors for a TextArea placed inside
// of a toolbar. ToolBar is otherwise the closest QQC2 component
// to what we want because of the padding and spacing values.
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
contentItem: RowLayout {
spacing: chatBar.spacing
ScrollView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: inputField.implicitHeight
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
+ inputField.topPadding + inputField.bottomPadding
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
FontMetrics {
id: fontMetrics
font: inputField.font
}
TextArea {
id: inputField
focus: true
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
* Make sure there is no background since we are using the ToolBar background.
*
* This could cause a problem if the QQC2 style was designed around TextArea
* background colors being very different from the QPalette::Base color.
* Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles.
*/
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
topPadding: 0
bottomPadding: 0
property real progress: 0
property bool autoAppeared: false
//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…")
verticalAlignment: TextEdit.AlignVCenter
horizontalAlignment: TextEdit.AlignLeft
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
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
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
room: currentRoom ?? null
}
Timer {
id: repeatTimer
interval: 5000
}
function sendMessage(event) {
if (isCompleting && completionMenu.count > 0) {
chatBar.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
currentRoom.sendTypingNotification(false)
chatBar.postMessage()
}
isCompleting = false;
}
Keys.onReturnPressed: { sendMessage(event) }
Keys.onEnterPressed: { sendMessage(event) }
Keys.onEscapePressed: {
closeAllTriggered()
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
replyPreviousUserMessage();
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
editLastUserMessage();
}
}
Keys.onBacktabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomUp();
return;
}
if (!isCompleting) {
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocusReason)
return
}
if (!autoAppeared) {
let decrementedIndex = completionMenu.currentIndex - 1
// Wrap around to the last item
if (decrementedIndex < 0) {
decrementedIndex = Math.max(completionMenu.count - 1, 0) // 0 if count == 0
}
completionMenu.currentIndex = decrementedIndex
} else {
autoAppeared = false;
}
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();
return;
}
if (!isCompleting) {
nextItemInFocusChain().forceActiveFocus(Qt.TabFocusReason);
return;
}
// TODO detect moved cursor
// ignore first time tab was clicked so that user can select
// first emoji/user
if (!autoAppeared) {
let incrementedIndex = completionMenu.currentIndex + 1;
// Wrap around to the first item
if (incrementedIndex > completionMenu.count - 1) {
incrementedIndex = 0
}
completionMenu.currentIndex = incrementedIndex;
} else {
autoAppeared = false;
}
chatBar.complete();
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
currentRoom.sendTypingNotification(true)
}
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
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;
}
completionMenu.completionType = completionInfo.type
if (completionInfo.type === ChatDocumentHandler.User) {
completionMenu.model = currentRoom.getUsers(completionInfo.keyword, 10);
} 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))
}
if (completionMenu.model.length === 0) {
isCompleting = false;
return;
}
if (!isCompleting) {
isCompleting = true
autoAppeared = true;
completionMenu.endPosition = cursorPosition
}
}
}
}
Item {
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
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: AbstractButton.IconOnly
onClicked: {
if (Clipboard.hasImage) {
attachDialog.open()
} else {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) { return }
chatBoxHelper.attachmentPath = path;
})
fileDialog.open()
}
}
ToolTip.text: text
ToolTip.visible: hovered
}
BusyIndicator {
id: uploadingBusySpinner
anchors.fill: parent
visible: running
running: currentRoom && currentRoom.hasFileUploading
}
}
ToolButton {
id: emojiButton
icon.name: "preferences-desktop-emoticons"
text: i18n("Add an Emoji")
display: AbstractButton.IconOnly
checkable: true
ToolTip.text: text
ToolTip.visible: hovered
}
ToolButton {
id: sendButton
icon.name: "document-send"
text: i18n("Send message")
display: AbstractButton.IconOnly
onClicked: {
chatBar.postMessage()
}
ToolTip.text: text
ToolTip.visible: hovered
}
}
Action {
id: pasteAction
shortcut: StandardKey.Paste
onTriggered: {
if (Clipboard.hasImage) {
pasteImageTriggered();
}
activeFocusItem.paste();
}
}
CompletionMenu {
id: completionMenu
width: parent.width
//height: 80 //Math.min(implicitHeight, delegate.implicitHeight * 6)
height: implicitHeight
y: -height - 1
z: 1
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
onCompleteTriggered: {
complete()
isCompleting = false;
}
}
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;
}
function postMessage() {
checkForFancyEffectsReason();
if (chatBoxHelper.hasAttachment) {
// send attachment but don't reset the text
actionsHandler.postMessage("", chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, {}, this.customEmojiModel);
currentRoom.markAllMessagesAsRead();
messageSent();
return;
}
const re = /^s\/([^\/]*)\/([^\/]*)/;
if (Config.allowQuickEdit && re.test(inputField.text)) {
// send edited messages
actionsHandler.postEdit(inputField.text);
} else {
// send normal message
actionsHandler.postMessage(inputField.text.trim(), chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
}
currentRoom.markAllMessagesAsRead();
inputField.clear();
inputField.text = currentRoom ? currentRoom.cachedInput : "";
messageSent()
}
function complete() {
documentHandler.replaceAutoComplete(completionMenu.currentDisplayText);
if (completionMenu.completionType === ChatDocumentHandler.User
&& completionMenu.currentDisplayText.length > 0
&& completionMenu.currentItem.userId.length > 0) {
userAutocompleted[completionMenu.currentDisplayText] = completionMenu.currentItem.userId;
}
}
}

View File

@@ -0,0 +1,283 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.ChatBox 1.0
import NeoChat.Component.Emoji 1.0
Item {
id: root
property alias inputFieldText: chatBar.inputFieldText
signal fancyEffectsReasonFound(string fancyEffect)
signal messageSent()
signal editLastUserMessage()
signal replyPreviousUserMessage()
Kirigami.Theme.colorSet: Kirigami.Theme.View
implicitWidth: {
let w = 0
for(let i = 0; i < visibleChildren.length; ++i) {
w = Math.max(w, Math.ceil(visibleChildren[i].implicitWidth))
}
return w
}
implicitHeight: {
let h = 0
for(let i = 0; i < visibleChildren.length; ++i) {
h += Math.ceil(visibleChildren[i].implicitHeight)
}
return h
}
// For some reason, this is needed to make the height animation work even though
// it used to work and height should be directly affected by implicitHeight
height: implicitHeight
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
Kirigami.Separator {
id: connectionPaneSeparator
visible: connectionPane.visible
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: connectionPane.top
z: 1
}
QQC2.Pane {
id: connectionPane
padding: fontMetrics.lineSpacing * 0.25
FontMetrics {
id: fontMetrics
font: networkLabel.font
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
visible: !Controller.isOnline
width: parent.width
QQC2.Label {
id: networkLabel
text: i18n("NeoChat is offline. Please check your network connection.")
}
anchors.bottom: emojiPickerLoaderSeparator.top
}
Kirigami.Separator {
id: emojiPickerLoaderSeparator
visible: emojiPickerLoader.visible
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: emojiPickerLoader.top
z: 1
}
Loader {
id: emojiPickerLoader
active: visible
visible: chatBar.emojiPaneOpened
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)
}
}
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
Kirigami.Separator {
id: replySeparator
visible: replyPane.visible
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: replyPane.top
}
ReplyPane {
id: replyPane
visible: chatBoxHelper.isReplying || chatBoxHelper.isEditing
user: chatBoxHelper.replyUser
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: attachmentSeparator.top
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
onReplyCancelled: {
root.focusInputField()
}
}
Kirigami.Separator {
id: attachmentSeparator
visible: attachmentPane.visible
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: attachmentPane.top
}
AttachmentPane {
id: attachmentPane
visible: chatBoxHelper.hasAttachment
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: chatBarSeparator.top
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
Kirigami.Separator {
id: chatBarSeparator
visible: chatBar.visible
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: chatBar.top
}
ChatBar {
id: chatBar
visible: currentRoom.canSendEvent("m.room.message")
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: parent.bottom
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
onCloseAllTriggered: closeAll()
onMessageSent: {
closeAll()
checkForFancyEffectsReason()
root.messageSent();
}
onEditLastUserMessage: {
root.editLastUserMessage();
}
onReplyPreviousUserMessage: {
root.replyPreviousUserMessage();
}
}
function checkForFancyEffectsReason() {
if (!Config.showFancyEffects) {
return
}
let text = root.inputFieldText.trim()
if (text.includes('\u{2744}')) {
root.fancyEffectsReasonFound("snowflake")
}
if (text.includes('\u{1F386}')) {
root.fancyEffectsReasonFound("fireworks")
}
if (text.includes('\u{1F387}')) {
root.fancyEffectsReasonFound("fireworks")
}
if (text.includes('\u{1F389}')) {
root.fancyEffectsReasonFound("confetti")
}
if (text.includes('\u{1F38A}')) {
root.fancyEffectsReasonFound("confetti")
}
}
function addText(text) {
root.inputFieldText = inputFieldText + text
}
function insertText(str) {
root.inputFieldText = inputFieldText.substr(0, inputField.cursorPosition) + str + inputFieldText.substr(inputField.cursorPosition)
}
function focusInputField() {
chatBar.inputFieldForceActiveFocusTriggered()
}
Connections {
target: RoomManager
function onCurrentRoomChanged() {
chatBar.userAutocompleted = {};
}
}
Connections {
target: chatBoxHelper
function onShouldClearText() {
root.inputFieldText = "";
}
function onEditing(editContent, editFormatedContent) {
// Set the input field in edit mode
root.inputFieldText = editContent;
// clean autocompletion list
chatBar.userAutocompleted = {};
// Fill autocompletion list with values extracted from message.
// We can't just iterate on every user in the list and try to
// find matching display name since some users have display name
// matching frequent words and this will marks too many words as
// mentions.
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
let match;
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
chatBar.userAutocompleted[match[2]] = match[1];
}
chatBox.forceActiveFocus();
}
}
function closeAll() {
chatBoxHelper.clear();
chatBar.emojiPaneOpened = false;
}
}

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
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
property alias delegate: completionListView.delegate
// Autocomplee text
property string currentDisplayText: currentItem && (currentItem.displayName ?? "")
property int completionType: ChatDocumentHandler.Emoji
property int beginPosition: 0
property int endPosition: 0
signal completeTriggered()
Kirigami.Theme.colorSet: Kirigami.Theme.View
bottomPadding: 0
leftPadding: 0
rightPadding: 0
topPadding: 0
clip: true
onVisibleChanged: if (!visible) {
completionListView.currentIndex = 0;
}
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
contentItem: ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: completionListView
implicitWidth: contentWidth
delegate: {
if (completionType === ChatDocumentHandler.Emoji) {
emojiDelegate
} else if (completionType === ChatDocumentHandler.Command) {
commandDelegate
} else if (completionType === ChatDocumentHandler.User) {
usernameDelegate
}
}
keyNavigationWraps: true
//interactive: Window.window ? contentHeight + control.topPadding + control.bottomPadding > Window.window.height : false
clip: true
currentIndex: control.currentIndex || 0
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
Component {
id: usernameDelegate
Kirigami.BasicListItem {
id: usernameItem
width: ListView.view.width ?? implicitWidth
property string displayName: modelData.displayName
property string userId: modelData.id
leading: Kirigami.Avatar {
implicitHeight: Kirigami.Units.gridUnit
implicitWidth: implicitHeight
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();
}
}
Component {
id: emojiDelegate
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
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
}
}
onClicked: completeTriggered();
}
}
Component {
id: commandDelegate
Kirigami.BasicListItem {
id: commandItem
width: ListView.view.width ?? implicitWidth
text: "<i>" + modelData.parameter.replace("<", "&lt;").replace(">", "&gt;") + "</i> " + modelData.help
property string displayName: modelData.command
leading: Label {
id: commandLabel
Layout.preferredHeight: Kirigami.Units.gridUnit
Layout.preferredWidth: textMetrics.tightBoundingRect.width
text: modelData.command
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
TextMetrics {
id: textMetrics
text: modelData.command
font: commandLabel.font
}
onClicked: completeTriggered();
}
}
}

View File

@@ -4,20 +4,22 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.14 as Kirigami
import org.kde.neochat 1.0
Loader {
id: replyPane
property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
id: root
readonly property bool isEdit: chatBoxHelper.isEditing
property var user: null
property string avatarMediaUrl: user ? "image://mxc/" + user.avatarMediaId : ""
signal replyCancelled()
active: visible
sourceComponent: QQC2.Pane {
sourceComponent: Pane {
id: replyPane
Kirigami.Theme.colorSet: Kirigami.Theme.View
@@ -38,7 +40,7 @@ Loader {
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
Layout.preferredWidth: Layout.preferredHeight
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : ""
source: root.avatarMediaUrl
name: user ? user.displayName : ""
color: user ? user.color : "transparent"
visible: Boolean(user)
@@ -49,14 +51,14 @@ Loader {
Layout.alignment: Qt.AlignCenter
Layout.fillWidth: true
spacing: fontMetrics.leading
QQC2.Label {
Label {
Layout.fillWidth: true
textFormat: Text.StyledText
elide: Text.ElideRight
text: {
let heading = "<b>%1</b>"
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
if (currentRoom.chatBoxEditId.length > 0) {
if (isEdit) {
heading = heading.arg(i18n("Editing message:")) + "<br/>"
} else {
heading = heading.arg(i18n("Replying to %1:", userName))
@@ -65,26 +67,29 @@ Loader {
return heading
}
}
//TODO edit user mentions
QQC2.ScrollView {
ScrollView {
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)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
QQC2.TextArea {
TextArea {
id: textArea
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage)
text: {
const stylesheet = "<style> a{color:"+Kirigami.Theme.linkColor+";}.user-pill{}</style>";
const content = chatBoxHelper.isReplying ? chatBoxHelper.replyEventContent : chatBoxHelper.editContent;
return stylesheet + content;
}
selectByMouse: true
selectByKeyboard: true
readOnly: true
wrapMode: QQC2.Label.Wrap
wrapMode: Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
@@ -94,19 +99,18 @@ Loader {
}
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
}
shortcut: "Escape"
Button {
id: cancelReplyButton
Layout.alignment: avatar.Layout.alignment
icon.name: "dialog-cancel"
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: {
chatBoxHelper.clear();
root.replyCancelled();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
ToolTip.text: text
ToolTip.visible: hovered
}
}

View File

@@ -0,0 +1,7 @@
module NeoChat.Component.ChatBox
ChatBox 1.0 ChatBox.qml
ChatBar 1.0 ChatBar.qml
ReplyPane 1.0 ReplyPane.qml
AttachmentPane 1.0 AttachmentPane.qml
CompletionMenu 1.0 CompletionMenu.qml
EmojiPickerPane 1.0 EmojiPickerPane.qml

View File

@@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2018-2019 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 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
ListView {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + 2 // for the focus line
boundsBehavior: Flickable.DragOverBounds
clip: true
orientation: ListView.Horizontal
model: ListModel {
ListElement { label: "custom"; category: "custom" }
ListElement { label: "⌛️"; category: "history" }
ListElement { label: "😏"; category: "people" }
ListElement { label: "🌲"; category: "nature" }
ListElement { label: "🍛"; category: "food"}
ListElement { label: "🚁"; category: "activity" }
ListElement { label: "🚅"; category: "travel" }
ListElement { label: "💡"; category: "objects" }
ListElement { label: "🔣"; category: "symbols" }
ListElement { label: "🏁"; category: "flags" }
}
delegate: ItemDelegate {
id: del
required property string label
required property string category
width: contentItem.Layout.preferredWidth
height: Kirigami.Units.gridUnit * 2
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
level: del.label === "custom" ? 4 : 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
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 2
visible: emojiCategory === category
color: Kirigami.Theme.focusColor
}
onClicked: emojiCategory = category
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.preferredHeight: 1
}
GridView {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 8
Layout.fillHeight: true
cellWidth: Kirigami.Units.gridUnit * 2
cellHeight: Kirigami.Units.gridUnit * 2
boundsBehavior: Flickable.DragOverBounds
clip: true
model: {
switch (emojiCategory) {
case "custom":
return _picker.customModel
case "history":
return emojiModel.history
case "people":
return emojiModel.people
case "nature":
return emojiModel.nature
case "food":
return emojiModel.food
case "activity":
return emojiModel.activity
case "travel":
return emojiModel.travel
case "objects":
return emojiModel.objects
case "symbols":
return emojiModel.symbols
case "flags":
return emojiModel.flags
}
return null
}
delegate: ItemDelegate {
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
contentItem: Kirigami.Heading {
level: 1
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 { }
}
}
onClicked: {
if (modelData.isCustom) {
chosen(modelData.shortname)
} else {
chosen(modelData.unicode)
}
emojiModel.emojiUsed(modelData)
}
}
ScrollBar.vertical: ScrollBar {}
}
}

View File

@@ -0,0 +1,2 @@
module NeoChat.Component.Emoji
EmojiPicker 1.0 EmojiPicker.qml

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import QtQuick.Particles 2.15
@@ -62,7 +62,7 @@ Item {
}
ImageParticle {
source: "qrc:/confetti.png"
source: "qrc:/imports/NeoChat/Component/confetti.png"
entryEffect: ImageParticle.Scale
rotationVariation: 360
rotationVelocity: 90
@@ -206,7 +206,7 @@ Item {
ImageParticle {
id: fireworksParticleA
system: fireworksSystem
source: "qrc:/glowdot.png"
source: "qrc:/imports/NeoChat/Component/glowdot.png"
alphaVariation: item.isThemeDark ? 0.1 : 0.1
alpha: item.isThemeDark ? 0.5 : 1
groups: ["a"]
@@ -217,7 +217,7 @@ Item {
ImageParticle {
system: fireworksSystem
source: "qrc:/glowdot.png"
source: "qrc:/imports/NeoChat/Component/glowdot.png"
color: item.isThemeDark ? "white" : "gold"
alphaVariation: item.isThemeDark ? 0.1 : 0.1
alpha: item.isThemeDark ? 0.5 : 1
@@ -230,7 +230,7 @@ Item {
ImageParticle {
id: fireworksParticleB
system: fireworksSystem
source: "qrc:/glowdot.png"
source: "qrc:/imports/NeoChat/Component/glowdot.png"
alphaVariation: item.isThemeDark ? 0.1 : 0.1
alpha: item.isThemeDark ? 0.5 : 1
groups: ["b"]

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow {
id: root
property alias source: image.source
property string filename
property string blurhash: ""
property int imageWidth: -1
property int imageHeight: -1
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
title: i18n("Image View - %1", filename)
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
color: Kirigami.Theme.backgroundColor
background: AbstractButton {
onClicked: root.destroy()
}
BusyIndicator {
visible: image.status !== Image.Ready && root.blurhash === ""
anchors.centerIn: parent
running: visible
}
AnimatedImage {
id: image
anchors.centerIn: parent
width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width)
height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height)
fillMode: Image.PreserveAspectFit
Image {
anchors.centerIn: parent
width: image.width
height: image.height
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
visible: root.blurhash !== "" && parent.status !== Image.Ready
}
}
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,15 +5,24 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
property var showContinueButton: false
property var showBackButton: false
property string title: i18n("Loading…")
anchors.centerIn: parent
QQC2.Label {
text: i18n("Please wait. This might take a little while.")
}
QQC2.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: false
}
}

View File

@@ -9,6 +9,7 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: login
@@ -29,7 +30,7 @@ LoginStep {
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if (acceptableInput) {
if(acceptableInput) {
LoginHelper.matrixId = text
}
}
@@ -49,14 +50,14 @@ LoginStep {
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? (LoginHelper.isLoggedIn ? i18n("Already logged in") : 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:/LoginMethod.qml");
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
} else if (LoginHelper.supportsPassword) {
processed("qrc:/Password.qml");
processed("qrc:/imports/NeoChat/Component/Login/Password.qml");
} else {
processed("qrc:/Sso.qml");
processed("qrc:/imports/NeoChat/Component/Login/Sso.qml");
}
}
enabled: LoginHelper.homeserverReachable

View File

@@ -2,11 +2,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
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
import NeoChat.Component.Login 1.0
LoginStep {
id: loginMethod
@@ -15,17 +16,17 @@ LoginStep {
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Password.qml")
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml")
}
QQC2.Button {
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Sso.qml")
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml")
}
}

View File

@@ -2,29 +2,30 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
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
import NeoChat.Component.Login 1.0
LoginStep {
id: loginRegister
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Login.qml")
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml")
}
QQC2.Button {
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Homeserver.qml")
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml")
}
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
/// Step for the login/registration flow
@@ -17,7 +17,7 @@ ColumnLayout {
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
property QQC2.Action action: null
property Action action: null
/// Called when switching to the next step.
signal processed(url nextUrl)

View File

@@ -8,6 +8,7 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: password
@@ -16,7 +17,7 @@ LoginStep {
message: i18n("Enter your password")
showContinueButton: true
showBackButton: true
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/LoginMethod.qml" : "qrc:/Login.qml"
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml"
action: Kirigami.Action {
text: i18nc("@action:button", "Login")
@@ -29,7 +30,7 @@ LoginStep {
Connections {
target: LoginHelper
function onConnected() {
processed("qrc:/Loading.qml")
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
}

View File

@@ -0,0 +1,40 @@
// 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.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: root
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
Kirigami.FormLayout {
Connections {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
}
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.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()
}
}
}

View File

@@ -0,0 +1,7 @@
module NeoChat.Component.Login
Login 1.0 Login.qml
Password 1.0 Password.qml
LoginRegister 1.0 LoginRegister.qml
Loading 1.0 Loading.qml
LoginMethod 1.0 LoginMethod.qml
LoginStep 1.0 LoginStep.qml

View File

@@ -40,11 +40,6 @@ QQC2.Popup {
// we used to be able to expect that the text field wouldn't attempt to
// perform a mini-DDOS attack using signals.
autoAccept: false
/**
* The focus is manged by the popup and we don't want to use the standard
* shortcut as it could block other SearchFields from using it.
*/
focusSequence: ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
Keys.onLeftPressed: cView.decrementCurrentIndex()
@@ -86,8 +81,6 @@ QQC2.Popup {
required property var currentRoom
required property int index
name: currentRoom.displayName
// When an item is hovered set the currentIndex of listview to it so that it is highlighted
onHoveredChanged: {
if (!hovered) {

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtMultimedia 5.15
@@ -13,12 +13,15 @@ import org.kde.neochat 1.0
TimelineContainer {
id: audioDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
onOpenContextMenu: openFileContext(model, audioDelegate)
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
onDownloadedChanged: audio.play()
innerObject: ColumnLayout {
hoverComponent: hoverActions
innerObject: Control {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.contentMaxWidth
@@ -79,47 +82,41 @@ TimelineContainer {
}
]
RowLayout {
QQC2.ToolButton {
id: playButton
contentItem: ColumnLayout {
RowLayout {
ToolButton {
id: playButton
}
Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
QQC2.Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
QQC2.ProgressBar {
id: downloadBar
visible: false
Layout.fillWidth: true
from: 0
to: model.content.info.size
value: model.progressInfo.progress
}
RowLayout {
visible: audio.hasAudio
QQC2.Slider {
ProgressBar {
id: downloadBar
visible: false
Layout.fillWidth: true
from: 0
to: audio.duration
value: audio.position
onMoved: audio.seek(value)
to: model.content.info.size
value: model.progressInfo.progress
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
QQC2.Label {
visible: audioDelegate.contentMaxWidth > Kirigami.Units.gridUnit * 12
Slider {
from: 0
to: audio.duration
value: audio.position
onMoved: audio.seek(value)
}
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
Label {
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
}
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: audio.hasAudio && audioDelegate.contentMaxWidth < Kirigami.Units.gridUnit * 12
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
}
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
@@ -14,8 +14,6 @@ TimelineContainer {
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

View File

@@ -14,69 +14,66 @@ DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: MessageEventModel.State
roleValue: "state"
delegate: StateDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Emote
roleValue: "emote"
delegate: MessageDelegate {
isEmote: true
}
}
DelegateChoice {
roleValue: MessageEventModel.Message
roleValue: "message"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Notice
roleValue: "notice"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Image
roleValue: "image"
delegate: ImageDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Sticker
delegate: ImageDelegate {}
roleValue: "sticker"
delegate: ImageDelegate {
cardBackground: false
}
}
DelegateChoice {
roleValue: MessageEventModel.Audio
roleValue: "audio"
delegate: AudioDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Video
roleValue: "video"
delegate: VideoDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.File
roleValue: "file"
delegate: FileDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Encrypted
roleValue: "encrypted"
delegate: EncryptedDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.ReadMarker
roleValue: "readMarker"
delegate: ReadMarkerDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Poll
delegate: PollDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Other
roleValue: "other"
delegate: Item {}
}
}

View File

@@ -9,18 +9,19 @@ 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: fileDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, fileDelegate)
readonly property bool downloaded: progressInfo && progressInfo.completed
property bool autoOpenFile: false
onDownloadedChanged: if (autoOpenFile) {
openSavedFile();
}
function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
@@ -29,45 +30,30 @@ TimelineContainer {
}
function openSavedFile() {
UrlHelper.openUrl(progressInfo.localPath);
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
}
innerObject: RowLayout {
Layout.maximumWidth: Math.min(fileDelegate.contentMaxWidth, implicitWidth)
Layout.fillWidth: true
Layout.maximumWidth: fileDelegate.contentMaxWidth
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloadedInstant"
when: progressInfo.completed && autoOpenFile
PropertyChanges {
target: openButton
icon.name: "document-open"
onClicked: openSavedFile()
}
PropertyChanges {
target: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
onClicked: saveFileAs()
}
},
State {
name: "downloaded"
when: progressInfo.completed && !autoOpenFile
PropertyChanges {
target: openButton
visible: false
}
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")
onClicked: openSavedFile()
}
},
@@ -75,11 +61,6 @@ TimelineContainer {
name: "downloading"
when: progressInfo.active
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
@@ -87,6 +68,7 @@ TimelineContainer {
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")
onClicked: currentRoom.cancelFileTransfer(eventId)
}
@@ -97,41 +79,37 @@ TimelineContainer {
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
}
}
QQC2.Button {
id: openButton
icon.name: "document-open"
onClicked: {
autoOpenFile = true;
currentRoom.downloadTempFile(eventId);
Layout.fillWidth: true
}
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.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
@@ -140,7 +118,6 @@ TimelineContainer {
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Component {
@@ -148,15 +125,9 @@ TimelineContainer {
FileDialog {
fileMode: FileDialog.SaveFile
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
Config.lastSaveDirectory = folder
Config.save()
if (autoOpenFile) {
UrlHelper.copyTo(progressInfo.localPath, file)
} else {
currentRoom.download(eventId, file);
}
currentRoom.downloadFile(eventId, file)
}
}
}

View File

@@ -2,17 +2,22 @@
// 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 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)
property var content: model.content
@@ -26,35 +31,13 @@ TimelineContainer {
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
innerObject: AnimatedImage {
innerObject: Image {
id: img
property var imageWidth: {
if (imageDelegate.info.w > 0) {
return imageDelegate.info.w;
} else if (sourceSize.width > 0) {
return sourceSize.width;
} else {
return imageDelegate.contentMaxWidth;
}
}
property var imageHeight: {
if (imageDelegate.info.h > 0) {
return imageDelegate.info.h;
} else if (sourceSize.height > 0) {
return sourceSize.height;
} else {
// Default to a 16:9 placeholder
return imageDelegate.contentMaxWidth / 16 * 9;
}
}
Layout.maximumWidth: Math.min(imageDelegate.contentMaxWidth, imageDelegate.maxWidth)
Layout.maximumHeight: Math.min(imageDelegate.contentMaxWidth / imageWidth * imageHeight, imageDelegate.maxWidth / imageWidth * imageHeight)
Layout.preferredWidth: imageWidth
Layout.preferredHeight: imageHeight
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
Image {
@@ -65,9 +48,8 @@ TimelineContainer {
fillMode: Image.PreserveAspectFit
QQC2.ToolTip.text: model.display
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.text: model.display
ToolTip.visible: hoverHandler.hovered
HoverHandler {
id: hoverHandler
@@ -80,7 +62,7 @@ TimelineContainer {
color: "#BB000000"
QQC2.ProgressBar {
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
@@ -105,25 +87,18 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onTapped: {
img.QQC2.ToolTip.hide()
img.paused = true
fullScreenImage.open()
fullScreenImage.createObject(parent, {
filename: eventId,
source: model.mediaUrl,
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h
}).showFullScreen();
}
}
FullScreenImage {
id: fullScreenImage
filename: eventId
source: mediaUrl
blurhash: model.content.info["xyz.amorgan.blurhash"]
imageWidth: content.info.w
imageHeight: content.info.h
modelData: model
onClosed: img.paused = false
}
function downloadAndOpen() {
if (downloaded) {
openSavedFile()

View File

@@ -0,0 +1,27 @@
// 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, parent.selectedText, Controller.plainText(label.textDocument))
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: RichLabel {
id: label
isEmote: messageDelegate.isEmote
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
}
}

View File

@@ -3,21 +3,20 @@
// 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 org.kde.kirigami 2.15 as Kirigami
Flow {
spacing: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Repeater {
model: reaction ?? null
delegate: QQC2.AbstractButton {
delegate: AbstractButton {
width: Math.max(implicitWidth, height)
contentItem: QQC2.Label {
contentItem: Label {
horizontalAlignment: Text.AlignHCenter
text: modelData.reaction + " " + modelData.count
}
@@ -33,6 +32,7 @@ Flow {
border.width: 1
}
checkable: true
checked: modelData.hasLocalUser
@@ -41,8 +41,8 @@ Flow {
hoverEnabled: true
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: {
ToolTip.visible: hovered
ToolTip.text: {
var text = "";
for (var i = 0; i < modelData.authors.length && i < 3; i++) {
@@ -53,7 +53,7 @@ Flow {
text += i18nc("Separate the usernames of users", " and ")
}
}
text += currentRoom.htmlSafeMemberName(modelData.authors[i].id)
text += modelData.authors[i].displayName
}
if (modelData.authors.length > 3) {
text += i18ncp("%1 is the number of other users", " and %1 other", " and %1 others", modelData.authors.length - 3)

View File

@@ -20,17 +20,6 @@ QQC2.ItemDelegate {
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)
property bool isTemporaryHighlighted: false
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
width: delegateMaxWidth
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: Kirigami.Units.largeSpacing
@@ -70,23 +59,58 @@ QQC2.ItemDelegate {
}
background: Kirigami.ShadowedRectangle {
id: readMarkerBackground
color: {
if (readMarkerDelegate.isTemporaryHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
}
opacity: readMarkerDelegate.isTemporaryHighlighted ? 1 : 0.6
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
}
Behavior on color {
ColorAnimation {target: readMarkerBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
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

@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// 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 org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Timeline 1.0
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 + 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; })
}
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
anchors.left: replyLeftBorder.right
anchors.leftMargin: Kirigami.Units.smallSpacing
width: visible ? Kirigami.Units.gridUnit : 0
height: Kirigami.Units.gridUnit
sourceSize.width: width
sourceSize.height: height
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
source: reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : ""
name: reply.author.name || ""
color: reply.author.color
}
QQC2.Label {
id: replyName
anchors {
left: replyAvatar.right
leftMargin: Kirigami.Units.smallSpacing
right: parent.right
rightMargin: Kirigami.Units.smallSpacing
}
text: currentRoom.htmlSafeMemberName(reply.author.id)
color: reply.author.color
elide: Text.ElideRight
}
Loader {
id: loader
anchors.top: replyName.bottom
sourceComponent: {
switch (reply.type) {
case "image":
case "sticker":
return imageComponent;
case "message":
return textComponent;
// TODO support more types
default:
return textComponent;
}
}
Component {
id: textComponent
RichLabel {
id: replyText
textMessage: reply.display
textFormat: Text.RichText
width: Math.min(implicitWidth, contentMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}
}
Component {
id: imageComponent
Image {
readonly property var content: reply.content
readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null)
// 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
source: "image://mxc/" + mediaId
width: contentMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - replyAvatar.width
height: reply.content.info.h / reply.content.info.w * width
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}
}
}
}

View File

@@ -15,36 +15,14 @@ TextEdit {
readonly property var hasSpoiler: /data-mx-spoiler/g
property bool isEmote: false
property bool isReplyLabel: false
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g
property string textMessage: model.display.includes("http")
? model.display.replace(linkRegex, function() {
if (arguments[0].includes("/_matrix/media/r0/download/")) {
return arguments[0];
}
if (arguments[1]) {
return arguments[0];
}
const l = arguments[2];
if ([".", ","].includes(l[l.length-1])) {
const link = l.substring(0, l.length-1);
const leftover = l[l.length-1];
return `<a href="${link}">${link}</a>${leftover}`;
}
return `<a href="${l}">${l}</a>`;
})
: model.display
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
persistentSelection: true
// Work around QTBUG 93281
Component.onCompleted: if (text.includes("<img")) {
Controller.forceRefreshTextDocument(contentLabel.textDocument, contentLabel)
}
Layout.fillWidth: Config.compactLayout
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
text: "<style>
table {
@@ -75,11 +53,9 @@ a{
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited && !contentLabel.isReplyLabel ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
</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

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
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

@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2018-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
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)
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: 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
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
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()
}
}
}

View File

@@ -0,0 +1,308 @@
// 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.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
QQC2.ItemDelegate {
id: timelineContainer
default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed
property bool isEmote: false
property bool cardBackground: true
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
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
width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : 0)
background: null
property Item hoverComponent
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
updateHoverComponent();
}
}
// 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}
}
]
SectionDelegate {
id: sectionDelegate
width: parent.width
anchors.left: avatar.left
anchors.leftMargin: Kirigami.Units.smallSpacing
visible: model.showSection
height: visible ? implicitHeight : 0
}
Kirigami.Avatar {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0
height: width
sourceSize.width: width
sourceSize.height: width
anchors {
top: sectionDelegate.bottom
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : 0
left: parent.left
leftMargin: Kirigami.Units.largeSpacing
}
visible: model.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: model.author.name ?? model.author.displayName
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color
MouseArea {
anchors.fill: parent
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
}
cursorShape: Qt.PointingHandCursor
}
}
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.smallSpacing
rightPadding: Config.compactLayout ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
hoverEnabled: true
anchors {
top: avatar.top
leftMargin: Kirigami.Units.largeSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : 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: 0
Item {
id: rowLayout
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 * 2
Layout.maximumWidth: contentMaxWidth
implicitHeight: visible ? nameLabel.implicitHeight : 0
QQC2.Label {
id: nameLabel
topInset: 0
visible: model.showAuthor && !isEmote
width: Math.min(contentMaxWidth - timeLabel.width, implicitWidth)
text: visible ? author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
}
}
}
QQC2.Label {
id: timeLabel
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
anchors.left: nameLabel.right
visible: model.showAuthor && !isEmote
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
HoverHandler {
id: hoverHandler
}
}
}
Loader {
id: replyLoader
active: model.reply !== undefined
source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml'
visible: active
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
Layout.leftMargin: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing
Connections {
target: replyLoader.item
function onClicked() {
replyClicked(reply.eventId)
}
}
}
}
background: Item {
Rectangle {
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
anchors.fill: parent
}
Kirigami.ShadowedRectangle {
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 (model.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return 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: 1
}
}
}
Loader {
id: loader
anchors {
left: bubble.left
right: parent.right
top: bubble.bottom
topMargin: active && Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
}
height: active ? item.implicitHeight : 0
//Layout.bottomMargin: readMarker ? Kirigami.Units.smallSpacing : 0
active: eventType !== "state" && eventType !== "notice" && reaction != undefined && reaction.length > 0
visible: active
sourceComponent: ReactionDelegate { }
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: timelineContainer.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: timelineContainer.openContextMenu()
}
}

View File

@@ -2,7 +2,7 @@
// 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 QtMultimedia 5.15
import Qt.labs.platform 1.1 as Platform
@@ -10,10 +10,16 @@ import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 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: videoDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
@@ -70,7 +76,7 @@ TimelineContainer {
fillMode: Image.PreserveAspectFit
}
QQC2.Label {
Label {
anchors.centerIn: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
@@ -94,7 +100,7 @@ TimelineContainer {
color: "#BB000000"
QQC2.ProgressBar {
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8

View File

@@ -0,0 +1,14 @@
module NeoChat.Component.Timeline
RichLabel 1.0 RichLabel.qml
TimelineContainer 1.0 TimelineContainer.qml
StateDelegate 1.0 StateDelegate.qml
SectionDelegate 1.0 SectionDelegate.qml
ImageDelegate 1.0 ImageDelegate.qml
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

View File

@@ -6,7 +6,7 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.14 as Kirigami
import org.kde.neochat 1.0
@@ -15,7 +15,7 @@ Loader {
property string labelText: ""
active: visible
sourceComponent: QQC2.Pane {
sourceComponent: Pane {
id: typingPane
leftPadding: Kirigami.Units.largeSpacing
@@ -86,7 +86,7 @@ Loader {
}
}
}
QQC2.Label {
Label {
id: typingLabel
elide: Text.ElideRight
text: root.labelText

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,7 @@
module NeoChat.Component
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

@@ -2,11 +2,12 @@
// 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 org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.OverlaySheet {
id: root
@@ -16,19 +17,19 @@ Kirigami.OverlaySheet {
title: i18n("Create a Room")
contentItem: Kirigami.FormLayout {
QQC2.TextField {
TextField {
id: roomNameField
Kirigami.FormData.label: i18n("Room Name")
onAccepted: roomTopicField.forceActiveFocus();
}
QQC2.TextField {
TextField {
id: roomTopicField
Kirigami.FormData.label: i18n("Room Topic")
onAccepted: okButton.forceActiveFocus();
}
QQC2.Button {
Button {
id: okButton
text: i18nc("@action:button", "Ok")

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Emoji 1.0
QQC2.Popup {
id: root
signal react(string emoji)
modal: true
focus: true
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent
margins: 0
padding: Kirigami.Units.smallSpacing
implicitWidth: Kirigami.Units.gridUnit * 15
implicitHeight: Kirigami.Units.gridUnit * 20
contentItem: EmojiPicker {
onChosen: react(emoji)
}
}

View File

@@ -9,12 +9,11 @@ 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
signal closed()
property var room
property var user
@@ -24,10 +23,9 @@ Kirigami.OverlaySheet {
parent: applicationWindow().overlay
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
title: i18nc("@title:menu Account detail dialog", "Account detail")
@@ -37,8 +35,8 @@ Kirigami.OverlaySheet {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
@@ -48,6 +46,16 @@ Kirigami.OverlaySheet {
name: displayName
source: avatarMediaId ? ("image://mxc/" + avatarMediaId) : ""
color: user.color
MouseArea {
anchors.fill: parent
onClicked: {
if (avatarMediaId) {
fullScreenImage.createObject(parent, {"filename": displayName, "source": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
}
}
}
}
ColumnLayout {
@@ -63,6 +71,12 @@ Kirigami.OverlaySheet {
text: room.htmlSafeMemberName(user.id)
}
QQC2.Label {
Layout.fillWidth: true
text: i18n("Online")
color: Kirigami.Theme.disabledTextColor
}
Kirigami.Heading {
level: 5
text: user.id
@@ -70,9 +84,7 @@ Kirigami.OverlaySheet {
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.MenuSeparator {}
Kirigami.BasicListItem {
visible: user !== room.localUser
@@ -97,19 +109,6 @@ Kirigami.OverlaySheet {
}
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser && room.canSendState("invite") && !room.containsUser(user.id)
action: Kirigami.Action {
enabled: !room.isUserBanned(user.id)
text: i18n("Invite this user")
icon.name: "list-add-user"
onTriggered: {
room.inviteToRoom(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser && room.canSendState("ban") && !room.isUserBanned(user.id)
@@ -118,10 +117,7 @@ Kirigami.OverlaySheet {
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
applicationWindow().pageStack.pushDialogLayer("qrc:/BanSheet.qml", {room: room, userId: user.id}, {
title: i18nc("@title", "Ban User"),
width: Kirigami.Units.gridUnit * 25
})
room.ban(user.id)
root.close()
}
}
@@ -143,15 +139,11 @@ Kirigami.OverlaySheet {
visible: user === room.localUser || room.canSendState("redact")
action: Kirigami.Action {
text: i18n("Remove recent messages by this user")
text: i18n("Delete recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
applicationWindow().pageStack.pushDialogLayer("qrc:/RemoveSheet.qml", {room: root.room, userId: user.id}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
})
root.close()
room.deleteMessagesByUser(user.id)
}
}
}
@@ -166,20 +158,10 @@ Kirigami.OverlaySheet {
}
}
}
Kirigami.BasicListItem {
action: Kirigami.Action {
text: i18n("Copy link")
icon.name: "username-copy"
onTriggered: {
Clipboard.saveText("https://matrix.to/#/" + user.id)
}
}
}
}
Component {
id: fullScreenImage
onSheetOpenChanged: {
if (!sheetOpen) {
closed()
FullScreenImage {}
}
}
}

View File

@@ -0,0 +1,9 @@
module NeoChat.Dialog
UserDetailDialog 1.0 UserDetailDialog.qml
LoginDialog 1.0 LoginDialog.qml
CreateRoomDialog 1.0 CreateRoomDialog.qml
AcceptInvitationDialog 1.0 AcceptInvitationDialog.qml
OpenFileDialog 1.0 OpenFileDialog.qml
ImageClipboardDialog 1.0 ImageClipboardDialog.qml
StartChatDialog 1.0 StartChatDialog.qml
EmojiDialog 1.0 EmojiDialog.qml

View File

@@ -10,6 +10,10 @@ 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 {
@@ -24,7 +28,7 @@ Labs.MenuBar {
text: i18nc("menu", "Configure NeoChat...")
shortcut: StandardKey.Preferences
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {}, {
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml", {}, {
title: i18n("Configure")
})
}
@@ -41,7 +45,7 @@ Labs.MenuBar {
Labs.MenuItem {
text: i18nc("menu", "New Private Chat…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
onTriggered: pushReplaceLayer("qrc:/StartChatPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
}
Labs.MenuItem {
text: i18nc("menu", "New Group…")
@@ -54,7 +58,7 @@ Labs.MenuBar {
}
Labs.MenuItem {
text: i18nc("menu", "Browse Chats…")
onTriggered: pushReplaceLayer("qrc:/JoinRoomPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
}
}
EditMenu {

View File

@@ -3,12 +3,13 @@
// 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 org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Page 1.0
/**
* Context menu when clicking on a room in the room list
@@ -20,40 +21,35 @@ Loader {
Component {
id: regularMenu
QQC2.Menu {
QQC2.MenuItem {
Menu {
MenuItem {
id: newWindow
text: i18n("Open in New Window")
icon.name: "window-new"
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
QQC2.MenuSeparator {
MenuSeparator {
visible: newWindow.visible
}
QQC2.MenuItem {
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
icon.name: room.isFavourite ? "bookmark-remove" : "bookmark-new"
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
QQC2.MenuItem {
MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
icon.name: room.isLowPriority ? "arrow-up" : "arrow-down"
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
QQC2.MenuItem {
MenuItem {
text: i18n("Mark as Read")
icon.name: "checkmark"
onTriggered: room.markAllMessagesAsRead()
}
QQC2.MenuItem {
text: i18nc("@action:inmenu", "Copy Address to Clipboard")
icon.name: "edit-copy"
MenuItem {
text: i18nc("@action:inmenu", "Copy address to clipboard")
onTriggered: if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
@@ -61,66 +57,15 @@ Loader {
}
}
QQC2.Menu {
title: i18n("Notification State")
QQC2.MenuItem {
text: i18n("Follow Global Setting")
icon.name: "globe"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Default
}
}
QQC2.MenuItem {
text: i18nc("As in 'notify for all messages'","All")
icon.name: "notifications"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.All
}
}
QQC2.MenuItem {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'","@Mentions and Keywords")
icon.name: "im-user"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
QQC2.MenuItem {
text: i18nc("As in 'do not notify for any messages'","Off")
icon.name: "notifications-disabled"
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})
}
QQC2.MenuItem {
text: i18n("Room Settings")
icon.name: "configure"
onTriggered: QQC2.ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/Categories.qml', {room: room}, { title: i18n("Room Settings") })
}
MenuSeparator {}
QQC2.MenuSeparator {}
QQC2.MenuItem {
MenuItem {
text: i18n("Leave Room")
icon.name: "go-previous"
onTriggered: RoomManager.leaveRoom(room)
}
@@ -158,7 +103,6 @@ Loader {
Kirigami.Avatar {
id: avatar
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
name: room.displayName
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
@@ -167,9 +111,9 @@ Loader {
level: 5
Layout.fillWidth: true
text: room.displayName
elide: Text.ElideRight
wrapMode: Text.WordWrap
}
QQC2.ToolButton {
ToolButton {
checked: room.isFavourite
checkable: true
icon.name: 'favorite'
@@ -177,35 +121,26 @@ Loader {
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
QQC2.ToolButton {
ToolButton {
icon.name: 'settings-configure'
onClicked: {
QQC2.ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/Categories.qml', {room: room}, { title: i18n("Room Settings") })
drawer.close()
}
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}
}
Kirigami.BasicListItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
icon: room.isLowPriority ? "arrow-up" : "arrow-down"
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")
icon: "checkmark"
onClicked: room.markAllMessagesAsRead()
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
Kirigami.BasicListItem {
text: i18n("Leave Room")
icon: "go-previous"
onClicked: {
RoomManager.leaveRoom(room)
drawer.close()
}
onClicked: RoomManager.leaveRoom(room)
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}

View File

@@ -3,7 +3,7 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15 as Controls
import org.kde.kirigami 2.14 as Kirigami
/**
@@ -55,7 +55,7 @@ Purpose.PurposeAlternativesModel {
icon.name: model.iconName
onTriggered: {
doBeforeSharing();
applicationWindow().pageStack.pushDialogLayer('qrc:/ShareDialog.qml', {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/ShareDialog.qml', {
title: shareAction.tooltip,
index: index,
model: shareAction._instantiator.model

View File

@@ -7,7 +7,7 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as QQC2
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
@@ -23,7 +23,7 @@ Kirigami.Page {
property alias index: jobView.index
property alias model: jobView.model
QQC2.Action {
Controls.Action {
shortcut: 'Escape'
onTriggered: window.closeDialog()
}

View File

@@ -2,18 +2,18 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.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
import NeoChat.Menu 1.0
MessageDelegateContextMenu {
id: root
signal closeFullscreen
required property var file
required property var progressInfo
required property string mimeType
@@ -41,7 +41,7 @@ MessageDelegateContextMenu {
text: i18n("Save As")
icon.name: "document-save"
onTriggered: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
var dialog = saveAsDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
@@ -50,9 +50,7 @@ MessageDelegateContextMenu {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
currentRoom.chatBoxReplyId = eventId
currentRoom.chatBoxEditId = ""
root.closeFullscreen()
chatBoxHelper.replyToMessage(eventId, message, author);
}
},
Kirigami.Action {
@@ -60,31 +58,20 @@ MessageDelegateContextMenu {
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/RemoveSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
},
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:/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
onTriggered: {
currentRoom.redactEvent(eventId);
}
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: {
applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: root.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
root.closeFullscreen()
}
}
]
@@ -113,13 +100,11 @@ MessageDelegateContextMenu {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
if (!currentFile) {
return;
}
Config.lastSaveDirectory = folder
Config.save()
currentRoom.downloadFile(eventId, currentFile)
}
}

View File

@@ -8,6 +8,7 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Loader {
id: loadRoot
@@ -15,7 +16,7 @@ Loader {
required property var author
required property string message
required property string eventId
property var eventType
property string eventType: ""
property string formattedBody: ""
required property string source
property string selectedText: ""
@@ -27,62 +28,37 @@ Loader {
Kirigami.Action {
text: i18n("Edit")
icon.name: "document-edit"
onTriggered: {
currentRoom.chatBoxEditId = eventId;
currentRoom.chatBoxReplyId = "";
}
visible: author.id === Controller.activeConnection.localUserId && (loadRoot.eventType === MessageEventModel.Emote || loadRoot.eventType === MessageEventModel.Message)
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: {
currentRoom.chatBoxReplyId = eventId;
currentRoom.chatBoxEditId = "";
}
onTriggered: chatBoxHelper.replyToMessage(eventId, message, author);
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/RemoveSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
onTriggered: currentRoom.redactEvent(eventId);
},
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:/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:/MessageSourceSheet.qml', {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: loadRoot.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
}
},
Kirigami.Action {
text: i18n("Copy Link")
icon.name: "edit-copy"
onTriggered: {
Clipboard.saveText("https://matrix.to/#/" + currentRoom.id + "/" + loadRoot.eventId)
}
}
]
@@ -150,7 +126,6 @@ Loader {
QQC2.MenuItem {
text: i18n("Configure Web Shortcuts...")
icon.name: "configure"
visible: !Controller.isFlatpak
onTriggered: webshortcutmodel.configureWebShortcuts()
}
}

View File

@@ -3,7 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import org.kde.syntaxhighlighting 1.0
import org.kde.kirigami 2.15 as Kirigami
@@ -17,16 +17,16 @@ Kirigami.Page {
rightPadding: 0
bottomPadding: 0
title: i18n("Event Source")
title: i18n("Message Source")
QQC2.ScrollView {
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
QQC2.TextArea {
TextArea {
id: sourceTextArea
text: sourceText
readOnly: true

View File

@@ -0,0 +1,4 @@
module NeoChat.Menu.Timeline
MessageDelegateContextMenu 1.0 MessageDelegateContextMenu.qml
FileDelegateContextMenu 1.0 FileDelegateContextMenu.qml
MessageSourceSheet 1.0 MessageSourceSheet.qml

View File

@@ -0,0 +1,6 @@
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

@@ -2,7 +2,7 @@
// 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 org.kde.kirigami 2.15 as Kirigami
@@ -36,7 +36,7 @@ Kirigami.ScrollablePage {
onAccepted: userDictListModel.search()
}
QQC2.Button {
Button {
visible: identifierField.isUserID
text: i18n("Add")
@@ -83,8 +83,8 @@ Kirigami.ScrollablePage {
Layout.preferredWidth: height
Layout.fillHeight: true
source: model.avatar ? ("image://mxc/" + model.avatar) : ""
name: model.name
source: avatar ? ("image://mxc/" + avatar) : ""
name: name
}
ColumnLayout {
@@ -104,7 +104,7 @@ Kirigami.ScrollablePage {
wrapMode: Text.NoWrap
}
QQC2.Label {
Label {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -115,7 +115,7 @@ Kirigami.ScrollablePage {
}
}
QQC2.ToolButton {
ToolButton {
visible: !inRoom
icon.name: "document-send"
text: i18n("Send invitation")

View File

@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 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
Kirigami.ScrollablePage {
id: root
property var connection
property alias keyword: identifierField.text
property string server
title: i18n("Explore Rooms")
Component.onCompleted: identifierField.forceActiveFocus()
header: Control {
padding: Kirigami.Units.largeSpacing
contentItem: RowLayout {
Kirigami.SearchField {
id: identifierField
property bool isRoomAlias: text.match(/#(.+):(.+)/g)
property var room: isRoomAlias ? connection.roomByAlias(text) : null
property bool isJoined: room != null
Layout.fillWidth: true
placeholderText: i18n("Find a room...")
}
Button {
id: joinButton
visible: identifierField.isRoomAlias
text: identifierField.isJoined ? i18n("View") : i18n("Join")
highlighted: true
onClicked: {
if (!identifierField.isJoined) {
Controller.joinRoom(identifierField.text);
// When joining the room, the room will be opened
}
applicationWindow().pageStack.layers.pop();
}
}
ComboBox {
Layout.maximumWidth: 120
id: serverField
editable: currentIndex == 1
model: [i18n("Local"), i18n("Global"), "matrix.org"]
onCurrentIndexChanged: {
if (currentIndex == 0) {
server = ""
} else if (currentIndex == 2) {
server = "matrix.org"
}
}
Keys.onReturnPressed: {
if (currentIndex == 1) {
server = editText
}
}
}
}
}
ListView {
id: publicRoomsListView
clip: true
model: PublicRoomListModel {
id: publicRoomListModel
connection: root.connection
server: root.server
keyword: root.keyword
}
onContentYChanged: {
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
publicRoomListModel.next();
}
delegate: Kirigami.AbstractListItem {
property bool justJoined: false
width: publicRoomsListView.width
onClicked: {
if (!isJoined) {
Controller.joinRoom(roomID)
justJoined = true;
} else {
RoomManager.enterRoom(connection.room(roomID))
}
applicationWindow().pageStack.layers.pop();
}
contentItem: RowLayout {
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
source: model.avatar ? ("image://mxc/" + model.avatar) : ""
name: name
}
ColumnLayout {
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
Kirigami.Heading {
Layout.fillWidth: true
level: 4
text: name
font.bold: true
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
visible: isJoined || justJoined
text: i18n("Joined")
color: Kirigami.Theme.linkColor
}
}
Label {
Layout.fillWidth: true
visible: text
text: topic ? topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
RowLayout {
Layout.fillWidth: true
Kirigami.Icon {
source: "user"
color: Kirigami.Theme.disabledTextColor
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
}
Label {
text: memberCount + " " + (alias ?? roomID)
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
}
}
}
}

View File

@@ -3,11 +3,18 @@
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 {
title: i18n("Loading…")
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
text: i18n("Loading…")
QQC2.BusyIndicator {
running: false
Layout.alignment: Qt.AlignHCenter
}
}
}

View File

@@ -11,24 +11,27 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Menu 1.0
Kirigami.ScrollablePage {
id: page
header: ColumnLayout {
visible: !page.collapsedMode
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
ListView {
id: spaceList
property string activeSpaceId: ""
property string activeSpaceId: ''
orientation: Qt.Horizontal
spacing: Kirigami.Units.smallSpacing
clip: true
spacing: Kirigami.Units.largeSpacing
clip:true
visible: spaceList.count > 0
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.fillWidth: true
model: SortFilterSpaceListModel {
@@ -39,86 +42,76 @@ Kirigami.ScrollablePage {
}
}
header: QQC2.ItemDelegate {
id: homeButton
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
height: parent.height
width: height
leftPadding: topPadding
rightPadding: topPadding
contentItem: Kirigami.Icon {
source: "home"
Connections {
target: SpaceHierarchyCache
function onSpaceHierarchyChanged() {
if (spaceList.activeSpaceId !== '') {
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(spaceList.activeSpaceId, false);
}
}
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip.text: homeButton.text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
delegate: QQC2.ItemDelegate {
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.activeSpaceRooms = [];
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
height: parent.height
width: height
leftPadding: topPadding
rightPadding: topPadding
implicitWidth: ListView.view.headerItem.implicitWidth
implicitHeight: ListView.view.headerItem.implicitHeight
contentItem: Kirigami.Avatar {
name: currentRoom.displayName
id: del
actions.main: Kirigami.Action {
id: enterSpaceAction
onTriggered: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(id, true);
}
}
QQC2.ToolTip {
text: currentRoom.displayName
}
source: avatar !== "" ? "image://mxc/" + avatar : ""
}
onClicked: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceId = id;
}
Accessible.name: currentRoom.displayName
QQC2.ToolTip.text: currentRoom.displayName
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onPressAndHold: {
spaceList.createContextMenu(currentRoom)
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: spaceList.createContextMenu(currentRoom)
}
}
function createContextMenu(room) {
const menu = spaceListContextMenu.createObject(page, {room: room})
menu.open()
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Component {
id: spaceListContextMenu
SpaceListContextMenu {}
}
}
id: page
title: i18n("Rooms")
property var enteredRoom
property bool collapsedMode: Config.roomListPageWidth < applicationWindow().minPageWidth && applicationWindow().shouldUseSidebars
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
verticalScrollBarPolicy: collapsedMode ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
onCollapsedModeChanged: if (collapsedMode) {
sortFilterRoomListModel.filterText = "";
@@ -168,7 +161,7 @@ Kirigami.ScrollablePage {
id: listView
activeFocusOnTab: true
clip: AccountRegistry.count > 1
clip: accountList.count > 1
header: QQC2.ItemDelegate {
visible: page.collapsedMode
@@ -180,7 +173,7 @@ Kirigami.ScrollablePage {
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: visible ? ListView.view.width : 0
width: visible ? page.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
Kirigami.Icon {
@@ -205,7 +198,7 @@ Kirigami.ScrollablePage {
helpfulAction: Kirigami.Action {
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", {
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {
connection: Controller.activeConnection,
keyword: sortFilterRoomListModel.filterText
})
@@ -234,43 +227,27 @@ Kirigami.ScrollablePage {
}
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
section.delegate: page.collapsedMode ? foldButton : sectionHeader
Component {
section.delegate: Kirigami.ListSectionHeader {
id: sectionHeader
Kirigami.ListSectionHeader {
height: implicitHeight
label: roomListModel.categoryName(section)
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
contentItem.children: QQC2.ToolButton {
icon.name: (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
icon.width: Kirigami.Units.iconSizes.small
icon.height: Kirigami.Units.iconSizes.small
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
height: implicitHeight
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
}
Component {
id: foldButton
Item {
width: ListView.view.width
height: visible ? width : 0
QQC2.ToolButton {
id: button
anchors.centerIn: parent
icon.name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section)
icon.width: Kirigami.Units.iconSizes.smallMedium
icon.height: Kirigami.Units.iconSizes.smallMedium
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
QQC2.ToolTip.text: roomListModel.categoryName(section)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
contentItem: RowLayout {
implicitHeight: categoryName.implicitHeight
Kirigami.Heading {
id: categoryName
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")
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignHCenter
}
}
}
@@ -297,8 +274,7 @@ Kirigami.ScrollablePage {
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: ListView.view.width
height: visible ? ListView.view.width : 0
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
height: ListView.view.width
contentItem: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
@@ -342,7 +318,6 @@ Kirigami.ScrollablePage {
labelItem.textFormat: Text.PlainText
subtitle: subtitleText
subtitleItem.textFormat: Text.PlainText
subtitleItem.visible: !Config.compactRoomList
onPressAndHold: {
createRoomListContextMenu()
}
@@ -356,29 +331,18 @@ Kirigami.ScrollablePage {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInRoomDrawer
visible: Config.showAvatarInTimeline
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
trailing: RowLayout {
Kirigami.Icon {
source: "notifications-disabled"
enabled: false
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && unreadCount <= 0
Accessible.name: i18n("Muted room")
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
padding: Kirigami.Units.smallSpacing
color: Kirigami.Theme.textColor
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
Layout.minimumWidth: height
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
@@ -387,15 +351,10 @@ Kirigami.ScrollablePage {
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
visible: roomListItem.hovered
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
@@ -409,14 +368,12 @@ Kirigami.ScrollablePage {
}
function createRoomListContextMenu() {
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
if (!Kirigami.Settings.isMobile && !Config.compactRoomList) {
configButton.visible = true
configButton.down = true
}
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 && !Config.compactRoomList })
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.open()
}
@@ -424,8 +381,28 @@ Kirigami.ScrollablePage {
}
}
footer: UserInfo {
width: parent.width
visible: !page.collapsedMode
footer: QQC2.ToolBar {
visible: accountList.count > 1 && !collapsedMode
height: visible ? implicitHeight : 0
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: RowLayout {
spacing: 0
Repeater {
id: accountList
model: AccountRegistry
delegate: Kirigami.BasicListItem {
checkable: true
checked: Controller.activeConnection && Controller.activeConnection.localUserId === model.connection.localUserId
onClicked: Controller.activeConnection = model.connection
Layout.fillWidth: true
Layout.fillHeight: true
text: model.connection.localUserId
subtitle: model.connection.localUser.accountLabel
}
}
}
}
}

View File

@@ -8,10 +8,15 @@ 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
import NeoChat.Component 1.0
import NeoChat.Component.ChatBox 1.0
import NeoChat.Component.Timeline 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Kirigami.ScrollablePage {
id: page
@@ -26,7 +31,7 @@ Kirigami.ScrollablePage {
/// cancel implementation.
property bool disableCancelShortcut: false
title: currentRoom.displayName
title: currentRoom.htmlSafeDisplayName
KeyNavigation.left: pageStack.get(0)
@@ -47,9 +52,8 @@ Kirigami.ScrollablePage {
signal switchRoomDown()
onCurrentRoomChanged: {
applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false;
chatBoxHelper.clearEditReply()
}
Connections {
@@ -74,6 +78,11 @@ Kirigami.ScrollablePage {
ActionsHandler {
id: actionsHandler
room: page.currentRoom
connection: Controller.activeConnection
}
ChatBoxHelper {
id: chatBoxHelper
}
Shortcut {
@@ -92,28 +101,58 @@ Kirigami.ScrollablePage {
}
Connections {
target: currentRoom
target: actionsHandler
function onShowMessage(messageType, message) {
page.header.contentItem.text = message;
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;
page.header.visible = true;
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
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.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
id: invitation
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout {
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: RoomManager.leaveRoom(page.currentRoom);
}
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
currentRoom.acceptInvitation();
}
}
}
}
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
visible: loading
text: i18n("Loading…")
QQC2.BusyIndicator {
running: loadingIndicator.visible
Layout.alignment: Qt.AlignHCenter
}
}
focus: true
@@ -159,45 +198,16 @@ Kirigami.ScrollablePage {
ListView {
id: messageListView
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 needs to be zero or the top sectionLabel overlay will be disrupted.
// This is because itemAt returns null in the spaces.
// All spacing should be handled by the delegates themselves
spacing: 0
// Ensures that the top item is not covered by sectionBanner if the page is scrolled all the way up
// topMargin: sectionBanner.height
spacing: Config.compactLayout ? 1 : Kirigami.Units.smallSpacing
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
// HACK: Needs to be here because the flickable in ScrollablePage gets all mouse events.
Kirigami.PlaceholderMessage {
id: invitation
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout {
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: RoomManager.leaveRoom(page.currentRoom);
}
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
currentRoom.acceptInvitation();
}
}
}
}
model: !isLoaded ? undefined : collapseStateProxyModel
MessageEventModel {
@@ -232,40 +242,6 @@ Kirigami.ScrollablePage {
hasScrolledUpBefore = true;
}
// Not rendered because the sections are part of the TimelineContainer.qml, this is only so that items have the section property available for use by sectionBanner.
// This is due to the fact that the ListView verticalLayout is BottomToTop.
// This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective).
// There is currently no option to put section headings at the bottom in qml.
section.property: "section"
readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined
function sectionBannerIndex() {
let center = messageListView.x + messageListView.width / 2;
let yStart = messageListView.y + messageListView.contentY;
let index = -1
let i = 0
while (index === -1 && i < 100) {
index = messageListView.indexAt(center, yStart + i);
i++;
}
return index
}
footer: SectionDelegate {
id: sectionBanner
anchors.left: parent.left
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0
anchors.right: parent.right
maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0
z: 3
visible: messageListView.sectionBannerItem != undefined && messageListView.sectionBannerItem.ListView.section != ""
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
}
footerPositioning: ListView.OverlayHeader
QQC2.Popup {
anchors.centerIn: parent
@@ -288,10 +264,9 @@ Kirigami.ScrollablePage {
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) {
return;
}
currentRoom.chatBoxAttachmentPath = path;
if (!path) return
chatBoxHelper.attachmentPath = path;
})
fileDialog.open()
@@ -313,7 +288,7 @@ Kirigami.ScrollablePage {
if (!Clipboard.saveImage(localPath)) {
return;
}
currentRoom.chatBoxAttachmentPath = localPath;
chatBoxHelper.attachmentPath = localPath;
attachDialog.close();
}
}
@@ -345,11 +320,9 @@ Kirigami.ScrollablePage {
id: goReadMarkerFab
z: 2
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action {
onTriggered: {
chatBox.focusInputField();
messageListView.goToEvent(currentRoom.readMarkerEventId)
}
icon.name: "go-up"
@@ -362,18 +335,16 @@ Kirigami.ScrollablePage {
QQC2.RoundButton {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.largeSpacing
anchors.bottomMargin: Kirigami.Units.largeSpacing + messageListView.headerItem.height
anchors.rightMargin: Kirigami.Units.largeSpacing
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
id: goMarkAsReadFab
z: 2
visible: !messageListView.atYEnd
action: Kirigami.Action {
onTriggered: {
chatBox.focusInputField();
goToLastMessage();
currentRoom.markAllMessagesAsRead();
}
@@ -392,8 +363,7 @@ Kirigami.ScrollablePage {
DropArea {
id: dropAreaFile
anchors.fill: parent
onDropped: currentRoom.chatBoxAttachmentPath = drop.urls[0];
enabled: !Controller.isFlatpak
onDropped: chatBoxHelper.attachmentPath = drop.urls[0]
}
QQC2.Pane {
@@ -434,7 +404,7 @@ Kirigami.ScrollablePage {
UserDetailDialog {}
}
TypingPane {
header: TypingPane {
id: typingPane
visible: !loadingIndicator.visible && currentRoom && currentRoom.usersTyping.length > 0
labelText: visible ? i18ncp(
@@ -443,7 +413,6 @@ Kirigami.ScrollablePage {
currentRoom.usersTyping.map(user => user.displayName).join(", ")
) : ""
anchors.left: parent.left
anchors.bottom: parent.bottom
height: visible ? implicitHeight : 0
Behavior on height {
NumberAnimation {
@@ -454,23 +423,21 @@ Kirigami.ScrollablePage {
}
z: 2
}
headerPositioning: ListView.OverlayHeader
function goToEvent(eventID) {
const index = eventToIndex(eventID)
messageListView.positionViewAtIndex(index, ListView.Center)
itemAtIndex(index).isTemporaryHighlighted = true
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
Item {
id: hoverActions
property var event: null
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
property bool showEdit: event && (userMsg && (event.eventType === MessageEventModel.Emote || event.eventType === MessageEventModel.Message))
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
property var updateFunction
onVisibleDelayedChanged: if (visibleDelayed) {
visible = true;
} else {
@@ -485,11 +452,12 @@ Kirigami.ScrollablePage {
}
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: 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 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
@@ -501,29 +469,15 @@ Kirigami.ScrollablePage {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
Kirigami.Icon {
source: "security-high"
width: height
height: parent.height
visible: hoverActions.event ? hoverActions.event.verified : false
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
showQuickReaction: true
onChosen: {
onReact: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.focusInputField();
}
@@ -532,23 +486,21 @@ Kirigami.ScrollablePage {
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: {
currentRoom.chatBoxEditId = hoverActions.event.eventId;
currentRoom.chatBoxReplyId = "";
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: {
currentRoom.chatBoxReplyId = hoverActions.event.eventId;
currentRoom.chatBoxEditId = "";
chatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
chatBox.focusInputField();
}
}
@@ -560,12 +512,24 @@ Kirigami.ScrollablePage {
footer: ChatBox {
id: chatBox
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
width: parent.width
onMessageSent: {
if (!messageListView.atYEnd) {
goToLastMessage();
}
}
onEditLastUserMessage: {
const targetMessage = messageEventModel.getLastLocalUserMessageEventId();
if (targetMessage) {
chatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], 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"]);
}
}
}
background: FancyEffectsContainer {
@@ -596,8 +560,8 @@ Kirigami.ScrollablePage {
Connections {
enabled: Config.showFancyEffects
target: actionsHandler
function onShowEffect(fancyEffect) {
target: chatBox
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
@@ -607,7 +571,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) {
@@ -634,7 +598,7 @@ Kirigami.ScrollablePage {
let center = messageListView.x + messageListView.width / 2;
let index = -1
let i = 0
while (index === -1 && i < 100) {
while(index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i);
i++;
}
@@ -645,7 +609,7 @@ Kirigami.ScrollablePage {
let center = messageListView.x + messageListView.width / 2;
let index = -1
let i = 0
while (index === -1 && i < 100) {
while(index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i);
i++
}
@@ -655,7 +619,7 @@ Kirigami.ScrollablePage {
// 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() && messageListView.atYEnd) {
if (readMarkerRow > 0 && readMarkerRow < firstVisibleIndex()) {
currentRoom.markAllMessagesAsRead()
}
}

View File

@@ -22,26 +22,4 @@ Kirigami.ApplicationWindow {
currentRoom: window.currentRoom
disableCancelShortcut: true
}
onCurrentRoomChanged: if (!currentRoom) {
window.close()
}
property Item hoverLinkIndicator: QQC2.Control {
parent: overlay.parent
property string text
opacity: linkText.text.length > 0 ? 1 : 0
z: 20
x: 0
y: parent.height - implicitHeight
contentItem: QQC2.Label {
id: linkText
text: parent.text.startsWith("https://matrix.to/") ? "" : parent.text
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
}
}

View File

@@ -3,12 +3,13 @@
// 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 org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.ScrollablePage {
id: root
@@ -17,7 +18,7 @@ Kirigami.ScrollablePage {
title: i18n("Start a Chat")
header: QQC2.Control {
header: Control {
padding: Kirigami.Units.largeSpacing
contentItem: RowLayout {
Kirigami.SearchField {
@@ -32,7 +33,7 @@ Kirigami.ScrollablePage {
onAccepted: userDictListModel.search()
}
QQC2.Button {
Button {
visible: identifierField.isUserID
text: i18n("Chat")
@@ -69,8 +70,8 @@ Kirigami.ScrollablePage {
Layout.preferredWidth: height
Layout.fillHeight: true
source: model.avatar ? ("image://mxc/" + model.avatar) : ""
name: model.name
source: avatar
name: name
}
ColumnLayout {
@@ -89,18 +90,19 @@ Kirigami.ScrollablePage {
wrapMode: Text.NoWrap
}
QQC2.Label {
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: userID
color: Kirigami.Theme.disabledColor
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
QQC2.Button {
Button {
id: joinChatButton
Layout.alignment: Qt.AlignRight
visible: directChats && directChats.length > 0
@@ -112,7 +114,7 @@ Kirigami.ScrollablePage {
}
}
QQC2.Button {
Button {
Layout.alignment: Qt.AlignRight
icon.name: "irc-join-channel"
// We wants to make sure an user can't start more than one

View File

@@ -2,12 +2,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
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
import NeoChat.Component.Login 1.0
Kirigami.ScrollablePage {
id: welcomePage
@@ -16,7 +17,7 @@ Kirigami.ScrollablePage {
title: module.item.title ?? i18n("Welcome")
header: QQC2.Control {
header: Controls.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
@@ -49,7 +50,7 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
QQC2.Label {
Controls.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 25
@@ -59,7 +60,7 @@ Kirigami.ScrollablePage {
Loader {
id: module
Layout.alignment: Qt.AlignHCenter
source: "qrc:/Login.qml"
source: "qrc:/imports/NeoChat/Component/Login/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
@@ -68,7 +69,7 @@ Kirigami.ScrollablePage {
RowLayout {
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
Controls.Button {
text: i18nc("@action:button", "Back")
enabled: welcomePage.currentStep.previousUrl !== ""
@@ -79,7 +80,7 @@ Kirigami.ScrollablePage {
}
}
QQC2.Button {
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton

View File

@@ -0,0 +1,9 @@
module NeoChat.Page
LoadingPage 1.0 LoadingPage.qml
RoomListPage 1.0 RoomListPage.qml
RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml
ImageEditorPage 1.0 ImageEditorPage.qml

View File

@@ -0,0 +1,315 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 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.kitemmodels 1.0
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Kirigami.OverlayDrawer {
id: roomDrawer
readonly property var room: RoomManager.currentRoom
width: modal ? undefined : 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
spacing: 0
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
topPadding: Kirigami.Units.smallSpacing / 2;
bottomPadding: Kirigami.Units.smallSpacing / 2;
rightPadding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing
RowLayout {
anchors.fill: parent
spacing: 0
ToolButton {
icon.name: "list-add-user"
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {room: room})
roomDrawer.close();
}
}
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
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
ToolTip {
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
}
}
ToolButton {
Layout.alignment: Qt.AlignRight
icon.name: 'settings-configure'
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
ToolTip {
text: i18n("Room settings")
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Kirigami.Heading {
text: i18n("Room information")
level: 3
}
RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
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) : ""
}
ColumnLayout {
Layout.fillWidth: true
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")
textFormat: Text.PlainText
}
TextEdit {
Layout.fillWidth: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
readOnly: 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
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
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")
}
}
Pane {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
contentItem: Kirigami.SearchField {
id: userListSearchField
onAccepted: sortedMessageEventModel.filterString = text;
}
}
ScrollView {
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 {
id: sortedMessageEventModel
sourceModel: UserListModel {
room: roomDrawer.room
}
sortRole: "perm"
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
z: 1
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.showAvatarInRoomDrawer
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
text: name
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
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
}
}
action: Kirigami.Action {
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open()
}
}
}
}
}
}
onRoomChanged: {
if (loader.active) {
loader.item.userSearchText = ""
}
if (room == null) {
close()
}
}
Component {
id: userDetailDialog
UserDetailDialog {}
}
}

View File

@@ -0,0 +1,2 @@
module NeoChat.Panel
RoomDrawer 1.0 RoomDrawer.qml

View File

@@ -3,12 +3,12 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
id: root
property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
@@ -30,26 +30,6 @@ Kirigami.CategorizedSettings {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Permissions")
icon.name: "visibility"
page: Qt.resolvedUrl("Permissions.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

@@ -0,0 +1,202 @@
// 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

@@ -0,0 +1,69 @@
// 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
CheckBox {
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
}
CheckBox {
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
}
CheckBox {
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

@@ -2,12 +2,11 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.neochat 1.0
MobileForm.AboutPage {
title: i18nc("@title:window", "About NeoChat")
aboutData: About
Kirigami.AboutPage {
title: i18nc('@title:window', 'About NeoChat')
aboutData: Controller.aboutData
}

View File

@@ -0,0 +1,130 @@
// 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

@@ -0,0 +1,113 @@
// 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

@@ -0,0 +1,256 @@
// 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: "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.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

@@ -6,18 +6,15 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.neochat 1.0
import NeoChat.Settings 1.0
MobileForm.FormComboBoxDelegate {
Layout.fillWidth: true
text: i18n("Color theme")
QQC2.ComboBox {
textRole: "display"
valueRole: "display"
model: ColorSchemer.model
Component.onCompleted: currentIndex = ColorSchemer.indexForScheme(Config.colorScheme);
onCurrentValueChanged: {
onActivated: {
ColorSchemer.apply(currentIndex);
Config.colorScheme = ColorSchemer.nameForIndex(currentIndex);
Config.save();

View File

@@ -0,0 +1,107 @@
// 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
}
anchors.fill: parent
Kirigami.PlaceholderMessage {
visible: parent.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
text: i18n("Loading…")
Controls.BusyIndicator {
running: parent.visible
}
}
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
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

@@ -10,14 +10,21 @@ 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")
title: i18nc('@title:window', 'Custom Emojis')
ListView {
anchors.fill: parent
model: CustomEmojiModel
model: CustomEmojiModel {
id: emojiModel
connection: Controller.activeConnection
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
@@ -44,7 +51,7 @@ Kirigami.ScrollablePage {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: ShimmerGradient { }
gradient: Components.ShimmerGradient { }
}
}
@@ -92,7 +99,7 @@ Kirigami.ScrollablePage {
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
this.fileDialog.chosen.connect((url) => {
CustomEmojiModel.addEmoji(emojiCreator.name, url)
emojiModel.addEmoji(emojiCreator.name, url)
this.fileDialog = null
})
this.fileDialog.onRejected.connect(() => {
@@ -110,7 +117,6 @@ Kirigami.ScrollablePage {
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
nameFilters: [i18n("Images (*.png *.gif *.webp)")]
}
}
}

View File

@@ -0,0 +1,135 @@
// 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()
}
}
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

@@ -3,6 +3,7 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
@@ -18,18 +19,13 @@ Kirigami.CategorizedSettings {
icon.name: "preferences-desktop-theme-global"
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Notifications")
icon.name: "preferences-desktop-notification"
page: Qt.resolvedUrl("GlobalNotificationsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Accounts")
icon.name: "preferences-system-users"
page: Qt.resolvedUrl("AccountsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Custom Emojis")
text: i18n("Custom Emoji")
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},
@@ -37,16 +33,10 @@ Kirigami.CategorizedSettings {
text: i18n("Spell Checking")
iconName: "tools-check-spelling"
page: Qt.resolvedUrl("SonnetConfigPage.qml")
visible: Qt.platform.os !== "android"
},
Kirigami.SettingAction {
text: i18n("Network Proxy")
iconName: "network-connect"
page: Qt.resolvedUrl("NetworkProxyPage.qml")
},
Kirigami.SettingAction {
text: i18n("Devices")
iconName: "computer"
iconName: "network-connect"
page: Qt.resolvedUrl("DevicesPage.qml")
},
Kirigami.SettingAction {

View File

@@ -47,7 +47,7 @@ Kirigami.Page {
dialog.close();
}
}
title: i18nc("@window:title", "Spellchecking")
title: i18nc('@window:title', 'Spellchecking')
QQC2.Dialog {
id: applyDialog

View File

@@ -15,12 +15,11 @@ QQC2.RadioButton {
implicitHeight: contentItem.implicitHeight
property alias innerObject: contentLayout.children
property bool thin
contentItem: ColumnLayout {
Kirigami.ShadowedRectangle {
implicitWidth: implicitHeight * 1.6
implicitHeight: delegate.thin ? Kirigami.Units.gridUnit * 5 : Kirigami.Units.gridUnit * 6
implicitHeight: Kirigami.Units.gridUnit * 6
radius: Kirigami.Units.smallSpacing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View

View File

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

View File

@@ -16,7 +16,6 @@
<name xml:lang="ca-valencia">NeoChat</name>
<name xml:lang="cs">NeoChat</name>
<name xml:lang="de">NeoChat</name>
<name xml:lang="el">NeoChat</name>
<name xml:lang="en-GB">NeoChat</name>
<name xml:lang="es">NeoChat</name>
<name xml:lang="eu">NeoChat</name>
@@ -25,9 +24,7 @@
<name xml:lang="hu">NeoChat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name>
<name xml:lang="ie">NeoChat</name>
<name xml:lang="it">NeoChat</name>
<name xml:lang="ka">NeoChat</name>
<name xml:lang="ko">NeoChat</name>
<name xml:lang="nl">NeoChat</name>
<name xml:lang="nn">NeoChat</name>
@@ -35,7 +32,6 @@
<name xml:lang="pl">NeoChat</name>
<name xml:lang="pt">NeoChat</name>
<name xml:lang="pt-BR">NeoChat</name>
<name xml:lang="ru">NeoChat</name>
<name xml:lang="sk">NeoChat</name>
<name xml:lang="sl">NeoChat</name>
<name xml:lang="sv">NeoChat</name>
@@ -51,7 +47,6 @@
<summary xml:lang="ca-valencia">Un client per a Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
@@ -60,9 +55,7 @@
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
<summary xml:lang="ie">Un cliente de Matrix, li protocol de communication decentralisat</summary>
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldings­protokollen</summary>
@@ -70,7 +63,6 @@
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="ru">Клиент для Matrix — децентрализованного коммуникационного протокола</summary>
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
@@ -85,7 +77,6 @@
<p xml:lang="ca">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="ca-valencia">NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics utilitzant el protocol Matrix.</p>
<p xml:lang="de">NeoChat ist ein Matrix-Client. Er ermöglicht Ihnen das Senden von Textnachrichten, Videos und Audiodateien an Ihre Familie, Kollegen und Freunde unter Verwendung des Matrix-Protokolls.</p>
<p xml:lang="el">Το NeoChat είναι μια εφαρμογή του Matrix. Σας επιτρέπει να στέλνετε μηνύματα κειμένου, βίντεο και ήχο στην οικογένειά σας, σε συναδέλφους και φίλους με το πρωτόκολλο Matrix.</p>
<p xml:lang="en-GB">NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos usando el protocolo Matrix.</p>
<p xml:lang="eu">NeoChat Matrix bezero bat da. Familiari, lankideei eta lagunei testu-mezuak, bideoak eta audio-fitxategiak bidaltzeko aukera ematen du, Matrix protokoloa erabiliz.</p>
@@ -94,15 +85,12 @@
<p xml:lang="hu">A NeoChat egy Matrix kliens. Szöveges üzeneteket, videókat ésaudio fájlokat küldhet családjának, kollégáinak és barátainak a Matrix protokoll használatával.</p>
<p xml:lang="ia">NeoChat es un cliente de Matrix. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante le protocollo de Matrix.</p>
<p xml:lang="id">NeoChat adalah sebuah klien Matrix. Memungkinkan Anda untuk mengirim pesan teks, file video dan audio ke keluarga, kolega dan teman Anda menggunakan protokol Matrix.</p>
<p xml:lang="ie">NeoChat es un cliente de Matrix. On posse usar it por inviar missages textual, video- e audiofiles a su familie, colegos e amics med li protocol Matrix.</p>
<p xml:lang="it">NeoChat è un client Matrix. Ti consente di inviare messaggi di testo, file video e audio a familiari, colleghi e amici utilizzando il protocollo Matrix.</p>
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
<p xml:lang="ko">NeoChat은 Matrix 클라이언트입니다. Matrix 프로토콜을 사용하여 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다.</p>
<p xml:lang="nl">NeoChat is een Matrix-client. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden met het Matrix-protocol.</p>
<p xml:lang="pl">NeoChat jest programem do Matrisa. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół poprzez protokół Matriksa.</p>
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix.</p>
<p xml:lang="pt-BR">O NeoChat é um cliente Matrix. Ele permite a você enviar mensagens de texto, arquivos de vídeo e áudio para seus familiares, colegas e amigos usando o protocolo Matrix.</p>
<p xml:lang="ru">NeoChat — это клиент, поддерживающий работу с протоколом Matrix. Он позволяет отправлять текстовые сообщения, видео и аудиофайлы.</p>
<p xml:lang="sk">NeoChat je Matrix klient. Umožňuje vám posielať textové správy, videá a zvukové súbory rodine, kolegom a priateľom pomocou protokolu Matrix.</p>
<p xml:lang="sl">NeoChat je odjemalec Matrixa. Dovoljuje vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, kolegom in prijateljem z uporabo protokola Matrix.</p>
<p xml:lang="sv">NeoChat är en Matrix-klient. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner med användning av Matrix-protokollet.</p>
@@ -116,7 +104,6 @@
<p xml:lang="ca">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="de">Matrix ist ein dezentralisiertes Kommunikationsprotokoll, das dem Benutzer wieder die Kontrolle zurückgibt. Derzeit implementiert NeoChat einen großen Teil des Protokolls mit der Ausnahme von verschlüsselten Chats und Video-Chat.</p>
<p xml:lang="el">Το Matrix είναι ένα αποκεντρωμένο πρωτόκολλο επικοινωνίας, δίνοντας πίσω στον χρήστη τον έλεγχο. Προς το παρόν το NeoChat υλοποιεί ένα μεγάλο μέρος του πρωτοκόλλου με εξαίρεση τις κρυπτογραφημένες συνομιλίες και τη συνομιλία με βίντεο.</p>
<p xml:lang="en-GB">Matrix is a decentralised communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="es">Matrix es un protocolo de comunicaciones descentralizado, que devuelve el control al usuario. En la actualidad, NeoChat implementa gran parte del protocolo con la excepción de chats cifrados y chats de vídeo.</p>
<p xml:lang="eu">Matrix komunikazio-protokolo deszentralizatu bat da, erabiltzaileari kontrola itzultzen diona. Gaur egun, NeoChat-ek protokoloaren zati handi bat inplementatzen du, berriketa zifratuak eta bideo berriketak izan ezik.</p>
@@ -125,15 +112,12 @@
<p xml:lang="hu">A Matrix egy decentralizált kommunikációs protokoll, amely a felhasználók kezébe adja az irányítást.</p>
<p xml:lang="ia">Matrix es un protocollo de communication decentrate, ponente le usator in le controlo. Currentemente NeoChat implementa un grande parte del protocollo con le exception de conversationes cryptate e conversationes video.</p>
<p xml:lang="id">Matrix adalah protokol komunikasi terdesentralisasi, menempatkan pengguna kembali dalam kendali. Saat ini NeoChat mengimplementasikan sebagian besar protokol dengan pengecualian obrolan terenkripsi dan obrolan video.</p>
<p xml:lang="ie">Matrix es un decentralisat protocol de communication quel retroda li control a un usator. Actualmen, NeoChat implementa un grand parte del protocol except ciffrat conversationes e videotelefonada.</p>
<p xml:lang="it">Matrix è un protocollo di comunicazione decentralizzato, che restituisce all'utente il controllo. Attualmente NeoChat implementa gran parte del protocollo ad eccezione delle chat cifrate e delle chat video.</p>
<p xml:lang="ka">Matrix -ი დეცენტრალიზებული კომუნიკაციის პროტოკოლია, რომელიც მომხმარებელს კონტროლის უფლებას უბრუნებს. ამჟამად NeoChat-ს პროტოკოლის ძირითადი ნაწილის მხარდაჭერა გააჩნია, დაშიფვრისა და ვიდეოჩატის გამოკლებით.</p>
<p xml:lang="ko">Matrix는 사용자에게 제어권을 돌려 주는 분산 통신 프로토콜입니다. NeoChat은 암호화된 대화 및 영상 통화를 제외한 프로토콜의 대부분 기능을 구현합니다.</p>
<p xml:lang="nl">Matrix is een gedecentraliseerd communicatieprotocol, dat de gebruiker de controle teruggeeft. Op dit moment implementeert NeoChat grote delen van het protocol met de uitzondering van versleutelde chats en video-chat.</p>
<p xml:lang="pl">Matrix jest protokołem rozproszonego porozumiewania się oddający użytkownikowi jego władzę. Obecnie NeoChat obsługuje dużą część protokołu poza szyfrowanymi rozmowami tekstowymi i z obrazem.</p>
<p xml:lang="pt">O Matrix é um protocolo de comunicações descentralizado, colocando de novo o utilizador no poder. De momento, o NeoChat implementa uma boa parte do protocolo, com a excepção das conversas encriptadas e as conversas de vídeo.</p>
<p xml:lang="pt-BR">O Matrix é um protocolo de comunicação descentralizado, colocando o usuário de volta no controle. Atualmente o NeoChat implementa grande parte do protocolo com a exceção de bate-papos criptografados e bate-papo por vídeo.</p>
<p xml:lang="ru">Matrix — это децентрализованный коммуникационный протокол, возвращающий пользователю контроль над своими данными. В настоящее время в приложении NeoChat реализована поддержка большей части протокола, за исключением зашифрованных чатов и видеочата.</p>
<p xml:lang="sk">Matrix je decentralizovaný komunikačný protokol, ktorý používateľovi vracia kontrolu. V súčasnosti NeoChat implementuje veľkú časť protokolu s výnimkou šifrovaných chatov a videohovorov.</p>
<p xml:lang="sl">Matrix je decentraliziran komunikacijski protokol, kjer ima uporabnik uporabnik kontrolo rabe. Trenutno ima NeoChat izveden velik del protokola z izjemo šifriranih klepetov in video klepetov.</p>
<p xml:lang="sv">Matrix är ett decentraliserat kommunikationsprotokoll, som ger tillbaka kontrollen till användaren. För närvarande implementerar NeoChat en stor del av protokollet, med undantag för krypterad chatt och videochatt.</p>
@@ -146,8 +130,7 @@
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">NeoChat funciona en els mòbils i en l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="de">NeoChat funktioniert sowohl auf Mobilgeräten als auch auf dem PC und bietet ein einheitliches Benutzererlebnis.</p>
<p xml:lang="el">Το NeoChat λειτουργεί και στα κινητά και στους υπολογιστές γραφείου παρέχοντας μια αδιάλειπτη εμπειρία χρήσης.</p>
<p xml:lang="de">NeoChat funktioniert sowohl auf dem Mobiltelefon als auch auf dem Arbeitsfläche und bietet ein einheitliches Benutzererlebnis. </p>
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
<p xml:lang="eu">NeoChat mugikorretan eta mahaigainean dabil, erabiltzaile esperientzia koherentea eskainiz.</p>
@@ -156,15 +139,12 @@
<p xml:lang="hu">A NeoChat mobilon és asztali számítógépen is működik, egységes felhasználói élményt nyújtva.</p>
<p xml:lang="ia">NeoChat functiona sia sur mobile que ur scriptorio durante que forni un experientia de usator consistente.</p>
<p xml:lang="id">NeoChat berfungsi baik di ponsel dan desktop sambil memberikan pengalaman pengguna yang konsisten.</p>
<p xml:lang="ie">NeoChat supporta amb computatores e aparates mobil, ancor providente un consistent experientie de usator.</p>
<p xml:lang="it">NeoChat funziona sia su dispositivi mobili che desktop, fornendo un'esperienza utente coerente.</p>
<p xml:lang="ka">NeoChat მუშაობს როგორც მობილურ, ისე ჩვეულებრივ კომპიუტერებზე იმ დროს, როცა მომხმარებლის ინტერფეისი არ იცვლება.</p>
<p xml:lang="ko">NeoChat은 모바일과 데스크톱 모두에서 일관된 사용자 경험을 제공합니다.</p>
<p xml:lang="nl">NeoChat werkt zowel op de mobiel en het bureaublad met het leveren van een consistente gebruikerservaring.</p>
<p xml:lang="pl">NeoChat działa zarówno na urządzeniach przenośnych jak i biurkowych, zapewniając spójne wrażenia użytkownika</p>
<p xml:lang="pt">O NeoChat funciona tanto em dispositivos móveis como no computador, fornecendo uma experiência de utilizador consistente.</p>
<p xml:lang="pt-BR">O NeoChat funciona tanto no celular como no computador enquanto fornece uma experiência consistente ao usuário.</p>
<p xml:lang="ru">NeoChat работает как на мобильных устройствах, так и на настольных компьютерах, обеспечивая единый пользовательский интерфейс.</p>
<p xml:lang="sk">NeoChat funguje na mobilných aj stolových počítačoch a poskytuje konzistentný používateľský zážitok.</p>
<p xml:lang="sl">NeoChat deluje tako na mobilnih kot na namiznih platformah z zagotavljanjem konsistentne uporabniške izkušnje.</p>
<p xml:lang="sv">NeoChat fungerar både på mobil och skrivbord och tillhandahåller en konsekvent användarupplevelse.</p>
@@ -185,7 +165,6 @@
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="el">Η Κοινότητα του KDE</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
@@ -194,9 +173,7 @@
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="ie">Li comunité de KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="ka">KDE-ის საზოგადოება</developer_name>
<developer_name xml:lang="ko">KDE 커뮤니티</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
@@ -204,7 +181,6 @@
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">A Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A comunidade KDE</developer_name>
<developer_name xml:lang="ru">Сообщество KDE</developer_name>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
@@ -216,7 +192,6 @@
<project_license>GPL-3.0</project_license>
<custom>
<value key="KDE::matrix">#neochat:kde.org</value>
<value key="KDE::windows_store">https://www.microsoft.com/store/apps/9PNXWVNRC29H</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -231,12 +206,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="22.11" date="2022-11-30">
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
</release>
<release version="22.09" date="2022-09-27">
<url>https://www.plasma-mobile.org/2022/09/27/plasma-mobile-gear-22-09/</url>
</release>
<release version="22.06" date="2022-06-24">
<url>https://www.plasma-mobile.org/2022/06/28/plasma-mobile-gear-22-06/</url>
<description>

View File

@@ -9,7 +9,6 @@ Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
Name[cs]=NeoChat
Name[de]=NeoChat
Name[el]=NeoChat
Name[en_GB]=NeoChat
Name[es]=NeoChat
Name[eu]=NeoChat
@@ -18,9 +17,7 @@ Name[fr]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
Name[ie]=NeoChat
Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[nl]=NeoChat
@@ -30,7 +27,6 @@ Name[pl]=NeoChat
Name[pt]=NeoChat
Name[pt_BR]=NeoChat
Name[ro]=NeoChat
Name[ru]=NeoChat
Name[sk]=NeoChat
Name[sl]=NeoChat
Name[sv]=NeoChat
@@ -46,7 +42,6 @@ GenericName[ca]=Client de Matrix
GenericName[ca@valencia]=Client de Matrix
GenericName[cs]=Klient protokolu Matrix
GenericName[de]=Matrix-Programm
GenericName[el]=Εφαρμογή του Matrix
GenericName[en_GB]=Matrix Client
GenericName[es]=Cliente para Matrix
GenericName[eu]=Matrix bezeroa
@@ -55,9 +50,7 @@ GenericName[fr]=Client « Matrix »
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
GenericName[ie]=Cliente de Matrix
GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programą
GenericName[nl]=Matrix-client
@@ -67,7 +60,6 @@ GenericName[pl]=Program Matriksa
GenericName[pt]=Cliente de Matrix
GenericName[pt_BR]=Cliente Matrix
GenericName[ro]=Client Matrix
GenericName[ru]=Клиент Matrix
GenericName[sk]=Matrix Client
GenericName[sl]=Odjemalec Matrix
GenericName[sv]=Matrix-klient
@@ -82,7 +74,6 @@ Comment[az]=Matrix protokolu üçün müştəri
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
Comment[de]=Programm für das Matrix-Protokoll
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
Comment[en_GB]=Client for the Matrix protocol
Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa
@@ -91,9 +82,7 @@ Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix
Comment[ie]=Un cliente del protocol Matrix
Comment[it]=Client per il protocollo Matrix
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[nl]=Client voor het Matrix-protocol
@@ -103,7 +92,6 @@ Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[ru]=Клиент для протокола Matrix
Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix

File diff suppressed because it is too large Load Diff

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