Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5837a208a | ||
|
|
3b45c0b2f7 | ||
|
|
e1a72816d0 | ||
|
|
aea46c547f | ||
|
|
fe009f764e | ||
|
|
8c14f8d2eb | ||
|
|
559da66197 | ||
|
|
759496620f | ||
|
|
1f7b1ad29f | ||
|
|
9338567137 | ||
|
|
75d2488dbc | ||
|
|
4bed6a07cd | ||
|
|
cb1c9d1869 | ||
|
|
76b2d4ac49 | ||
|
|
5fc931484e | ||
|
|
e60ed3b889 | ||
|
|
1314567fde | ||
|
|
047aa793e7 | ||
|
|
d60f8f4dfa | ||
|
|
20139c08d8 | ||
|
|
fb59bac165 | ||
|
|
5a65dc4d9a | ||
|
|
03ec17366e | ||
|
|
15137cd704 | ||
|
|
2aea0015ba | ||
|
|
f6fd099808 | ||
|
|
04082fc095 | ||
|
|
bc977c3fc6 | ||
|
|
c4c283c85a | ||
|
|
56f49fabf7 | ||
|
|
fe407a3421 | ||
|
|
bbe539885e | ||
|
|
ff978b9586 | ||
|
|
885b75e35f | ||
|
|
cb81eaf26f | ||
|
|
22732b801b | ||
|
|
ae3e395b47 | ||
|
|
96c91e2a35 | ||
|
|
e36204bbd8 | ||
|
|
1804140ac0 | ||
|
|
5a28a93ab6 | ||
|
|
76bd529c3c | ||
|
|
293288a0b6 | ||
|
|
51b6593f96 | ||
|
|
51e73568c4 | ||
|
|
e461e2098b | ||
|
|
79ceb45fae | ||
|
|
0c292b34ff | ||
|
|
db6640ba49 | ||
|
|
3827249f0c | ||
|
|
f40a3daef4 | ||
|
|
8d2608a230 | ||
|
|
3e5628def3 | ||
|
|
3b3673fdff | ||
|
|
d81e4c417d | ||
|
|
44d3f628d9 | ||
|
|
0db9c0454f | ||
|
|
e5c65a662e | ||
|
|
8913aa8a66 | ||
|
|
5db3e14ae6 | ||
|
|
c5a3fc0431 | ||
|
|
fc791d41fa | ||
|
|
127ad19109 | ||
|
|
066ea4f8bd | ||
|
|
cf60337b27 | ||
|
|
98672cf870 | ||
|
|
abd03299ec | ||
|
|
41b64f977c | ||
|
|
ac75dd57c0 | ||
|
|
1d3d61ed77 | ||
|
|
1e047a8ff1 | ||
|
|
530b4c24a0 | ||
|
|
ada7bcef65 | ||
|
|
ad4ca3ad9e | ||
|
|
4103c44eb5 | ||
|
|
dd4ed7539e | ||
|
|
52ad911b2d | ||
|
|
f09dff979e | ||
|
|
0476398f91 | ||
|
|
41993bfe24 | ||
|
|
b3d90ebf82 | ||
|
|
ef0a6e276c | ||
|
|
a104968a29 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,5 +5,3 @@ build
|
||||
neochat.kdev4
|
||||
compile_commands.json
|
||||
.cache/
|
||||
.vscode/
|
||||
kate.project.ctags.*
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# SPDX-FileCopyrightText: none
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-reuse.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
25
.kde-ci.yml
25
.kde-ci.yml
@@ -1,25 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
Dependencies:
|
||||
- 'on': ['@all']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@stable'
|
||||
'frameworks/kcoreaddons': '@stable'
|
||||
'frameworks/kirigami': '@stable'
|
||||
'frameworks/ki18n': '@stable'
|
||||
'frameworks/kconfig': '@stable'
|
||||
'frameworks/syntax-highlighting': '@stable'
|
||||
'frameworks/kitemmodels': '@stable'
|
||||
'frameworks/knotifications': '@stable'
|
||||
'libraries/kquickimageeditor': '@stable'
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@stable'
|
||||
'frameworks/kio': '@stable'
|
||||
'frameworks/kwindowsystem': '@stable'
|
||||
'frameworks/sonnet': '@stable'
|
||||
'frameworks/kconfigwidgets': '@stable'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@stable'
|
||||
29
.reuse/dep5
29
.reuse/dep5
@@ -13,32 +13,3 @@ License: CC0-1.0
|
||||
Files: android/res/drawable/splash.xml
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: */qmldir .gitignore
|
||||
Copyright: None
|
||||
License: CC0-1.0
|
||||
|
||||
Files: .gitlab/issue_templates/bug.md
|
||||
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: res.qrc
|
||||
Copyright: None
|
||||
License: CC0-1.0
|
||||
|
||||
Files: cmake/Flatpak/99-noto-mono-color-emoji.conf
|
||||
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: src/neochatconfig.kcfg
|
||||
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>
|
||||
Copyright: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: neochat.notifyrc
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: imports/NeoChat/Component/confetti.png imports/NeoChat/Component/glowdot.png
|
||||
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
|
||||
License: CC0-1.0
|
||||
|
||||
@@ -4,23 +4,20 @@
|
||||
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
cmake_minimum_required(VERSION 3.1)
|
||||
|
||||
project(NeoChat)
|
||||
set(PROJECT_VERSION "21.12")
|
||||
|
||||
set(KF5_MIN_VERSION "5.88.0")
|
||||
set(QT_MIN_VERSION "5.15.2")
|
||||
set(KF5_MIN_VERSION "5.77.0")
|
||||
set(QT_MIN_VERSION "5.15.0")
|
||||
|
||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
|
||||
|
||||
include(FeatureSummary)
|
||||
include(ECMSetupVersion)
|
||||
include(KDEInstallDirs)
|
||||
@@ -29,8 +26,6 @@ include(KDEClangFormat)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
include(ECMAddAppIcon)
|
||||
include(KDEGitCommitHooks)
|
||||
include(ECMCheckOutboundLicense)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
include(cmake/Flatpak.cmake)
|
||||
@@ -39,7 +34,7 @@ endif()
|
||||
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
|
||||
ecm_setup_version(${PROJECT_VERSION}
|
||||
ecm_setup_version(1.2.0
|
||||
VARIABLE_PREFIX NEOCHAT
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
|
||||
)
|
||||
@@ -73,11 +68,10 @@ if(ANDROID)
|
||||
)
|
||||
else()
|
||||
find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem Sonnet)
|
||||
find_package(KF5QQC2DesktopStyle ${KF5_MIN_VERSION} REQUIRED)
|
||||
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
||||
TYPE RUNTIME
|
||||
)
|
||||
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
@@ -111,14 +105,6 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
PURPOSE "Add image editing capability to image attachments"
|
||||
)
|
||||
|
||||
find_package(QCoro REQUIRED)
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
if(ANDROID)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||
install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
|
||||
@@ -136,7 +122,3 @@ feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAG
|
||||
|
||||
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)
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
|
||||
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Copyright (c) <year> <owner>. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,446 +0,0 @@
|
||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc.
|
||||
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the library GPL. It is numbered 2 because
|
||||
it goes with version 2 of the ordinary GPL.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share
|
||||
and change it. By contrast, the GNU General Public Licenses are intended to
|
||||
guarantee your freedom to share and change free software--to make sure the
|
||||
software is free for all its users.
|
||||
|
||||
This license, the Library General Public License, applies to some specially
|
||||
designated Free Software Foundation software, and to any other libraries whose
|
||||
authors decide to use it. You can use it for your libraries, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our
|
||||
General Public Licenses are designed to make sure that you have the freedom
|
||||
to distribute copies of free software (and charge for this service if you
|
||||
wish), that you receive source code or can get it if you want it, that you
|
||||
can change the software or use pieces of it in new free programs; and that
|
||||
you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone to
|
||||
deny you these rights or to ask you to surrender the rights. These restrictions
|
||||
translate to certain responsibilities for you if you distribute copies of
|
||||
the library, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis or for
|
||||
a fee, you must give the recipients all the rights that we gave you. You must
|
||||
make sure that they, too, receive or can get the source code. If you link
|
||||
a program with the library, you must provide complete object files to the
|
||||
recipients so that they can relink them with the library, after making changes
|
||||
to the library and recompiling it. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Our method of protecting your rights has two steps: (1) copyright the library,
|
||||
and (2) offer you this license which gives you legal permission to copy, distribute
|
||||
and/or modify the library.
|
||||
|
||||
Also, for each distributor's protection, we want to make certain that everyone
|
||||
understands that there is no warranty for this free library. If the library
|
||||
is modified by someone else and passed on, we want its recipients to know
|
||||
that what they have is not the original version, so that any problems introduced
|
||||
by others will not reflect on the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents. We
|
||||
wish to avoid the danger that companies distributing free software will individually
|
||||
obtain patent licenses, thus in effect transforming the program into proprietary
|
||||
software. To prevent this, we have made it clear that any patent must be licensed
|
||||
for everyone's free use or not licensed at all.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the ordinary GNU
|
||||
General Public License, which was designed for utility programs. This license,
|
||||
the GNU Library General Public License, applies to certain designated libraries.
|
||||
This license is quite different from the ordinary one; be sure to read it
|
||||
in full, and don't assume that anything in it is the same as in the ordinary
|
||||
license.
|
||||
|
||||
The reason we have a separate public license for some libraries is that they
|
||||
blur the distinction we usually make between modifying or adding to a program
|
||||
and simply using it. Linking a program with a library, without changing the
|
||||
library, is in some sense simply using the library, and is analogous to running
|
||||
a utility program or application program. However, in a textual and legal
|
||||
sense, the linked executable is a combined work, a derivative of the original
|
||||
library, and the ordinary General Public License treats it as such.
|
||||
|
||||
Because of this blurred distinction, using the ordinary General Public License
|
||||
for libraries did not effectively promote software sharing, because most developers
|
||||
did not use the libraries. We concluded that weaker conditions might promote
|
||||
sharing better.
|
||||
|
||||
However, unrestricted linking of non-free programs would deprive the users
|
||||
of those programs of all benefit from the free status of the libraries themselves.
|
||||
This Library General Public License is intended to permit developers of non-free
|
||||
programs to use free libraries, while preserving your freedom as a user of
|
||||
such programs to change the free libraries that are incorporated in them.
|
||||
(We have not seen how to achieve this as regards changes in header files,
|
||||
but we have achieved it as regards changes in the actual functions of the
|
||||
Library.) The hope is that this will lead to faster development of free libraries.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow. Pay close attention to the difference between a "work based on the
|
||||
library" and a "work that uses the library". The former contains code derived
|
||||
from the library, while the latter only works together with the library.
|
||||
|
||||
Note that it is possible for a library to be covered by the ordinary General
|
||||
Public License rather than by this special one.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library which contains a
|
||||
notice placed by the copyright holder or other authorized party saying it
|
||||
may be distributed under the terms of this Library General Public License
|
||||
(also called "this License"). Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data prepared
|
||||
so as to be conveniently linked with application programs (which use some
|
||||
of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work which has
|
||||
been distributed under these terms. A "work based on the Library" means either
|
||||
the Library or any derivative work under copyright law: that is to say, a
|
||||
work containing the Library or a portion of it, either verbatim or with modifications
|
||||
and/or translated straightforwardly into another language. (Hereinafter, translation
|
||||
is included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for making modifications
|
||||
to it. For a library, complete source code means all the source code for all
|
||||
modules it contains, plus any associated interface definition files, plus
|
||||
the scripts used to control compilation and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not covered
|
||||
by this License; they are outside its scope. The act of running a program
|
||||
using the Library is not restricted, and output from such a program is covered
|
||||
only if its contents constitute a work based on the Library (independent of
|
||||
the use of the Library in a tool for writing it). Whether that is true depends
|
||||
on what the Library does and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's complete source
|
||||
code as you receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice and disclaimer
|
||||
of warranty; keep intact all the notices that refer to this License and to
|
||||
the absence of any warranty; and distribute a copy of this License along with
|
||||
the Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you
|
||||
may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion of it,
|
||||
thus forming a work based on the Library, and copy and distribute such modifications
|
||||
or work under the terms of Section 1 above, provided that you also meet all
|
||||
of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices stating that
|
||||
you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no charge to all
|
||||
third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a table of
|
||||
data to be supplied by an application program that uses the facility, other
|
||||
than as an argument passed when the facility is invoked, then you must make
|
||||
a good faith effort to ensure that, in the event an application does not supply
|
||||
such function or table, the facility still operates, and performs whatever
|
||||
part of its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has a purpose
|
||||
that is entirely well-defined independent of the application. Therefore, Subsection
|
||||
2d requires that any application-supplied function or table used by this function
|
||||
must be optional: if the application does not supply it, the square root function
|
||||
must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable
|
||||
sections of that work are not derived from the Library, and can be reasonably
|
||||
considered independent and separate works in themselves, then this License,
|
||||
and its terms, do not apply to those sections when you distribute them as
|
||||
separate works. But when you distribute the same sections as part of a whole
|
||||
which is a work based on the Library, the distribution of the whole must be
|
||||
on the terms of this License, whose permissions for other licensees extend
|
||||
to the entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your
|
||||
rights to work written entirely by you; rather, the intent is to exercise
|
||||
the right to control the distribution of derivative or collective works based
|
||||
on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library with
|
||||
the Library (or with a work based on the Library) on a volume of a storage
|
||||
or distribution medium does not bring the other work under the scope of this
|
||||
License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public License
|
||||
instead of this License to a given copy of the Library. To do this, you must
|
||||
alter all the notices that refer to this License, so that they refer to the
|
||||
ordinary GNU General Public License, version 2, instead of to this License.
|
||||
(If a newer version than version 2 of the ordinary GNU General Public License
|
||||
has appeared, then you can specify that version instead if you wish.) Do not
|
||||
make any other change in these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for that copy,
|
||||
so the ordinary GNU General Public License applies to all subsequent copies
|
||||
and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of the Library
|
||||
into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or derivative of
|
||||
it, under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you accompany it with the complete corresponding
|
||||
machine-readable source code, which must be distributed under the terms of
|
||||
Sections 1 and 2 above on a medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy from a designated
|
||||
place, then offering equivalent access to copy the source code from the same
|
||||
place satisfies the requirement to distribute the source code, even though
|
||||
third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the Library, but
|
||||
is designed to work with the Library by being compiled or linked with it,
|
||||
is called a "work that uses the Library". Such a work, in isolation, is not
|
||||
a derivative work of the Library, and therefore falls outside the scope of
|
||||
this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library creates an
|
||||
executable that is a derivative of the Library (because it contains portions
|
||||
of the Library), rather than a "work that uses the library". The executable
|
||||
is therefore covered by this License. Section 6 states terms for distribution
|
||||
of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file that
|
||||
is part of the Library, the object code for the work may be a derivative work
|
||||
of the Library even though the source code is not. Whether this is true is
|
||||
especially significant if the work can be linked without the Library, or if
|
||||
the work is itself a library. The threshold for this to be true is not precisely
|
||||
defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data structure layouts
|
||||
and accessors, and small macros and small inline functions (ten lines or less
|
||||
in length), then the use of the object file is unrestricted, regardless of
|
||||
whether it is legally a derivative work. (Executables containing this object
|
||||
code plus portions of the Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may distribute
|
||||
the object code for the work under the terms of Section 6. Any executables
|
||||
containing that work also fall under Section 6, whether or not they are linked
|
||||
directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also compile or link a "work
|
||||
that uses the Library" with the Library to produce a work containing portions
|
||||
of the Library, and distribute that work under terms of your choice, provided
|
||||
that the terms permit modification of the work for the customer's own use
|
||||
and reverse engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the Library
|
||||
is used in it and that the Library and its use are covered by this License.
|
||||
You must supply a copy of this License. If the work during execution displays
|
||||
copyright notices, you must include the copyright notice for the Library among
|
||||
them, as well as a reference directing the user to the copy of this License.
|
||||
Also, you must do one of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding machine-readable source
|
||||
code for the Library including whatever changes were used in the work (which
|
||||
must be distributed under Sections 1 and 2 above); and, if the work is an
|
||||
executable linked with the Library, with the complete machine-readable "work
|
||||
that uses the Library", as object code and/or source code, so that the user
|
||||
can modify the Library and then relink to produce a modified executable containing
|
||||
the modified Library. (It is understood that the user who changes the contents
|
||||
of definitions files in the Library will not necessarily be able to recompile
|
||||
the application to use the modified definitions.)
|
||||
|
||||
b) Accompany the work with a written offer, valid for at least three years,
|
||||
to give the same user the materials specified in Subsection 6a, above, for
|
||||
a charge no more than the cost of performing this distribution.
|
||||
|
||||
c) If distribution of the work is made by offering access to copy from a designated
|
||||
place, offer equivalent access to copy the above specified materials from
|
||||
the same place.
|
||||
|
||||
d) Verify that the user has already received a copy of these materials or
|
||||
that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the Library" must
|
||||
include any data and utility programs needed for reproducing the executable
|
||||
from it. However, as a special exception, the source code distributed need
|
||||
not include anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the operating
|
||||
system on which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license restrictions of
|
||||
other proprietary libraries that do not normally accompany the operating system.
|
||||
Such a contradiction means you cannot use both them and the Library together
|
||||
in an executable that you distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the Library side-by-side
|
||||
in a single library together with other library facilities not covered by
|
||||
this License, and distribute such a combined library, provided that the separate
|
||||
distribution of the work based on the Library and of the other library facilities
|
||||
is otherwise permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based on the
|
||||
Library, uncombined with any other library facilities. This must be distributed
|
||||
under the terms of the Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact that part of
|
||||
it is a work based on the Library, and explaining where to find the accompanying
|
||||
uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute the Library
|
||||
except as expressly provided under this License. Any attempt otherwise to
|
||||
copy, modify, sublicense, link with, or distribute the Library is void, and
|
||||
will automatically terminate your rights under this License. However, parties
|
||||
who have received copies, or rights, from you under this License will not
|
||||
have their licenses terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not signed
|
||||
it. However, nothing else grants you permission to modify or distribute the
|
||||
Library or its derivative works. These actions are prohibited by law if you
|
||||
do not accept this License. Therefore, by modifying or distributing the Library
|
||||
(or any work based on the Library), you indicate your acceptance of this License
|
||||
to do so, and all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the Library),
|
||||
the recipient automatically receives a license from the original licensor
|
||||
to copy, distribute, link with or modify the Library subject to these terms
|
||||
and conditions. You may not impose any further restrictions on the recipients'
|
||||
exercise of the rights granted herein. You are not responsible for enforcing
|
||||
compliance by third parties to this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent infringement
|
||||
or for any other reason (not limited to patent issues), conditions are imposed
|
||||
on you (whether by court order, agreement or otherwise) that contradict the
|
||||
conditions of this License, they do not excuse you from the conditions of
|
||||
this License. If you cannot distribute so as to satisfy simultaneously your
|
||||
obligations under this License and any other pertinent obligations, then as
|
||||
a consequence you may not distribute the Library at all. For example, if a
|
||||
patent license would not permit royalty-free redistribution of the Library
|
||||
by all those who receive copies directly or indirectly through you, then the
|
||||
only way you could satisfy both it and this License would be to refrain entirely
|
||||
from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents
|
||||
or other property right claims or to contest validity of any such claims;
|
||||
this section has the sole purpose of protecting the integrity of the free
|
||||
software distribution system which is implemented by public license practices.
|
||||
Many people have made generous contributions to the wide range of software
|
||||
distributed through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing to
|
||||
distribute software through any other system and a licensee cannot impose
|
||||
that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a
|
||||
consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in certain
|
||||
countries either by patents or by copyrighted interfaces, the original copyright
|
||||
holder who places the Library under this License may add an explicit geographical
|
||||
distribution limitation excluding those countries, so that distribution is
|
||||
permitted only in or among countries not thus excluded. In such case, this
|
||||
License incorporates the limitation as if written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new versions of
|
||||
the Library General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to address
|
||||
new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library specifies
|
||||
a version number of this License which applies to it and "any later version",
|
||||
you have the option of following the terms and conditions either of that version
|
||||
or of any later version published by the Free Software Foundation. If the
|
||||
Library does not specify a license version number, you may choose any version
|
||||
ever published by the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free programs
|
||||
whose distribution conditions are incompatible with these, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free Software
|
||||
Foundation, write to the Free Software Foundation; we sometimes make exceptions
|
||||
for this. Our decision will be guided by the two goals of preserving the free
|
||||
status of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
|
||||
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
|
||||
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
|
||||
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
|
||||
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
|
||||
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
|
||||
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
|
||||
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
|
||||
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
|
||||
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest possible
|
||||
use to the public, we recommend making it free software that everyone can
|
||||
redistribute and change. You can do so by permitting redistribution under
|
||||
these terms (or, alternatively, under the terms of the ordinary General Public
|
||||
License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is safest
|
||||
to attach them to the start of each source file to most effectively convey
|
||||
the exclusion of warranty; and each file should have at least the "copyright"
|
||||
line and a pointer to where the full notice is found.
|
||||
|
||||
one line to give the library's name and an idea of what it does.
|
||||
|
||||
Copyright (C) year name of author
|
||||
|
||||
This library is free software; you can redistribute it and/or modify it under
|
||||
the terms of the GNU Library General Public License as published by the Free
|
||||
Software Foundation; either version 2 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Library General Public License
|
||||
along with this library; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school,
|
||||
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
|
||||
is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in
|
||||
|
||||
the library `Frob' (a library for tweaking knobs) written
|
||||
|
||||
by James Random Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1990
|
||||
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -1,12 +0,0 @@
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as
|
||||
published by the Free Software Foundation; either version 3 of
|
||||
the license or (at your option) at any later version that is
|
||||
accepted by the membership of KDE e.V. (or its successor
|
||||
approved by the membership of KDE e.V.), which shall act as a
|
||||
proxy as defined in Section 14 of version 3 of the license.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
@@ -1,19 +0,0 @@
|
||||
MIT License Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,4 +1,2 @@
|
||||
#! /usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: None
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
$XGETTEXT `find . \( -name \*.cpp -o -name \*.h -o -name \*.qml \)` -o $podir/neochat.pot
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
|
||||
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
-->
|
||||
# NeoChat
|
||||
|
||||
NeoChat is a client for Matrix, the decentralized communication protocol for instant
|
||||
@@ -27,7 +22,7 @@ flatpak install kdeapps org.kde.neochat
|
||||
```
|
||||
|
||||
A nightly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
|
||||
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/NeoChat_Nightly_android-arm64/).
|
||||
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/Neochat_android/).
|
||||
|
||||
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.kde.neochat"
|
||||
android:versionName="${versionName}"
|
||||
android:versionCode="${versionCode}"
|
||||
android:versionName="0.0.1"
|
||||
android:versionCode="1604412458"
|
||||
android:installLocation="auto">
|
||||
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2018-2020 Volker Krause <vkrause@kde.org>
|
||||
SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
SPDX-FileCopyrightText: 2020 Gabriel Souza Franco <gabrielfrancosouza@gmail.com>
|
||||
SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.4'
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: '../version.gradle'
|
||||
def timestamp = (int)(new Date().getTime()/1000)
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
android {
|
||||
/*******************************************************
|
||||
* The following variables:
|
||||
* - androidBuildToolsVersion,
|
||||
* - androidCompileSdkVersion
|
||||
* - qt5AndroidDir - holds the path to qt android files
|
||||
* needed to build any Qt application
|
||||
* on Android.
|
||||
*
|
||||
* are defined in gradle.properties file. This file is
|
||||
* updated by QtCreator and androiddeployqt tools.
|
||||
* Changing them manually might break the compilation!
|
||||
*******************************************************/
|
||||
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
|
||||
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
|
||||
res.srcDirs = [qt5AndroidDir + '/res', 'res']
|
||||
resources.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion qtMinSdkVersion
|
||||
targetSdkVersion qtTargetSdkVersion
|
||||
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'lib/*/*RemoteObjects*'
|
||||
exclude 'lib/*/*StateMachine*'
|
||||
exclude 'lib/*/*_imageformats_qico_*'
|
||||
exclude 'lib/*/*_imageformats_qicns_*'
|
||||
exclude 'lib/*/*_imageformats_qtga_*'
|
||||
exclude 'lib/*/*_imageformats_qtiff_*'
|
||||
exclude 'lib/*/*_qmltooling_*'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
// different syntax than above
|
||||
// see https://android.googlesource.com/platform/frameworks/base/+/refs/heads/pie-release/tools/aapt2/util/Files.h#90
|
||||
ignoreAssetsPattern '!<dir>ECM:!<dir>aclocal:!<dir>doc:!<dir>gtk-doc:!<dir>iso-codes:!<dir>man:!<dir>mime:!<dir>pkgconfig:!<dir>qlogging-categories5:!<file>iso_15924.mo:!<file>iso_3166-2.mo:!<file>iso_3166-3.mo:!<file>iso_4217.mo:!<file>iso_639-2.mo:!<file>iso_639-3.mo:!<file>iso_639-5.mo:!<file>kcodecs5_qt.qm:!<file>kde5_xml_mimetypes.qm'
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
ext {
|
||||
projectVersionFull = "@NEOCHAT_VERSION@"
|
||||
}
|
||||
@@ -5,11 +5,10 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Templates 2.15 as T
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick.Window 2.15
|
||||
|
||||
import org.kde.kirigami 2.18 as Kirigami
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
ToolBar {
|
||||
@@ -67,7 +66,6 @@ ToolBar {
|
||||
id: fontMetrics
|
||||
font: inputField.font
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: inputField
|
||||
focus: true
|
||||
@@ -83,7 +81,6 @@ ToolBar {
|
||||
cursorShape: Qt.IBeamCursor
|
||||
z: 1
|
||||
}
|
||||
|
||||
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
topPadding: 0
|
||||
@@ -100,16 +97,6 @@ ToolBar {
|
||||
wrapMode: Text.Wrap
|
||||
readOnly: currentRoom.usesEncryption
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.SpellChecking.enabled: true
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
hoverEnabled: !Kirigami.Settings.tabletMode
|
||||
|
||||
selectByMouse: !Kirigami.Settings.tabletMode
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
@@ -196,25 +183,6 @@ ToolBar {
|
||||
chatBar.complete();
|
||||
}
|
||||
|
||||
// yes, decrement goes up and increment goes down visually.
|
||||
Keys.onUpPressed: (event) => {
|
||||
if (chatBar.isCompleting) {
|
||||
event.accepted = true
|
||||
completionMenu.listView.decrementCurrentIndex()
|
||||
autoAppeared = true;
|
||||
}
|
||||
event.accepted = false
|
||||
}
|
||||
|
||||
Keys.onDownPressed: (event) => {
|
||||
if (chatBar.isCompleting) {
|
||||
event.accepted = true
|
||||
completionMenu.listView.incrementCurrentIndex()
|
||||
autoAppeared = true;
|
||||
}
|
||||
event.accepted = false
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomDown();
|
||||
@@ -249,18 +217,11 @@ ToolBar {
|
||||
currentRoom.cachedInput = text
|
||||
autoAppeared = false;
|
||||
|
||||
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
|
||||
const completionInfo = documentHandler.getAutocompletionInfo();
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.Ignore) {
|
||||
if (completionInfo.keyword) {
|
||||
// custom emojis
|
||||
const idx = completionMenu.currentIndex;
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
completionMenu.currentIndex = idx;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.None) {
|
||||
isCompleting = false;
|
||||
return;
|
||||
@@ -272,19 +233,16 @@ ToolBar {
|
||||
} else if (completionInfo.type === ChatDocumentHandler.Command) {
|
||||
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
|
||||
} else {
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
completionMenu.model = EmojiModel.filterModel(completionInfo.keyword);
|
||||
}
|
||||
|
||||
if (completionMenu.model.length === 0) {
|
||||
isCompleting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCompleting) {
|
||||
isCompleting = true
|
||||
autoAppeared = true;
|
||||
completionMenu.endPosition = cursorPosition
|
||||
}
|
||||
isCompleting = true
|
||||
autoAppeared = true;
|
||||
completionMenu.endPosition = cursorPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,10 +341,6 @@ ToolBar {
|
||||
}
|
||||
}
|
||||
|
||||
property CustomEmojiModel customEmojiModel: CustomEmojiModel {
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
|
||||
function pasteImage() {
|
||||
let localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
|
||||
if (!Clipboard.saveImage(localPath)) {
|
||||
@@ -401,7 +355,7 @@ ToolBar {
|
||||
if (ChatBoxHelper.hasAttachment) {
|
||||
// send attachment but don't reset the text
|
||||
actionsHandler.postMessage("", ChatBoxHelper.attachmentPath,
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {}, this.customEmojiModel);
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {});
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
messageSent();
|
||||
return;
|
||||
@@ -414,7 +368,7 @@ ToolBar {
|
||||
} else {
|
||||
// send normal message
|
||||
actionsHandler.postMessage(inputField.text.trim(), ChatBoxHelper.attachmentPath,
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted);
|
||||
}
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
inputField.clear();
|
||||
|
||||
@@ -61,10 +61,6 @@ Item {
|
||||
QQC2.Pane {
|
||||
id: connectionPane
|
||||
padding: fontMetrics.lineSpacing * 0.25
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
font: networkLabel.font
|
||||
}
|
||||
spacing: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
background: Rectangle {
|
||||
@@ -73,7 +69,6 @@ Item {
|
||||
visible: !Controller.isOnline
|
||||
width: parent.width
|
||||
QQC2.Label {
|
||||
id: networkLabel
|
||||
text: i18n("NeoChat is offline. Please check your network connection.")
|
||||
}
|
||||
anchors.bottom: emojiPickerLoaderSeparator.top
|
||||
|
||||
@@ -10,14 +10,12 @@ import Qt.labs.qmlmodels 1.0
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Popup {
|
||||
id: control
|
||||
|
||||
// Expose internal ListView properties.
|
||||
property alias model: completionListView.model
|
||||
property alias listView: completionListView
|
||||
property alias currentIndex: completionListView.currentIndex
|
||||
property alias currentItem: completionListView.currentItem
|
||||
property alias count: completionListView.count
|
||||
@@ -44,7 +42,7 @@ Popup {
|
||||
completionListView.currentIndex = 0;
|
||||
}
|
||||
|
||||
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 5)
|
||||
|
||||
contentItem: ScrollView {
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
@@ -86,7 +84,6 @@ Popup {
|
||||
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
||||
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
|
||||
}
|
||||
labelItem.textFormat: Text.PlainText
|
||||
text: modelData.displayName
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
@@ -97,40 +94,23 @@ Popup {
|
||||
Kirigami.BasicListItem {
|
||||
id: emojiItem
|
||||
width: ListView.view.width ?? implicitWidth
|
||||
property string displayName: modelData.isCustom ? modelData.shortname : modelData.unicode
|
||||
text: modelData.shortname
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
property string displayName: modelData.unicode
|
||||
text: modelData.unicode + " " + modelData.shortname
|
||||
|
||||
leading: Image {
|
||||
source: modelData.isCustom ? modelData.unicode : ""
|
||||
|
||||
width: height
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: ShimmerGradient { }
|
||||
}
|
||||
|
||||
Label {
|
||||
id: unicodeLabel
|
||||
|
||||
visible: !modelData.isCustom
|
||||
|
||||
font.family: 'emoji'
|
||||
font.pixelSize: height - 2
|
||||
|
||||
text: modelData.unicode
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
leading: Label {
|
||||
id: unicodeLabel
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit
|
||||
Layout.preferredWidth: textMetrics.tightBoundingRect.width
|
||||
font.pointSize: Kirigami.Units.gridUnit * 0.75
|
||||
text: modelData.unicode
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: modelData.unicode
|
||||
font: unicodeLabel.font
|
||||
}
|
||||
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ Loader {
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
let heading = "<b>%1</b>"
|
||||
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
|
||||
let userName = user ? "<font color=\""+ user.color +"\">" + user.displayName + "</font>" : ""
|
||||
if (isEdit) {
|
||||
heading = heading.arg(i18n("Editing message:")) + "<br/>"
|
||||
} else {
|
||||
|
||||
@@ -10,16 +10,10 @@ import org.kde.neochat 1.0 as NeoChat
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: _picker
|
||||
|
||||
property string emojiCategory: "history"
|
||||
property var textArea
|
||||
readonly property var emojiModel: NeoChat.EmojiModel
|
||||
|
||||
property NeoChat.CustomEmojiModel customModel: NeoChat.CustomEmojiModel {
|
||||
connection: NeoChat.Controller.activeConnection
|
||||
}
|
||||
|
||||
signal chosen(string emoji)
|
||||
|
||||
spacing: 0
|
||||
@@ -35,7 +29,6 @@ ColumnLayout {
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
model: ListModel {
|
||||
ListElement { label: "custom"; category: "custom" }
|
||||
ListElement { label: "⌛️"; category: "history" }
|
||||
ListElement { label: "😏"; category: "people" }
|
||||
ListElement { label: "🌲"; category: "nature" }
|
||||
@@ -48,23 +41,16 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: del
|
||||
|
||||
required property string label
|
||||
required property string category
|
||||
|
||||
width: contentItem.Layout.preferredWidth
|
||||
width: Kirigami.Units.gridUnit * 2
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: del.label === "custom" ? 4 : 1
|
||||
level: 1
|
||||
|
||||
Layout.preferredWidth: del.label === "custom" ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2
|
||||
|
||||
font.family: del.label === "custom" ? "" : 'emoji'
|
||||
text: del.label === "custom" ? i18n("Custom") : del.label
|
||||
font.family: 'emoji'
|
||||
text: label
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -101,8 +87,6 @@ ColumnLayout {
|
||||
|
||||
model: {
|
||||
switch (emojiCategory) {
|
||||
case "custom":
|
||||
return _picker.customModel
|
||||
case "history":
|
||||
return emojiModel.history
|
||||
case "people":
|
||||
@@ -134,32 +118,11 @@ ColumnLayout {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: 'emoji'
|
||||
text: modelData.isCustom ? "" : modelData.unicode
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: modelData.isCustom
|
||||
source: visible ? modelData.unicode : ""
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: ShimmerGradient { }
|
||||
}
|
||||
text: modelData.unicode
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (modelData.isCustom) {
|
||||
chosen(modelData.shortname)
|
||||
} else {
|
||||
chosen(modelData.unicode)
|
||||
}
|
||||
chosen(modelData.unicode)
|
||||
emojiModel.emojiUsed(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ ApplicationWindow {
|
||||
|
||||
property string filename
|
||||
property url localPath
|
||||
property string blurhash: ""
|
||||
property int imageWidth: -1
|
||||
property int imageHeight: -1
|
||||
|
||||
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
|
||||
visibility: Qt.WindowFullScreen
|
||||
|
||||
title: i18n("Image View - %1", filename)
|
||||
|
||||
@@ -31,29 +29,22 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
visible: image.status !== Image.Ready && root.blurhash === ""
|
||||
anchors.centerIn: parent
|
||||
running: visible
|
||||
visible: image.status !== Image.Ready
|
||||
anchors.centerIn: parent
|
||||
running: visible
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
id: image
|
||||
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)
|
||||
width: Math.min(sourceSize.width, root.width)
|
||||
height: Math.min(sourceSize.height, root.height)
|
||||
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: localPath
|
||||
|
||||
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 {
|
||||
|
||||
@@ -10,18 +10,11 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
QQC2.BusyIndicator {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,10 @@ LoginStep {
|
||||
Kirigami.FormLayout {
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onSsoUrlChanged() {
|
||||
onSsoUrlChanged: {
|
||||
Qt.openUrlExternally(LoginHelper.ssoUrl)
|
||||
}
|
||||
function onConnected() {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
|
||||
}
|
||||
onConnected: processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.10
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
QQC2.Popup {
|
||||
id: _popup
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
quickSearch.forceActiveFocus()
|
||||
quickSearch.text = ""
|
||||
}
|
||||
anchors.centerIn: QQC2.Overlay.overlay
|
||||
background: Kirigami.Card {}
|
||||
height: 2 * Math.round(implicitHeight / 2)
|
||||
padding: Kirigami.Units.largeSpacing * 2
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: quickSearch
|
||||
|
||||
// TODO: get this broken property removed/disabled by default in Kirigami,
|
||||
// we used to be able to expect that the text field wouldn't attempt to
|
||||
// perform a mini-DDOS attack using signals.
|
||||
autoAccept: false
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
|
||||
Keys.onLeftPressed: cView.decrementCurrentIndex()
|
||||
Keys.onRightPressed: cView.incrementCurrentIndex()
|
||||
onAccepted: {
|
||||
const item = cView.itemAtIndex(cView.currentIndex)
|
||||
|
||||
RoomManager.enterRoom(item.currentRoom)
|
||||
|
||||
_popup.close()
|
||||
}
|
||||
}
|
||||
ListView {
|
||||
id: cView
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
filterText: quickSearch.text
|
||||
roomSortOrder: SortFilterRoomListModel.LastActivity
|
||||
}
|
||||
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: Kirigami.Avatar {
|
||||
id: del
|
||||
|
||||
implicitHeight: Kirigami.Units.gridUnit * 3
|
||||
implicitWidth: Kirigami.Units.gridUnit * 3
|
||||
|
||||
required property string avatar
|
||||
required property var currentRoom
|
||||
|
||||
source: avatar != "" ? "image://mxc/" + avatar : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
modal: true
|
||||
focus: true
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
// Not to be confused with the Shimmer project.
|
||||
// I like their gradiented GTK themes though.
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Gradient {
|
||||
id: gradient
|
||||
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
property color color: Kirigami.Theme.textColor
|
||||
property color translucent: Qt.rgba(color.r, color.g, color.b, 0.2)
|
||||
property color bright: Qt.rgba(color.r, color.g, color.b, 0.3)
|
||||
property real pos: 0.5
|
||||
property real offset: 0.6
|
||||
|
||||
property SequentialAnimation ani: SequentialAnimation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: -2.0
|
||||
to: 2.0
|
||||
duration: 700
|
||||
target: gradient
|
||||
properties: "pos"
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
GradientStop { position: gradient.pos-gradient.offset; color: gradient.translucent }
|
||||
GradientStop { position: gradient.pos; color: gradient.bright }
|
||||
GradientStop { position: gradient.pos+gradient.offset; color: gradient.translucent }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
TextEdit {
|
||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
@@ -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 QtGraphicalEffects 1.15
|
||||
import Qt.labs.platform 1.1
|
||||
@@ -23,84 +23,31 @@ RowLayout {
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: progressInfo.completed
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
|
||||
icon.name: "document-open"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: progressInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: sizeLabel
|
||||
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
|
||||
}
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "media-playback-stop"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||
onClicked: currentRoom.cancelFileTransfer(eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "raw"
|
||||
when: true
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
|
||||
onClicked: root.saveFileAs()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Kirigami.Icon {
|
||||
id: ikon
|
||||
source: model.fileMimetypeIcon
|
||||
fallback: "unknown"
|
||||
onDownloadedChanged: if (downloaded && openOnFinished) {
|
||||
openSavedFile();
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: progressInfo.completed ? "document-open" : "document-save"
|
||||
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 4
|
||||
text: model.display
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Label.Wrap
|
||||
}
|
||||
QQC2.Label {
|
||||
id: sizeLabel
|
||||
|
||||
text: Controller.formatByteSize(content.info ? content.info.size : 0)
|
||||
opacity: 0.7
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: !progressInfo.completed && progressInfo.active ? (Controller.formatByteSize(progressInfo.progress) + "/" + Controller.formatByteSize(progressInfo.total)) : Controller.formatByteSize(content.info ? content.info.size : 0)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
wrapMode: Label.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: downloadButton
|
||||
icon.name: "download"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
@@ -114,11 +61,21 @@ RowLayout {
|
||||
}
|
||||
|
||||
function saveFileAs() {
|
||||
var dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
||||
}
|
||||
|
||||
function downloadAndOpen() {
|
||||
if (downloaded) {
|
||||
openSavedFile();
|
||||
} else {
|
||||
openOnFinished = true;
|
||||
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/"
|
||||
+ eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId));
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (Qt.openUrlExternally(progressInfo.localPath)) return;
|
||||
if (Qt.openUrlExternally(progressInfo.localDir)) return;
|
||||
|
||||
@@ -29,12 +29,6 @@ Image {
|
||||
|
||||
source: "image://mxc/" + mediaId
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : ""
|
||||
visible: parent.status !== Image.Ready
|
||||
}
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
ToolTip.text: display
|
||||
|
||||
@@ -29,7 +29,7 @@ Flow {
|
||||
shadow.size: Kirigami.Units.smallSpacing
|
||||
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||
border.width: 1
|
||||
border.width: Kirigami.Units.devicePixelRatio
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,12 +25,11 @@ MouseArea {
|
||||
id: replyLeftBorder
|
||||
width: Kirigami.Units.smallSpacing
|
||||
height: parent.height
|
||||
x: Config.compactLayout ? Kirigami.Units.largeSpacing : 0
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: replyAvatar
|
||||
id: avatatReply
|
||||
anchors.left: replyLeftBorder.right
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
width: visible ? Kirigami.Units.gridUnit : 0
|
||||
@@ -47,12 +46,12 @@ MouseArea {
|
||||
QQC2.Label {
|
||||
id: replyName
|
||||
anchors {
|
||||
left: replyAvatar.right
|
||||
left: avatatReply.right
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
right: parent.right
|
||||
rightMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
text: currentRoom.htmlSafeMemberName(reply.author.id)
|
||||
text: reply.author.displayName
|
||||
color: reply.author.color
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
@@ -79,9 +78,9 @@ MouseArea {
|
||||
id: replyText
|
||||
textMessage: reply.display
|
||||
textFormat: Text.RichText
|
||||
hasContextMenu: false
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(implicitWidth, bubbleMaxWidth - Kirigami.Units.largeSpacing * 3)
|
||||
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
|
||||
x: Kirigami.Units.smallSpacing * 3 + avatatReply.width
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +94,9 @@ MouseArea {
|
||||
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
|
||||
source: "image://mxc/" + mediaId
|
||||
|
||||
width: bubbleMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - replyAvatar.width
|
||||
width: bubbleMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - avatatReply.width
|
||||
height: reply.content.info.h / reply.content.info.w * width
|
||||
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
|
||||
x: Kirigami.Units.smallSpacing * 3 + avatatReply.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
Kirigami.Heading {
|
||||
level: 4
|
||||
text: model.showSection ? section : ""
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
topPadding: Kirigami.Units.largeSpacing * 2
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
@@ -13,10 +13,7 @@ import NeoChat.Dialog 1.0
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
height: label.contentHeight
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: icon
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignTop
|
||||
@@ -38,13 +35,11 @@ RowLayout {
|
||||
}
|
||||
|
||||
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> " + display
|
||||
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
text: "<style>a {text-decoration: none;}</style><a href=\"https://matrix.to/#/" + author.id + "\" style='color: " + author.color + "'>" + author.displayName + "</a> " + display
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,13 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
TextEdit {
|
||||
id: contentLabel
|
||||
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.topMargin: 0
|
||||
|
||||
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
|
||||
readonly property var hasSpoiler: /data-mx-spoiler/g
|
||||
|
||||
property bool isEmote: false
|
||||
property string textMessage: model.display
|
||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||
|
||||
property bool hasContextMenu: true
|
||||
|
||||
signal requestOpenMessageContext()
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||
|
||||
Layout.fillWidth: Config.compactLayout
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
|
||||
text: "<style>
|
||||
table {
|
||||
@@ -47,21 +38,17 @@ a{
|
||||
color: " + Kirigami.Theme.linkColor + ";
|
||||
text-decoration: none;
|
||||
}
|
||||
" + (!spoilerRevealed ? "
|
||||
[data-mx-spoiler] {
|
||||
color: transparent;
|
||||
background: " + Kirigami.Theme.textColor + ";
|
||||
}
|
||||
" : "") + "
|
||||
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.Wrap
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
onLinkActivated: RoomManager.openResource(link)
|
||||
onHoveredLinkChanged: if (hoveredLink.length > 0) {
|
||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||
@@ -69,24 +56,9 @@ a{
|
||||
applicationWindow().hoverLinkIndicator.text = "";
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
||||
onTapped: spoilerRevealed = true
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(model, parent.selectedText)
|
||||
enabled: hasContextMenu
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: requestOpenMessageContext()
|
||||
enabled: hasContextMenu
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ QQC2.ItemDelegate {
|
||||
property bool isEmote: false
|
||||
property bool cardBackground: true
|
||||
|
||||
readonly property int bubbleMaxWidth: Config.compactLayout && !Config.showAvatarInTimeline ? width : (Config.compactLayout ? width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, Kirigami.Units.gridUnit * 20))
|
||||
|
||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !applicationWindow().wideScreen
|
||||
readonly property int bubbleMaxWidth: !Config.showAvatarInTimeline ? width : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4, Kirigami.Units.gridUnit * 20)
|
||||
|
||||
signal saveFileAs()
|
||||
signal openExternally()
|
||||
@@ -50,7 +48,7 @@ QQC2.ItemDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : 0)
|
||||
height: sectionDelegate.height + Math.max(avatar.height, bubble.implicitHeight) + loader.height + (model.showAuthor ? Kirigami.Units.smallSpacing : 0) - (Config.showAvatarInTimeline ? 0 : Kirigami.Units.largeSpacing)
|
||||
|
||||
SectionDelegate {
|
||||
id: sectionDelegate
|
||||
@@ -69,12 +67,12 @@ QQC2.ItemDelegate {
|
||||
sourceSize.height: width
|
||||
anchors {
|
||||
top: sectionDelegate.bottom
|
||||
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : 0
|
||||
topMargin: model.showAuthor ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.smallSpacing
|
||||
left: parent.left
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
visible: model.showAuthor && Config.showAvatarInTimeline && !showUserMessageOnRight
|
||||
visible: model.showAuthor && Config.showAvatarInTimeline
|
||||
name: model.author.name ?? model.author.displayName
|
||||
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
|
||||
color: model.author.color
|
||||
@@ -96,50 +94,18 @@ QQC2.ItemDelegate {
|
||||
|
||||
QQC2.Control {
|
||||
id: bubble
|
||||
topPadding: !Config.compactLayout ? Kirigami.Units.largeSpacing : 0
|
||||
bottomPadding: !Config.compactLayout ? Kirigami.Units.largeSpacing : 0
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Config.compactLayout ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
|
||||
topPadding: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
hoverEnabled: true
|
||||
|
||||
// state: Config.compactLayout ? "compactLayout" : "default"
|
||||
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
|
||||
|
||||
anchors {
|
||||
top: avatar.top
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
|
||||
left: avatar.right
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
right: Config.showAvatarInTimeline ? undefined : parent.right
|
||||
rightMargin: Config.showAvatarInTimeline ? undefined : Kirigami.Units.largeSpacing
|
||||
}
|
||||
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
|
||||
width: Config.compactLayout ? messageDelegate.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
|
||||
|
||||
|
||||
// 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
|
||||
@@ -150,7 +116,7 @@ QQC2.ItemDelegate {
|
||||
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.preferredWidth: nameLabel.implicitWidth + timeLabel.implicitWidth + Kirigami.Units.largeSpacing
|
||||
Layout.maximumWidth: bubbleMaxWidth
|
||||
implicitHeight: visible ? nameLabel.implicitHeight : 0
|
||||
|
||||
@@ -164,7 +130,6 @@ QQC2.ItemDelegate {
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
text: visible ? author.displayName : ""
|
||||
textFormat: Text.PlainText
|
||||
font.weight: Font.Bold
|
||||
color: author.color
|
||||
wrapMode: Text.Wrap
|
||||
@@ -195,8 +160,7 @@ QQC2.ItemDelegate {
|
||||
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.bottomMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
Connections {
|
||||
target: replyLoader.item
|
||||
@@ -207,31 +171,14 @@ QQC2.ItemDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
background: Item {
|
||||
Rectangle {
|
||||
visible: messageDelegate.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
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
visible: cardBackground && Config.showAvatarInTimeline
|
||||
color: model.isHighlighted ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
shadow.size: Kirigami.Units.smallSpacing
|
||||
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||
border.width: Kirigami.Units.devicePixelRatio
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +188,7 @@ QQC2.ItemDelegate {
|
||||
left: bubble.left
|
||||
right: parent.right
|
||||
top: bubble.bottom
|
||||
topMargin: active && !Config.compactLayout ? Kirigami.Units.smallSpacing : 0
|
||||
topMargin: active && Config.showAvatarInTimeline ? Kirigami.Units.smallSpacing : 0
|
||||
}
|
||||
height: active ? item.implicitHeight : 0
|
||||
//Layout.bottomMargin: readMarker ? Kirigami.Units.smallSpacing : 0
|
||||
|
||||
@@ -8,4 +8,3 @@ FileDelegate 1.0 FileDelegate.qml
|
||||
VideoDelegate 1.0 VideoDelegate.qml
|
||||
ReactionDelegate 1.0 ReactionDelegate.qml
|
||||
AudioDelegate 1.0 AudioDelegate.qml
|
||||
EncryptedDelegate 1.0 EncryptedDelegate.qml
|
||||
@@ -90,7 +90,6 @@ Loader {
|
||||
id: typingLabel
|
||||
elide: Text.ElideRight
|
||||
text: root.labelText
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,3 @@ FullScreenImage 1.0 FullScreenImage.qml
|
||||
ChatTextInput 1.0 ChatTextInput.qml
|
||||
FancyEffectsContainer 1.0 FancyEffectsContainer.qml
|
||||
TypingPane 1.0 TypingPane.qml
|
||||
QuickSwitcher 1.0 QuickSwitcher.qml
|
||||
ShimmerGradient 1.0 ShimmerGradient.qml
|
||||
|
||||
@@ -14,7 +14,9 @@ Kirigami.OverlaySheet {
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
title: i18n("Create a Room")
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Create a Room")
|
||||
}
|
||||
|
||||
contentItem: Kirigami.FormLayout {
|
||||
TextField {
|
||||
|
||||
207
imports/NeoChat/Dialog/RoomSettingsDialog.qml
Normal file
207
imports/NeoChat/Dialog/RoomSettingsDialog.qml
Normal file
@@ -0,0 +1,207 @@
|
||||
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: root
|
||||
|
||||
property var room
|
||||
|
||||
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
|
||||
readonly property bool canChangeName: room.canSendState("m.room.name")
|
||||
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
|
||||
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
|
||||
header: Kirigami.Heading {
|
||||
text: i18nc("%1 is the room name", "Room Settings - %1", room.displayName)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 72
|
||||
Layout.preferredHeight: 72
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
name: room.name
|
||||
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
enabled: canChangeAvatar
|
||||
|
||||
onClicked: {
|
||||
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
room.changeAvatar(path)
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
TextField {
|
||||
id: roomNameField
|
||||
text: room.name
|
||||
Kirigami.FormData.label: i18n("Room Name:")
|
||||
enabled: canChangeName
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: roomTopicField
|
||||
Layout.fillWidth: true
|
||||
text: room.topic
|
||||
Kirigami.FormData.label: i18n("Room topic:")
|
||||
enabled: canChangeTopic
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
visible: canChangeName || canChangeTopic
|
||||
|
||||
text: i18n("Save")
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
if (room.name != roomNameField.text) {
|
||||
room.setName(roomNameField.text)
|
||||
}
|
||||
|
||||
if (room.topic != roomTopicField.text) {
|
||||
room.setTopic(roomTopicField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: canonicalAliasComboBox.visible || altAlias.visible
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: canonicalAliasComboBox
|
||||
visible: room.aliases && room.aliases.length
|
||||
Kirigami.FormData.label: i18n("Canonical Alias:")
|
||||
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
|
||||
|
||||
enabled: canChangeCanonicalAlias
|
||||
|
||||
model: room.aliases
|
||||
|
||||
currentIndex: room.aliases.indexOf(room.canonicalAlias)
|
||||
onCurrentIndexChanged: {
|
||||
if (room.canonicalAlias != room.aliases[currentIndex]) {
|
||||
room.setCanonicalAlias(room.aliases[currentIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: altAlias
|
||||
Kirigami.FormData.label: i18n("Alt Aliases")
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.altAliases && room.altAliases.length
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: room.altAliases
|
||||
|
||||
delegate: RowLayout {
|
||||
Layout.maximumWidth: parent.width
|
||||
|
||||
Label {
|
||||
text: modelData
|
||||
color: Kirigami.Theme.disabledColor
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: ""
|
||||
onClicked: room.removeLocalAlias(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: next.visible || prev.visible
|
||||
}
|
||||
|
||||
Control {
|
||||
id: next
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.predecessorId && room.connection.room(room.predecessorId)
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
text: i18n("This room continues another conversation.")
|
||||
actions: Kirigami.Action {
|
||||
text: i18n("See older messages...")
|
||||
onTriggered: {
|
||||
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
id: prev
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.successorId && room.connection.room(room.successorId)
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
text: i18n("This room has been replaced.")
|
||||
actions: Kirigami.Action {
|
||||
text: i18n("See new room...")
|
||||
onTriggered: {
|
||||
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ Kirigami.OverlaySheet {
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
|
||||
title: i18nc("@title:menu Account detail dialog", "Account detail")
|
||||
header: Kirigami.Heading {
|
||||
id: heading
|
||||
text: i18nc("@title:menu Account detail dialog", "Account detail")
|
||||
elide: Text.ElideRight
|
||||
QQC2.ToolTip.visible: truncated && hovered
|
||||
QQC2.ToolTip.text: text
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
@@ -68,7 +74,7 @@ Kirigami.OverlaySheet {
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: room.htmlSafeMemberName(user.id)
|
||||
text: displayName
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
@@ -123,32 +129,6 @@ Kirigami.OverlaySheet {
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
visible: user !== room.localUser && room.canSendState("ban") && room.isUserBanned(user.id)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Unban this user")
|
||||
icon.name: "im-irc"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
room.unban(user.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
visible: user === room.localUser || room.canSendState("redact")
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Delete recent messages by this user")
|
||||
icon.name: "delete"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
room.deleteMessagesByUser(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
visible: user !== room.localUser
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Open a private chat")
|
||||
icon.name: "document-send"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module NeoChat.Dialog
|
||||
RoomSettingsDialog 1.0 RoomSettingsDialog.qml
|
||||
UserDetailDialog 1.0 UserDetailDialog.qml
|
||||
LoginDialog 1.0 LoginDialog.qml
|
||||
CreateRoomDialog 1.0 CreateRoomDialog.qml
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Labs
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.10
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Labs.Menu {
|
||||
id: editMenu
|
||||
|
||||
required property Item field
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.canUndo
|
||||
text: i18nc("text editing menu action", "Undo")
|
||||
shortcut: StandardKey.Undo
|
||||
onTriggered: {
|
||||
editMenu.field.undo()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.canRedo
|
||||
text: i18nc("text editing menu action", "Redo")
|
||||
shortcut: StandardKey.Redo
|
||||
onTriggered: {
|
||||
editMenu.field.undo()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.selectedText
|
||||
text: i18nc("text editing menu action", "Cut")
|
||||
shortcut: StandardKey.Cut
|
||||
onTriggered: {
|
||||
editMenu.field.cut()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.selectedText
|
||||
text: i18nc("text editing menu action", "Copy")
|
||||
shortcut: StandardKey.Copy
|
||||
onTriggered: {
|
||||
editMenu.field.copy()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.canPaste
|
||||
text: i18nc("text editing menu action", "Paste")
|
||||
shortcut: StandardKey.Paste
|
||||
onTriggered: {
|
||||
editMenu.field.paste()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null && editMenu.field.selectedText !== ""
|
||||
text: i18nc("text editing menu action", "Delete")
|
||||
shortcut: ""
|
||||
onTriggered: {
|
||||
editMenu.field.remove(editMenu.field.selectionStart, editMenu.field.selectionEnd)
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: editMenu.field !== null
|
||||
text: i18nc("text editing menu action", "Select All")
|
||||
shortcut: StandardKey.SelectAll
|
||||
onTriggered: {
|
||||
editMenu.field.selectAll()
|
||||
editMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Labs
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.10
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
import NeoChat.Page 1.0
|
||||
import NeoChat.Panel 1.0
|
||||
|
||||
Labs.MenuBar {
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "NeoChat")
|
||||
|
||||
// TODO: make about page its own thing so we can go to it instead of settings where it's currently at
|
||||
// Labs.MenuItem {
|
||||
// text: i18nc("menu", "About NeoChat")
|
||||
// }
|
||||
Labs.MenuItem {
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
|
||||
text: i18nc("menu", "Preferences…")
|
||||
|
||||
shortcut: StandardKey.Preferences
|
||||
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Quit NeoChat")
|
||||
|
||||
shortcut: StandardKey.Quit
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "File")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "New Private Chat…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "New Group…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
const dialog = createRoomDialog.createObject(root.overlay)
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Browse Chats…")
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
|
||||
}
|
||||
}
|
||||
EditMenu {
|
||||
title: i18nc("menu", "Edit")
|
||||
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "View")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu item that opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Open Quick Switcher")
|
||||
shortcut: "Ctrl+K"
|
||||
onTriggered: quickView.item.open()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "Window")
|
||||
|
||||
// Labs.MenuItem {
|
||||
// text: settings.userWantsSidebars ? i18nc("menu", "Hide Sidebar") : i18nc("menu", "Show Sidebar")
|
||||
// onTriggered: settings.userWantsSidebars = !settings.userWantsSidebars
|
||||
// }
|
||||
Labs.MenuItem {
|
||||
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
||||
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
|
||||
}
|
||||
}
|
||||
// TODO: offline help system (https://invent.kde.org/network/neochat/-/issues/411)
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "Help")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Matrix FAQ")
|
||||
onTriggered: Qt.openUrlExternally("https://matrix.org/faq/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,9 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.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
|
||||
*/
|
||||
@@ -19,15 +16,11 @@ Menu {
|
||||
property var room
|
||||
|
||||
MenuItem {
|
||||
id: newWindow
|
||||
text: i18n("Open in new window")
|
||||
onTriggered: RoomManager.openWindow(room);
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
visible: newWindow.visible
|
||||
}
|
||||
MenuSeparator {}
|
||||
|
||||
MenuItem {
|
||||
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
|
||||
@@ -47,18 +40,6 @@ Menu {
|
||||
onTriggered: room.markAllMessagesAsRead()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: i18nc("@action:inmenu", "Copy address to clipboard")
|
||||
|
||||
onTriggered: {
|
||||
if (room.canonicalAlias.length === 0) {
|
||||
Clipboard.saveText(room.id)
|
||||
} else {
|
||||
Clipboard.saveText(room.canonicalAlias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {}
|
||||
|
||||
MenuItem {
|
||||
|
||||
@@ -65,12 +65,7 @@ MessageDelegateContextMenu {
|
||||
text: i18n("View Source")
|
||||
icon.name: "code-context"
|
||||
onTriggered: {
|
||||
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
|
||||
sourceText: root.source
|
||||
}, {
|
||||
title: i18n("Message Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
messageSourceSheet.createObject(root, {'sourceText': root.source}).open();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,7 +19,6 @@ Loader {
|
||||
property string eventType: ""
|
||||
property string formattedBody: ""
|
||||
required property string source
|
||||
property string selectedText: ""
|
||||
|
||||
property list<Kirigami.Action> actions: [
|
||||
Kirigami.Action {
|
||||
@@ -43,18 +42,13 @@ Loader {
|
||||
Kirigami.Action {
|
||||
text: i18n("Copy")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.message : loadRoot.selectedText)
|
||||
onTriggered: Clipboard.saveText(message)
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("View Source")
|
||||
icon.name: "code-context"
|
||||
onTriggered: {
|
||||
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
|
||||
sourceText: loadRoot.source
|
||||
}, {
|
||||
title: i18n("Message Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
messageSourceSheet.createObject(page, {'sourceText': loadRoot.source}).open();
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -71,33 +65,89 @@ Loader {
|
||||
onClicked: loadRoot.item.close();
|
||||
}
|
||||
}
|
||||
QQC2.Menu {
|
||||
id: webshortcutmenu
|
||||
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
|
||||
property bool isVisible: selectedText && selectedText.length > 0 && webshortcutmodel.enabled
|
||||
Component.onCompleted: webshortcutmenu.parent.visible = isVisible
|
||||
onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible
|
||||
Instantiator {
|
||||
model: WebShortcutModel {
|
||||
id: webshortcutmodel
|
||||
selectedText: loadRoot.selectedText
|
||||
onOpenUrl: RoomManager.visitNonMatrix(url)
|
||||
}
|
||||
/*
|
||||
Kirigami.OverlaySheet {
|
||||
id: root
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
header: Kirigami.Heading {
|
||||
text: i18nc("@title:menu Message detail dialog", "Message detail")
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Kirigami.Avatar {
|
||||
id: avatar
|
||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||
Layout.alignment: Qt.AlignTop
|
||||
name: author.displayName
|
||||
color: author.color
|
||||
}
|
||||
delegate: QQC2.MenuItem {
|
||||
text: model.display
|
||||
icon.name: model.decoration
|
||||
onTriggered: webshortcutmodel.trigger(model.edit)
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
level: 3
|
||||
Layout.fillWidth: true
|
||||
text: author.displayName
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
text: message
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: RoomManager.openResource(link);
|
||||
}
|
||||
}
|
||||
onObjectAdded: webshortcutmenu.insertItem(0, object)
|
||||
}
|
||||
QQC2.MenuSeparator {}
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Configure Web Shortcuts...")
|
||||
icon.name: "configure"
|
||||
onTriggered: webshortcutmodel.configureWebShortcuts()
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
|
||||
Repeater {
|
||||
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
|
||||
delegate: QQC2.ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.pixelSize: 16
|
||||
font.family: "emoji"
|
||||
text: modelData
|
||||
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
currentRoom.toggleReaction(eventId, modelData)
|
||||
loadRoot.item.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
Component {
|
||||
id: mobileMenu
|
||||
@@ -135,7 +185,7 @@ Loader {
|
||||
Kirigami.Heading {
|
||||
level: 3
|
||||
Layout.fillWidth: true
|
||||
text: currentRoom.htmlSafeMemberName(author.id)
|
||||
text: author.displayName
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
|
||||
@@ -5,41 +5,20 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import org.kde.syntaxhighlighting 1.0
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.Page {
|
||||
Kirigami.OverlaySheet {
|
||||
|
||||
property string sourceText
|
||||
|
||||
topPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
bottomPadding: 0
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Message Source")
|
||||
}
|
||||
|
||||
title: i18n("Message Source")
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
TextArea {
|
||||
id: sourceTextArea
|
||||
text: sourceText
|
||||
readOnly: true
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
|
||||
SyntaxHighlighter {
|
||||
textEdit: sourceTextArea
|
||||
definition: "JSON"
|
||||
repository: Repository
|
||||
}
|
||||
}
|
||||
TextArea {
|
||||
text: sourceText
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
module NeoChat.Menu
|
||||
RoomListContextMenu 1.0 RoomListContextMenu.qml
|
||||
GlobalMenu 1.0 GlobalMenu.qml
|
||||
EditMenu 1.0 EditMenu.qml
|
||||
|
||||
@@ -11,99 +11,52 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
|
||||
Kirigami.Page {
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18n("Accounts")
|
||||
|
||||
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pageSettingStack
|
||||
function onWideModeChanged() {
|
||||
scroll.background.visible = pageSettingStack.wideMode
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ScrollView {
|
||||
id: scroll
|
||||
Component.onCompleted: background.visible = pageSettingStack.wideMode
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
|
||||
ListView {
|
||||
clip: true
|
||||
model: AccountRegistry
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection, true)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
]
|
||||
ListView {
|
||||
model: AccountListModel { }
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
action: Kirigami.Action {
|
||||
onTriggered: Controller.activeConnection = model.connection
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
footer: Column {
|
||||
height: visible ? implicitHeight : 0
|
||||
Kirigami.ActionToolBar {
|
||||
alignment: Qt.AlignRight
|
||||
visible: pageSettingStack.wideMode
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
width: parent.width
|
||||
flat: false
|
||||
text: model.user.displayName
|
||||
subtitle: model.user.id
|
||||
icon: model.user.avatarMediaId ? ("image://mxc/" + model.user.avatarMediaId) : "im-user"
|
||||
|
||||
onClicked: {
|
||||
Controller.activeConnection = model.connection
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Add an account")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection, true)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
function onConnectionAdded() {
|
||||
@@ -120,6 +73,12 @@ Kirigami.Page {
|
||||
}
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
text: i18n("Add an account")
|
||||
iconName: "list-add-user"
|
||||
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
@@ -133,13 +92,16 @@ Kirigami.Page {
|
||||
|
||||
property var connection
|
||||
|
||||
title: i18n("Edit Account")
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Edit Account")
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
anchors.top: passwordsMessage.bottom
|
||||
RowLayout {
|
||||
Kirigami.Avatar {
|
||||
id: avatar
|
||||
source: userEditSheet.connection && userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
|
||||
source: userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
@@ -176,7 +138,7 @@ Kirigami.Page {
|
||||
}
|
||||
Controls.TextField {
|
||||
id: name
|
||||
text: userEditSheet.connection ? userEditSheet.connection.localUser.displayName : ""
|
||||
text: userEditSheet.connection.localUser.displayName
|
||||
Kirigami.FormData.label: i18n("Name:")
|
||||
}
|
||||
Controls.TextField {
|
||||
100
imports/NeoChat/Page/DevicesPage.qml
Normal file
100
imports/NeoChat/Page/DevicesPage.qml
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18n("Devices")
|
||||
|
||||
ListView {
|
||||
model: DevicesModel {
|
||||
id: devices
|
||||
}
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: model.displayName
|
||||
subtitle: model.id
|
||||
icon: "network-connect"
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit device name")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
renameSheet.index = model.index
|
||||
renameSheet.name = model.displayName
|
||||
renameSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout device")
|
||||
iconName: "edit-delete-remove"
|
||||
onTriggered: {
|
||||
passwordSheet.index = index
|
||||
passwordSheet.open()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: passwordSheet
|
||||
|
||||
property var index
|
||||
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Remove device")
|
||||
}
|
||||
Kirigami.FormLayout {
|
||||
Controls.TextField {
|
||||
id: passwordField
|
||||
Kirigami.FormData.label: i18n("Password:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
Controls.Button {
|
||||
text: i18n("Confirm")
|
||||
onClicked: {
|
||||
devices.logout(passwordSheet.index, passwordField.text)
|
||||
passwordField.text = ""
|
||||
passwordSheet.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: renameSheet
|
||||
property int index
|
||||
property string name
|
||||
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Edit device")
|
||||
}
|
||||
Kirigami.FormLayout {
|
||||
Controls.TextField {
|
||||
id: nameField
|
||||
Kirigami.FormData.label: i18n("Name:")
|
||||
text: renameSheet.name
|
||||
}
|
||||
Controls.Button {
|
||||
text: i18n("Save")
|
||||
onClicked: {
|
||||
devices.setName(renameSheet.index, nameField.text)
|
||||
renameSheet.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,14 @@ Kirigami.Page {
|
||||
title: i18n("Edit")
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
|
||||
|
||||
function crop() {
|
||||
const ratioX = editImage.paintedWidth / editImage.nativeWidth;
|
||||
const ratioY = editImage.paintedHeight / editImage.nativeHeight;
|
||||
rootEditorView.resizing = false
|
||||
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
|
||||
imageDoc.crop(resizeRectangle.insideX / ratioX, resizeRectangle.insideY / ratioY, resizeRectangle.insideWidth / ratioX, resizeRectangle.insideHeight / ratioY);
|
||||
}
|
||||
|
||||
actions {
|
||||
@@ -58,11 +58,8 @@ Kirigami.Page {
|
||||
|
||||
|
||||
|
||||
KQuickImageEditor.ImageItem {
|
||||
contentItem: KQuickImageEditor.ImageItem {
|
||||
id: editImage
|
||||
// Assigning this to the contentItem and setting the padding causes weird positioning issues
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.gridUnit
|
||||
fillMode: KQuickImageEditor.ImageItem.PreserveAspectFit
|
||||
image: imageDoc.image
|
||||
|
||||
@@ -79,41 +76,12 @@ Kirigami.Page {
|
||||
Shortcut {
|
||||
sequence: StandardKey.SaveAs
|
||||
onActivated: saveAsAction.trigger();
|
||||
}
|
||||
} anchors.fill: parent
|
||||
|
||||
KQuickImageEditor.ImageDocument {
|
||||
id: imageDoc
|
||||
path: rootEditorView.imagePath
|
||||
}
|
||||
|
||||
KQuickImageEditor.SelectionTool {
|
||||
id: selectionTool
|
||||
visible: rootEditorView.resizing
|
||||
width: editImage.paintedWidth
|
||||
height: editImage.paintedHeight
|
||||
x: editImage.horizontalPadding
|
||||
y: editImage.verticalPadding
|
||||
KQuickImageEditor.CropBackground {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
insideX: selectionTool.selectionX
|
||||
insideY: selectionTool.selectionY
|
||||
insideWidth: selectionTool.selectionWidth
|
||||
insideHeight: selectionTool.selectionHeight
|
||||
}
|
||||
Connections {
|
||||
target: selectionTool.selectionArea
|
||||
function onDoubleClicked() {
|
||||
rootEditorView.crop()
|
||||
}
|
||||
}
|
||||
}
|
||||
onImageChanged: {
|
||||
selectionTool.selectionX = 0
|
||||
selectionTool.selectionY = 0
|
||||
selectionTool.selectionWidth = Qt.binding(() => selectionTool.width)
|
||||
selectionTool.selectionHeight = Qt.binding(() => selectionTool.height)
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.ToolBar {
|
||||
@@ -177,4 +145,22 @@ Kirigami.Page {
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
KQuickImageEditor.ResizeRectangle {
|
||||
id: resizeRectangle
|
||||
|
||||
visible: rootEditorView.resizing
|
||||
|
||||
width: editImage.paintedWidth
|
||||
height: editImage.paintedHeight
|
||||
x: editImage.horizontalPadding
|
||||
y: editImage.verticalPadding
|
||||
|
||||
insideX: 100
|
||||
insideY: 100
|
||||
insideWidth: 100
|
||||
insideHeight: 100
|
||||
|
||||
onAcceptSize: rootEditorView.crop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
title: i18n("Loading")
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: loadingIndicator
|
||||
QQC2.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Loading")
|
||||
QQC2.BusyIndicator {
|
||||
running: loadingIndicator.visible
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,7 @@ import NeoChat.Menu 1.0
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
title: i18n("Rooms")
|
||||
|
||||
property var enteredRoom
|
||||
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
|
||||
|
||||
onCollapsedModeChanged: if (collapsedMode) {
|
||||
sortFilterRoomListModel.filterText = "";
|
||||
if (page.contentItem && page.contentItem.flickableItem && page.contentItem.flickableItem.QQC2.ScrollBar.vertical) {
|
||||
page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = false;
|
||||
}
|
||||
} else {
|
||||
page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = true;
|
||||
}
|
||||
|
||||
// HACK: the scrollbar is created with a 0 timer, so we need to set the visible flag
|
||||
// after it has been created
|
||||
Timer {
|
||||
running: true
|
||||
interval: 200
|
||||
onTriggered: page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = !collapsedMode;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoom() {
|
||||
do {
|
||||
@@ -60,56 +33,21 @@ Kirigami.ScrollablePage {
|
||||
listView.currentItem.action.trigger();
|
||||
}
|
||||
|
||||
titleDelegate: collapsedMode ? empty : searchField
|
||||
title: i18n("Rooms")
|
||||
|
||||
Component {
|
||||
id: empty
|
||||
Item {}
|
||||
titleDelegate: Kirigami.SearchField {
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: sortFilterRoomListModel.filterText = text
|
||||
KeyNavigation.tab: listView
|
||||
}
|
||||
|
||||
Component {
|
||||
id: searchField
|
||||
Kirigami.SearchField {
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: sortFilterRoomListModel.filterText = text
|
||||
KeyNavigation.tab: listView
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
visible: page.collapsedMode
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: quickView.item.open();
|
||||
}
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: visible ? page.width : 0
|
||||
height: visible ? Kirigami.Units.gridUnit * 2 : 0
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "search"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
activeFocusOnTab: true
|
||||
clip: accountList.count > 1
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
@@ -126,7 +64,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemSelectionModel {
|
||||
id: itemSelection
|
||||
model: roomListModel
|
||||
@@ -150,7 +87,6 @@ Kirigami.ScrollablePage {
|
||||
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
id: sectionHeader
|
||||
height: implicitHeight
|
||||
action: Kirigami.Action {
|
||||
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
}
|
||||
@@ -161,14 +97,11 @@ Kirigami.ScrollablePage {
|
||||
level: 3
|
||||
text: roomListModel.categoryName(section)
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
visible: !page.collapsedMode
|
||||
}
|
||||
Kirigami.Icon {
|
||||
source: page.collapsedMode ? roomListModel.categoryIconName(section) : (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
|
||||
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,124 +109,87 @@ Kirigami.ScrollablePage {
|
||||
reuseItems: true
|
||||
currentIndex: -1 // we don't want any room highlighted by default
|
||||
|
||||
delegate: page.collapsedMode ? collapsedModeListComponent : normalModeListComponent
|
||||
delegate: Kirigami.BasicListItem {
|
||||
id: roomListItem
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
highlighted: listView.currentIndex === index
|
||||
focus: true
|
||||
icon: undefined
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
itemSelection.setCurrentIndex(sortFilterRoomListModel.mapToSource(
|
||||
sortFilterRoomListModel.index(index, 0)), ItemSelectionModel.SelectCurrent)
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
bold: unreadCount > 0
|
||||
label: name ?? ""
|
||||
subtitle: {
|
||||
let txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
|
||||
if (txt.length) {
|
||||
return txt
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
Component {
|
||||
id: collapsedModeListComponent
|
||||
leading: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
implicitWidth: visible ? height : 0
|
||||
visible: Config.showAvatarInTimeline
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
trailing: RowLayout {
|
||||
QQC2.Label {
|
||||
text: notificationCount
|
||||
visible: notificationCount > 0
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
color: highlightCount > 0 ? "white" : Kirigami.Theme.textColor
|
||||
Layout.minimumWidth: height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: ListView.view.width
|
||||
height: ListView.view.width
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: roomListItem.hovered || Kirigami.Settings.isMobile
|
||||
Accessible.name: i18n("Configure room")
|
||||
|
||||
contentItem: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
enabled: text.length !== 0
|
||||
text: name ?? ""
|
||||
action: Kirigami.Action {
|
||||
id: optionAction
|
||||
icon.name: "configure"
|
||||
onTriggered: {
|
||||
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
|
||||
configButton.visible = true
|
||||
configButton.down = true
|
||||
menu.closed.connect(function() {
|
||||
configButton.down = undefined
|
||||
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
|
||||
})
|
||||
menu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListContextMenu
|
||||
RoomListContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: normalModeListComponent
|
||||
Kirigami.BasicListItem {
|
||||
id: roomListItem
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
highlighted: listView.currentIndex === index
|
||||
focus: true
|
||||
icon: undefined
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
bold: unreadCount > 0
|
||||
label: name ?? ""
|
||||
subtitle: {
|
||||
let txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
|
||||
if (txt.length) {
|
||||
return txt
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
leading: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
implicitWidth: visible ? height : 0
|
||||
visible: Config.showAvatarInTimeline
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
trailing: RowLayout {
|
||||
QQC2.Label {
|
||||
text: notificationCount
|
||||
visible: notificationCount > 0
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
color: highlightCount > 0 ? "white" : Kirigami.Theme.textColor
|
||||
Layout.minimumWidth: height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: roomListItem.hovered || Kirigami.Settings.isMobile
|
||||
Accessible.name: i18n("Configure room")
|
||||
|
||||
action: Kirigami.Action {
|
||||
id: optionAction
|
||||
icon.name: "configure"
|
||||
onTriggered: {
|
||||
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
|
||||
configButton.visible = true
|
||||
configButton.down = true
|
||||
menu.closed.connect(function() {
|
||||
configButton.down = undefined
|
||||
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
|
||||
})
|
||||
menu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
visible: accountList.count > 1 && !collapsedMode
|
||||
visible: accountList.count > 1
|
||||
height: visible ? implicitHeight : 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
@@ -303,14 +199,16 @@ Kirigami.ScrollablePage {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: accountList
|
||||
model: AccountRegistry
|
||||
model: AccountListModel {
|
||||
id: accountListModel
|
||||
}
|
||||
delegate: Kirigami.BasicListItem {
|
||||
checkable: true
|
||||
checked: Controller.activeConnection && Controller.activeConnection.localUserId === model.connection.localUserId
|
||||
checked: Controller.activeConnection && Controller.activeConnection.localUser.id === model.user.id
|
||||
onClicked: Controller.activeConnection = model.connection
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: model.connection.localUserId
|
||||
text: model.user.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,20 +26,7 @@ Kirigami.ScrollablePage {
|
||||
/// Used to determine if scrolling to the bottom should mark the message as unread
|
||||
property bool hasScrolledUpBefore: false;
|
||||
|
||||
title: currentRoom.htmlSafeDisplayName
|
||||
|
||||
KeyNavigation.left: pageStack.get(0)
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
if(!RoomManager.currentRoom) {
|
||||
if(pageStack.lastItem == page) {
|
||||
pageStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
title: currentRoom.displayName
|
||||
|
||||
signal switchRoomUp()
|
||||
signal switchRoomDown()
|
||||
@@ -57,8 +44,8 @@ Kirigami.ScrollablePage {
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
function onJoinedRoom(room, invited) {
|
||||
if(page.currentRoom.id === invited.id) {
|
||||
function onJoinedRoom(room) {
|
||||
if(room.id === invitation.id) {
|
||||
RoomManager.enterRoom(room);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +53,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
Connections {
|
||||
target: actionsHandler
|
||||
function onShowMessage(messageType, message) {
|
||||
onShowMessage: {
|
||||
page.header.contentItem.text = message;
|
||||
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
|
||||
page.header.contentItem.visible = true;
|
||||
@@ -86,6 +73,8 @@ Kirigami.ScrollablePage {
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: invitation
|
||||
|
||||
property var id
|
||||
|
||||
visible: currentRoom && currentRoom.isInvite
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Accept this invitation?")
|
||||
@@ -94,7 +83,7 @@ Kirigami.ScrollablePage {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Reject")
|
||||
|
||||
onClicked: RoomManager.leaveRoom(page.currentRoom);
|
||||
onClicked: RoomManager.leave(page.currentRoom);
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
@@ -103,6 +92,8 @@ Kirigami.ScrollablePage {
|
||||
|
||||
onClicked: {
|
||||
currentRoom.acceptInvitation();
|
||||
invitation.id = currentRoom.id
|
||||
currentRoom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,9 +148,9 @@ Kirigami.ScrollablePage {
|
||||
|
||||
Item {
|
||||
id: hoverActions
|
||||
property var event: null
|
||||
property var event
|
||||
property bool showEdit: event && (event.author.id === Controller.activeConnection.localUserId && (event.eventType === "emote" || event.eventType === "message"))
|
||||
property var bubble: null
|
||||
property var bubble
|
||||
property var hovered: bubble && bubble.hovered
|
||||
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
|
||||
onVisibleDelayedChanged: if (visibleDelayed) {
|
||||
@@ -174,8 +165,8 @@ Kirigami.ScrollablePage {
|
||||
interval: 200
|
||||
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
|
||||
}
|
||||
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0) - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0)) : 0
|
||||
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
|
||||
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0)) : 0
|
||||
y: bubble ? bubble.mapToItem(page, 0, -Kirigami.Units.largeSpacing - hoverActions.childHeight * 1.5).y : 0
|
||||
visible: false
|
||||
|
||||
property var updateFunction
|
||||
@@ -231,12 +222,14 @@ Kirigami.ScrollablePage {
|
||||
|
||||
ListView {
|
||||
id: messageListView
|
||||
pixelAligned: true
|
||||
visible: !invitation.visible
|
||||
|
||||
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
|
||||
readonly property bool isLoaded: page.width * page.height > 10
|
||||
|
||||
spacing: Config.compactLayout ? 1 : Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
reuseItems: true
|
||||
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
highlightMoveDuration: 500
|
||||
@@ -267,7 +260,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
onAtYEndChanged: if (atYEnd && hasScrolledUpBefore) {
|
||||
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
|
||||
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
}
|
||||
hasScrolledUpBefore = false;
|
||||
@@ -355,7 +348,6 @@ Kirigami.ScrollablePage {
|
||||
leftPadding: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
height: contentItem.height
|
||||
contentItem: StateDelegate { }
|
||||
implicitWidth: messageListView.width - Kirigami.Units.largeSpacing
|
||||
}
|
||||
@@ -373,8 +365,19 @@ Kirigami.ScrollablePage {
|
||||
|
||||
innerObject: TextDelegate {
|
||||
isEmote: true
|
||||
Layout.fillWidth: !Config.showAvatarInTimeline
|
||||
Layout.maximumWidth: emoteContainer.bubbleMaxWidth
|
||||
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,8 +392,19 @@ Kirigami.ScrollablePage {
|
||||
hoverComponent: hoverActions
|
||||
|
||||
innerObject: TextDelegate {
|
||||
Layout.fillWidth: !Config.showAvatarInTimeline
|
||||
Layout.maximumWidth: messageContainer.bubbleMaxWidth
|
||||
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,9 +418,11 @@ Kirigami.ScrollablePage {
|
||||
onReplyClicked: goToEvent(eventID)
|
||||
|
||||
innerObject: TextDelegate {
|
||||
Layout.fillWidth: !Config.compactLayout
|
||||
hasContextMenu: false
|
||||
Layout.fillWidth: !Config.showAvatarInTimeline
|
||||
Layout.maximumWidth: noticeContainer.bubbleMaxWidth
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,23 +439,18 @@ Kirigami.ScrollablePage {
|
||||
innerObject: ImageDelegate {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 15
|
||||
Layout.maximumWidth: imageContainer.bubbleMaxWidth
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: info.h / info.w * width
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openFileContext(model, parent)
|
||||
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openFileContext(model, parent)
|
||||
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
onTapped: {
|
||||
fullScreenImage.createObject(parent, {
|
||||
filename: eventId,
|
||||
localPath: currentRoom.urlToDownload(eventId),
|
||||
blurhash: model.content.info["xyz.amorgan.blurhash"],
|
||||
imageWidth: content.info.w,
|
||||
imageHeight: content.info.h
|
||||
}).showFullScreen();
|
||||
fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,11 +489,11 @@ Kirigami.ScrollablePage {
|
||||
Layout.maximumWidth: audioContainer.bubbleMaxWidth
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openFileContext(model, parent)
|
||||
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openFileContext(model, parent)
|
||||
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,11 +517,11 @@ Kirigami.ScrollablePage {
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openFileContext(model, parent)
|
||||
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openFileContext(model, parent)
|
||||
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,32 +540,16 @@ Kirigami.ScrollablePage {
|
||||
Layout.maximumWidth: fileContainer.bubbleMaxWidth
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openFileContext(model, parent)
|
||||
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openFileContext(model, parent)
|
||||
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "encrypted"
|
||||
delegate: TimelineContainer {
|
||||
id: encryptedContainer
|
||||
width: messageListView.width
|
||||
isLoaded: timelineDelegateChooser.delegateLoaded
|
||||
|
||||
innerObject: EncryptedDelegate {
|
||||
Layout.fillWidth: Config.compactLayout
|
||||
Layout.maximumWidth: encryptedContainer.bubbleMaxWidth
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "readMarker"
|
||||
delegate: QQC2.ItemDelegate {
|
||||
@@ -574,7 +569,7 @@ Kirigami.ScrollablePage {
|
||||
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
|
||||
border.width: Kirigami.Units.devicePixelRatio
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -713,6 +708,12 @@ Kirigami.ScrollablePage {
|
||||
MessageDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceSheet
|
||||
|
||||
MessageSourceSheet {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDelegateContextMenu
|
||||
|
||||
@@ -867,28 +868,27 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
/// Open message context dialog for file and videos
|
||||
function openFileContext(event, file) {
|
||||
function openFileContext(author, message, eventId, source, progressInfo, file) {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(page, {
|
||||
author: event.author,
|
||||
message: event.message,
|
||||
eventId: event.eventId,
|
||||
source: event.toolTip,
|
||||
author: author,
|
||||
message: message,
|
||||
eventId: eventId,
|
||||
source: source,
|
||||
file: file,
|
||||
progressInfo: event.progressInfo,
|
||||
progressInfo: progressInfo,
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
/// Open context menu for normal message
|
||||
function openMessageContext(event, selectedText) {
|
||||
function openMessageContext(author, message, eventId, source, eventType, formattedBody) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(page, {
|
||||
selectedText: selectedText,
|
||||
author: event.author,
|
||||
message: event.message,
|
||||
eventId: event.eventId,
|
||||
formattedBody: event.formattedBody,
|
||||
source: event.toolTip,
|
||||
eventType: event.eventType
|
||||
author: author,
|
||||
message: message,
|
||||
eventId: eventId,
|
||||
formattedBody: formattedBody,
|
||||
source: source,
|
||||
eventType: eventType
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
88
imports/NeoChat/Page/SettingsPage.qml
Normal file
88
imports/NeoChat/Page/SettingsPage.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18n("Settings")
|
||||
|
||||
Kirigami.FormLayout {
|
||||
QQC2.CheckBox {
|
||||
Kirigami.FormData.label: i18n("General settings:")
|
||||
text: i18n("Close to system tray")
|
||||
checked: Config.systemTray
|
||||
visible: Controller.supportSystemTray
|
||||
onToggled: {
|
||||
Config.systemTray = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
// TODO: When there are enough notification and timeline event
|
||||
// settings, make 2 separate groups with FormData labels.
|
||||
Kirigami.FormData.label: i18n("Notifications and events:")
|
||||
text: i18n("Show notifications")
|
||||
checked: Config.showNotifications
|
||||
onToggled: {
|
||||
Config.showNotifications = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Show leave and join events")
|
||||
checked: Config.showLeaveJoinEvent
|
||||
onToggled: {
|
||||
Config.showLeaveJoinEvent = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.RadioButton {
|
||||
Kirigami.FormData.label: i18n("Rooms and private chats:")
|
||||
text: i18n("Separated")
|
||||
checked: !Config.mergeRoomList
|
||||
onToggled: {
|
||||
Config.mergeRoomList = false
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.RadioButton {
|
||||
text: i18n("Intermixed")
|
||||
checked: Config.mergeRoomList
|
||||
onToggled: {
|
||||
Config.mergeRoomList = true
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
Kirigami.FormData.label: i18n("Timeline:")
|
||||
text: i18n("Show User Avatar")
|
||||
checked: Config.showAvatarInTimeline
|
||||
onToggled: {
|
||||
Config.showAvatarInTimeline = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Show Fancy Effects")
|
||||
checked: Config.showFancyEffects
|
||||
onToggled: {
|
||||
Config.showFancyEffects = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Use s/text/replacement syntax to edit your last message")
|
||||
checked: Config.allowQuickEdit
|
||||
onToggled: {
|
||||
Config.allowQuickEdit = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onErrorOccured(message) {
|
||||
onErrorOccured: {
|
||||
headerMessage.text = message;
|
||||
headerMessage.visible = true;
|
||||
headerMessage.type = Kirigami.MessageType.Error;
|
||||
|
||||
@@ -5,5 +5,6 @@ RoomPage 1.0 RoomPage.qml
|
||||
RoomWindow 1.0 RoomWindow.qml
|
||||
JoinRoomPage 1.0 JoinRoomPage.qml
|
||||
InviteUserPage 1.0 InviteUserPage.qml
|
||||
SettingsPage 1.0 SettingsPage.qml
|
||||
ImageEditorPage 1.0 ImageEditorPage.qml
|
||||
|
||||
|
||||
@@ -18,46 +18,6 @@ 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
|
||||
@@ -70,6 +30,7 @@ Kirigami.OverlayDrawer {
|
||||
active: roomDrawer.drawerOpen
|
||||
sourceComponent: ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
Kirigami.AbstractApplicationHeader {
|
||||
Layout.fillWidth: true
|
||||
@@ -86,7 +47,7 @@ Kirigami.OverlayDrawer {
|
||||
icon.name: "list-add-user"
|
||||
text: i18n("Invite")
|
||||
onClicked: {
|
||||
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {room: room})
|
||||
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
|
||||
roomDrawer.close();
|
||||
}
|
||||
}
|
||||
@@ -108,7 +69,12 @@ Kirigami.OverlayDrawer {
|
||||
ToolButton {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
icon.name: 'settings-configure'
|
||||
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
|
||||
onClicked: {
|
||||
roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
|
||||
if (!wideScreen) {
|
||||
roomDrawer.close();
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
text: i18n("Room settings")
|
||||
@@ -117,61 +83,67 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Kirigami.Heading {
|
||||
text: i18n("Room information")
|
||||
level: 3
|
||||
}
|
||||
RowLayout {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
contentItem: ColumnLayout {
|
||||
id: infoLayout
|
||||
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) : ""
|
||||
Kirigami.Heading {
|
||||
text: i18n("Room information")
|
||||
level: 3
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
|
||||
Layout.fillWidth: true
|
||||
level: 1
|
||||
font.bold: true
|
||||
wrapMode: Label.Wrap
|
||||
text: room ? room.displayName : i18n("No name")
|
||||
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) : ""
|
||||
}
|
||||
Label {
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
|
||||
Layout.fillWidth: true
|
||||
level: 1
|
||||
font.bold: true
|
||||
wrapMode: Label.Wrap
|
||||
text: room ? room.displayName : i18n("No name")
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
Layout.fillWidth: true
|
||||
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
|
||||
readonly property var replaceLinks: /\(https:\/\/[^ ]*\)/
|
||||
textFormat: TextEdit.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
selectByMouse: true
|
||||
color: Kirigami.Theme.textColor
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
readOnly: true
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
TextEdit {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 13
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 13
|
||||
Layout.fillWidth: true
|
||||
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
|
||||
readonly property var replaceLinks: /\(https:\/\/[^ ]*\)/
|
||||
textFormat: TextEdit.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
selectByMouse: true
|
||||
color: Kirigami.Theme.textColor
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
readOnly: true
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +153,7 @@ Kirigami.OverlayDrawer {
|
||||
activeFocusOnTab: false
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: room ? i18np("%1 Member", "%1 Members", room.joinedCount) : i18n("No Member Count")
|
||||
text: room ? i18np("%1 Member", "%1 Members", room.totalMemberCount) : i18n("No Member Count")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +161,6 @@ Kirigami.OverlayDrawer {
|
||||
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;
|
||||
@@ -220,7 +187,6 @@ Kirigami.OverlayDrawer {
|
||||
|
||||
sortRole: "perm"
|
||||
filterRole: "name"
|
||||
filterCaseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
@@ -232,7 +198,7 @@ Kirigami.OverlayDrawer {
|
||||
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
|
||||
visible: Config.showAvatarInTimeline
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
source: avatar ? ("image://mxc/" + avatar) : ""
|
||||
@@ -288,6 +254,12 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomSettingDialog
|
||||
|
||||
RoomSettingsDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.18 as Kirigami
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
Kirigami.CategorizedSettings {
|
||||
id: root
|
||||
required property var room
|
||||
objectName: "settingsPage"
|
||||
actions: [
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("General")
|
||||
icon.name: "settings-configure"
|
||||
page: Qt.resolvedUrl("General.qml")
|
||||
initialProperties: {
|
||||
return {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Security")
|
||||
icon.name: "security-low"
|
||||
page: Qt.resolvedUrl("Security.qml")
|
||||
initialProperties: {
|
||||
return {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
property var room
|
||||
|
||||
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
|
||||
readonly property bool canChangeName: room.canSendState("m.room.name")
|
||||
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
|
||||
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
|
||||
|
||||
title: i18n('General')
|
||||
|
||||
ColumnLayout {
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
name: room.name
|
||||
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
|
||||
|
||||
RoundButton {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: Kirigami.Units.gridUnits
|
||||
width: Kirigami.Units.gridUnits
|
||||
icon.name: 'cloud-upload'
|
||||
Accessible.name: i18n("Update avatar")
|
||||
enabled: canChangeAvatar
|
||||
onClicked: {
|
||||
const fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
room.changeAvatar(path)
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: roomNameField
|
||||
text: room.name
|
||||
Kirigami.FormData.label: i18n("Room Name:")
|
||||
enabled: canChangeName
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: roomTopicField
|
||||
Layout.fillWidth: true
|
||||
text: room.topic
|
||||
Kirigami.FormData.label: i18n("Room topic:")
|
||||
enabled: canChangeTopic
|
||||
}
|
||||
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: canonicalAliasComboBox.visible || altAlias.visible
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: canonicalAliasComboBox
|
||||
visible: room.aliases && room.aliases.length
|
||||
Kirigami.FormData.label: i18n("Canonical Alias:")
|
||||
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
|
||||
|
||||
enabled: canChangeCanonicalAlias
|
||||
|
||||
model: room.aliases
|
||||
|
||||
currentIndex: room.aliases.indexOf(room.canonicalAlias)
|
||||
onCurrentIndexChanged: {
|
||||
if (room.canonicalAlias != room.aliases[currentIndex]) {
|
||||
room.setCanonicalAlias(room.aliases[currentIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: altAlias
|
||||
Kirigami.FormData.label: i18n("Other Aliases:")
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.altAliases && room.altAliases.length
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: room.altAliases
|
||||
|
||||
delegate: RowLayout {
|
||||
Layout.maximumWidth: parent.width
|
||||
|
||||
Label {
|
||||
text: modelData
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: ""
|
||||
onClicked: room.removeLocalAlias(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: next.visible || prev.visible
|
||||
}
|
||||
|
||||
Control {
|
||||
id: next
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.predecessorId && room.connection.room(room.predecessorId)
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
text: i18n("This room continues another conversation.")
|
||||
actions: Kirigami.Action {
|
||||
text: i18n("See older messages...")
|
||||
onTriggered: {
|
||||
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
id: prev
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.successorId && room.connection.room(room.successorId)
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
text: i18n("This room has been replaced.")
|
||||
actions: Kirigami.Action {
|
||||
text: i18n("See new room...")
|
||||
onTriggered: {
|
||||
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {}
|
||||
}
|
||||
}
|
||||
|
||||
footer: ToolBar {
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
enabled: room.name !== roomNameField.text || room.topic !== roomTopicField.text
|
||||
text: i18n("Apply")
|
||||
onClicked: {
|
||||
if (room.name != roomNameField.text) {
|
||||
room.setName(roomNameField.text)
|
||||
}
|
||||
|
||||
if (room.topic != roomTopicField.text) {
|
||||
room.setTopic(roomTopicField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
property var room
|
||||
|
||||
title: i18n('Security')
|
||||
|
||||
ColumnLayout {
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.AboutPage {
|
||||
title: i18nc('@title:window', 'About NeoChat')
|
||||
aboutData: Controller.aboutData
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Settings 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18nc('@title:window', 'Appearance')
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
spacing: Kirigami.Units.gridUnit * 2
|
||||
QQC2.ButtonGroup { id: themeGroup }
|
||||
ThemeRadioButton {
|
||||
innerObject: [
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Avatar {
|
||||
color: "#4a5bcc"
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: Config.showAvatarInTimeline
|
||||
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
QQC2.Control {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: 7
|
||||
text: "Paul Müller"
|
||||
color: "#4a5bcc"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta mauris, quis finibus sem suscipit tincidunt."
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 7
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
shadow.size: Kirigami.Units.smallSpacing
|
||||
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Avatar {
|
||||
color: "#9f244b"
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: Config.showAvatarInTimeline
|
||||
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
QQC2.Control {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: 7
|
||||
text: "Jean Paul"
|
||||
color: "#9f244b"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta , quis sem suscipit tincidunt."
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 7
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
shadow.size: Kirigami.Units.smallSpacing
|
||||
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
text: i18n("Bubbles")
|
||||
checked: !Config.compactLayout
|
||||
QQC2.ButtonGroup.group: themeGroup
|
||||
enabled: !Config.isCompactLayoutImmutable
|
||||
|
||||
onToggled: {
|
||||
Config.compactLayout = !checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
ThemeRadioButton {
|
||||
innerObject: [
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Avatar {
|
||||
color: "#4a5bcc"
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: Config.showAvatarInTimeline
|
||||
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: 7
|
||||
text: "Paul Müller"
|
||||
color: "#4a5bcc"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Lorem ipsum dolor sit amet, consectetur elit. Vivamus facilisis porta mauris, finibus sem suscipit tincidunt."
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Avatar {
|
||||
color: "#9f244b"
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: Config.showAvatarInTimeline
|
||||
Layout.preferredWidth: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: 7
|
||||
text: "Jean Paul"
|
||||
color: "#9f244b"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus facilisis porta mauris, quis finibus sem suscipit tincidunt."
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 7
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
text: i18n("Compact")
|
||||
checked: Config.compactLayout
|
||||
QQC2.ButtonGroup.group: themeGroup
|
||||
enabled: !Config.isCompactLayoutImmutable
|
||||
|
||||
onToggled: {
|
||||
Config.compactLayout = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.FormLayout {
|
||||
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
|
||||
onToggled: {
|
||||
Config.showLocalMessagesOnRight = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Settings 1.0
|
||||
|
||||
QQC2.ComboBox {
|
||||
textRole: "display"
|
||||
model: ColorSchemer.model
|
||||
Component.onCompleted: currentIndex = ColorSchemer.indexForScheme(Config.colorScheme);
|
||||
onActivated: {
|
||||
ColorSchemer.apply(currentIndex);
|
||||
Config.colorScheme = ColorSchemer.nameForIndex(currentIndex);
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.Page {
|
||||
title: i18n("Devices")
|
||||
|
||||
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
|
||||
Connections {
|
||||
target: pageSettingStack
|
||||
function onWideModeChanged() {
|
||||
scroll.background.visible = pageSettingStack.wideMode
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ScrollView {
|
||||
id: scroll
|
||||
Component.onCompleted: background.visible = pageSettingStack.wideMode
|
||||
anchors.fill: parent
|
||||
ListView {
|
||||
clip: true
|
||||
model: DevicesModel {
|
||||
id: devices
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
visible: parent.model.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.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: model.displayName
|
||||
subtitle: model.id
|
||||
icon: "network-connect"
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit device name")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
renameSheet.index = model.index
|
||||
renameSheet.name = model.displayName
|
||||
renameSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout device")
|
||||
iconName: "edit-delete-remove"
|
||||
onTriggered: {
|
||||
passwordSheet.index = index
|
||||
passwordSheet.open()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: passwordSheet
|
||||
|
||||
property var index
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import 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.Page {
|
||||
title: i18nc('@title:window', 'Custom Emojis')
|
||||
|
||||
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
|
||||
Connections {
|
||||
target: pageSettingStack
|
||||
function onWideModeChanged() {
|
||||
scroll.background.visible = pageSettingStack.wideMode
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scroll
|
||||
Component.onCompleted: background.visible = pageSettingStack.wideMode
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ListView {
|
||||
clip: true
|
||||
model: CustomEmojiModel {
|
||||
id: emojiModel
|
||||
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
text: i18n("No custom inline stickers found")
|
||||
visible: parent.model.count === 0
|
||||
}
|
||||
|
||||
delegate: Kirigami.BasicListItem {
|
||||
id: del
|
||||
|
||||
required property string name
|
||||
required property url imageURL
|
||||
|
||||
text: name
|
||||
reserveSpaceForSubtitle: true
|
||||
|
||||
leading: Image {
|
||||
width: height
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
source: imageURL
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: Components.ShimmerGradient { }
|
||||
}
|
||||
}
|
||||
|
||||
trailing: QQC2.ToolButton {
|
||||
width: height
|
||||
icon.name: "delete"
|
||||
onClicked: emojiModel.removeEmoji(del.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: pageSettingStack.wideMode
|
||||
sourceComponent: addEmojiComponent
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
id: toolbar
|
||||
width: parent.width
|
||||
visible: !pageSettingStack.wideMode
|
||||
height: visible ? implicitHeight : 0
|
||||
contentItem: Loader {
|
||||
active: !pageSettingStack.wideMode
|
||||
sourceComponent: addEmojiComponent
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
property Component addEmojiComponent: RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: Qt.application.layoutDirection == Qt.LeftToRight
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
id: emojiField
|
||||
placeholderText: i18n("new_emoji_name_here")
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /[a-zA-Z_0-9]*/
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("Add Emoji...")
|
||||
|
||||
enabled: emojiField.text != ""
|
||||
property var fileDialog: null
|
||||
|
||||
onClicked: {
|
||||
if (this.fileDialog != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
|
||||
|
||||
this.fileDialog.chosen.connect((url) => {
|
||||
emojiModel.addEmoji(emojiField.text, url)
|
||||
this.fileDialog = null
|
||||
})
|
||||
this.fileDialog.onRejected.connect(() => {
|
||||
rej()
|
||||
this.fileDialog = null
|
||||
})
|
||||
this.fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: Qt.application.layoutDirection == Qt.RightToLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18nc('@title:window', 'General')
|
||||
ColumnLayout {
|
||||
Kirigami.FormLayout {
|
||||
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 {
|
||||
// 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.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 {
|
||||
text: i18n("Use s/text/replacement syntax to edit your last message")
|
||||
checked: Config.allowQuickEdit
|
||||
enabled: !Config.isAllowQuickEditImmutable
|
||||
onToggled: {
|
||||
Config.allowQuickEdit = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.18 as Kirigami
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
Kirigami.CategorizedSettings {
|
||||
objectName: "settingsPage"
|
||||
actions: [
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("General")
|
||||
icon.name: "org.kde.neochat"
|
||||
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Appearance")
|
||||
icon.name: "preferences-desktop-theme-global"
|
||||
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Accounts")
|
||||
icon.name: "preferences-system-users"
|
||||
page: Qt.resolvedUrl("AccountsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Custom Emoji")
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
page: Qt.resolvedUrl("Emoticons.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Spell Checking")
|
||||
iconName: "tools-check-spelling"
|
||||
page: Qt.resolvedUrl("SonnetConfigPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("Devices")
|
||||
iconName: "network-connect"
|
||||
page: Qt.resolvedUrl("DevicesPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
text: i18n("About NeoChat")
|
||||
icon.name: "help-about"
|
||||
page: Qt.resolvedUrl("About.qml")
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
import QtQml 2.15
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.sonnet 1.0 as Sonnet
|
||||
|
||||
Kirigami.Page {
|
||||
id: page
|
||||
|
||||
/**
|
||||
* This property holds whether the setting on that page are automatically
|
||||
* applied or whether the user can apply then manually. By default, false.
|
||||
*/
|
||||
property bool instantApply: false
|
||||
|
||||
/**
|
||||
* This property holds whether the ListViews inside the page should get
|
||||
* extra padding and a background. By default, use the Kirigami.ApplicationWindow
|
||||
* wideMode value.
|
||||
*/
|
||||
property bool wideMode: QQC2.ApplicationWindow.window.wideMode ?? QQC2.ApplicationWindow.window.width > Kirigami.Units.gridUnit * 40
|
||||
|
||||
/**
|
||||
* Signal emmited when the user decide to discard it's change and close the
|
||||
* setting page.
|
||||
*
|
||||
* For example when using the ConfigPage inside Kirigami PageRow:
|
||||
*
|
||||
* \code
|
||||
* Sonnet.ConfigPage {
|
||||
* onClose: applicationWindow().pageStack.pop();
|
||||
* }
|
||||
* \endcode
|
||||
*/
|
||||
signal close()
|
||||
|
||||
function onBackRequested(event) {
|
||||
if (settings.modified) {
|
||||
applyDialog.open();
|
||||
event.accepted = true;
|
||||
}
|
||||
if (dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
title: i18nc('@window:title', 'Spellchecking')
|
||||
|
||||
QQC2.Dialog {
|
||||
id: applyDialog
|
||||
title: qsTr("Apply Settings")
|
||||
contentItem: QQC2.Label {
|
||||
text: qsTr("The settings of the current module have changed.<br /> Do you want to apply the changes or discard them?")
|
||||
}
|
||||
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel | QQC2.Dialog.Discard
|
||||
|
||||
onAccepted: {
|
||||
settings.save();
|
||||
applyDialog.close();
|
||||
page.close();
|
||||
}
|
||||
onDiscarded: {
|
||||
applyDialog.close();
|
||||
page.close();
|
||||
}
|
||||
onRejected: applyDialog.close();
|
||||
}
|
||||
|
||||
onWideModeChanged: scroll.background.visible = wideMode;
|
||||
|
||||
leftPadding: wideMode ? Kirigami.Units.gridUnit : 0
|
||||
topPadding: wideMode ? Kirigami.Units.gridUnit : 0
|
||||
bottomPadding: wideMode ? Kirigami.Units.gridUnit : 0
|
||||
rightPadding: wideMode ? Kirigami.Units.gridUnit : 0
|
||||
|
||||
property var dialog: null
|
||||
|
||||
Sonnet.Settings {
|
||||
id: settings
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.ComboBox {
|
||||
Kirigami.FormData.label: i18n("Selected default language:")
|
||||
model: settings.dictionaryModel
|
||||
textRole: "display"
|
||||
valueRole: "languageCode"
|
||||
Component.onCompleted: currentIndex = indexOfValue(settings.defaultLanguage);
|
||||
onActivated: {
|
||||
settings.defaultLanguage = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("Open Personal Dictionary")
|
||||
onClicked: if (!dialog) {
|
||||
if (Kirigami.Settings.isMobile) {
|
||||
dialog = mobileSheet.createObject(page, {settings: settings});
|
||||
dialog.open();
|
||||
} else {
|
||||
dialog = desktopSheet.createObject(page, {settings: settings})
|
||||
dialog.show();
|
||||
}
|
||||
} else {
|
||||
if (Kirigami.Settings.isMobile) {
|
||||
dialog.open();
|
||||
} else {
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.CheckBox {
|
||||
Kirigami.FormData.label: i18n("Options:")
|
||||
checked: settings.checkerEnabledByDefault
|
||||
text: i18n("Enable automatic spell checking")
|
||||
onCheckedChanged: {
|
||||
settings.checkerEnabledByDefault = checked;
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.CheckBox {
|
||||
checked: settings.skipUppercase
|
||||
text: i18n("Ignore uppercase words")
|
||||
onCheckedChanged: {
|
||||
settings.skipUppercase = checked;
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.CheckBox {
|
||||
checked: settings.skipRunTogether
|
||||
text: i18n("Ignore hyphenated words")
|
||||
onCheckedChanged: {
|
||||
settings.skipRunTogether = checked;
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.CheckBox {
|
||||
id: autodetectLanguageCheckbox
|
||||
checked: settings.autodetectLanguage
|
||||
text: i18n("Detect language automatically")
|
||||
onCheckedChanged: {
|
||||
settings.autodetectLanguage = checked;
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Spell checking languages")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18n("%1 will provide spell checking and suggestions for the languages listed here when autodetection is enabled.", Qt.application.displayName)
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scroll
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
enabled: autodetectLanguageCheckbox.checked
|
||||
Component.onCompleted: background.visible = wideMode
|
||||
ListView {
|
||||
clip: true
|
||||
model: settings.dictionaryModel
|
||||
delegate: Kirigami.CheckableListItem {
|
||||
label: model.display
|
||||
action: Kirigami.Action {
|
||||
onTriggered: model.checked = checked
|
||||
}
|
||||
checked: model.checked
|
||||
trailing: Kirigami.Icon {
|
||||
source: "favorite"
|
||||
visible: model.isDefault
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
visible: hover.hovered
|
||||
text: qsTr("Default Language")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SheetHeader : RowLayout {
|
||||
QQC2.TextField {
|
||||
id: dictionaryField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: i18n("Add a new word to your personal dictionary…")
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Add word")
|
||||
icon.name: "list-add"
|
||||
enabled: dictionaryField.text.length > 0
|
||||
onClicked: {
|
||||
add(dictionaryField.text);
|
||||
dictionaryField.clear();
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: desktopSheet
|
||||
QQC2.ApplicationWindow {
|
||||
id: window
|
||||
required property Sonnet.Settings settings
|
||||
title: i18n("Spell checking dictionary")
|
||||
width: Kirigami.Units.gridUnit * 20
|
||||
height: Kirigami.Units.gridUnit * 20
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
header: Kirigami.AbstractApplicationHeader {
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
contentItem: SheetHeader {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
QQC2.ScrollView {
|
||||
anchors.fill: parent
|
||||
ListView {
|
||||
model: settings.currentIgnoreList
|
||||
delegate: Kirigami.BasicListItem {
|
||||
label: model.modelData
|
||||
trailing: QQC2.ToolButton {
|
||||
icon.name: "delete"
|
||||
onClicked: {
|
||||
remove(modelData)
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
text: i18n("Delete word")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mobileSheet
|
||||
Kirigami.OverlaySheet {
|
||||
required property Sonnet.Settings settings
|
||||
id: dictionarySheet
|
||||
|
||||
header: SheetHeader {}
|
||||
|
||||
ListView {
|
||||
implicitWidth: Kirigami.Units.gridUnit * 15
|
||||
model: settings.currentIgnoreList
|
||||
delegate: Kirigami.BasicListItem {
|
||||
label: model.modelData
|
||||
trailing: QQC2.ToolButton {
|
||||
icon.name: "delete"
|
||||
onClicked: {
|
||||
remove(modelData)
|
||||
if (instantApply) {
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
text: i18n("Delete word")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
visible: !instantApply
|
||||
height: visible ? implicitHeight : 0
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("Apply")
|
||||
enabled: settings.modified
|
||||
onClicked: settings.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add(word) {
|
||||
const dictionary = settings.currentIgnoreList;
|
||||
dictionary.push(word);
|
||||
settings.currentIgnoreList = dictionary;
|
||||
}
|
||||
|
||||
function remove(word) {
|
||||
settings.currentIgnoreList = settings.currentIgnoreList.filter(function (value, _, _) {
|
||||
return value !== word;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright 2021 Marco Martin <mart@kde.org>
|
||||
// Copyright 2018 Furkan Tokac <furkantokac34@gmail.com>
|
||||
// Copyright 2019 Nate Graham <nate@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
QQC2.RadioButton {
|
||||
id: delegate
|
||||
|
||||
implicitWidth: contentItem.implicitWidth
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
|
||||
property alias innerObject: contentLayout.children
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
Kirigami.ShadowedRectangle {
|
||||
implicitWidth: implicitHeight * 1.6
|
||||
implicitHeight: Kirigami.Units.gridUnit * 6
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 2
|
||||
shadow.size: 10
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
|
||||
color: {
|
||||
if (delegate.checked) {
|
||||
return Kirigami.Theme.highlightColor;
|
||||
} else if (delegate.hovered) {
|
||||
// Match appearance of hovered list items
|
||||
return Qt.rgba(Kirigami.Theme.highlightColor.r,
|
||||
Kirigami.Theme.highlightColor.g,
|
||||
Kirigami.Theme.highlightColor.b,
|
||||
0.5);
|
||||
} else {
|
||||
return Kirigami.Theme.backgroundColor;
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: label
|
||||
Layout.fillWidth: true
|
||||
text: delegate.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
indicator: Item {}
|
||||
background: Item {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module NeoChat.Settings
|
||||
ThemeRadioButton 1.0 ThemeRadioButton.qml
|
||||
SettingsPage 1.0 SettingsPage.qml
|
||||
SonnetConfigPage 1.0 SonnetConfigPage.qml
|
||||
@@ -11,16 +11,13 @@ Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[it]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
Name[nl]=NeoChat
|
||||
Name[nn]=NeoChat
|
||||
Name[pa]=ਨਿਓ-ਚੈਟ
|
||||
Name[pl]=NeoChat
|
||||
Name[pt]=NeoChat
|
||||
Name[pt_BR]=NeoChat
|
||||
Name[ro]=NeoChat
|
||||
Name[sk]=NeoChat
|
||||
@@ -33,24 +30,22 @@ Name[zh_CN]=NeoChat
|
||||
DesktopEntry=org.kde.neochat
|
||||
Comment=A client for matrix, the decentralized communication protocol
|
||||
Comment[az]=Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu
|
||||
Comment[ca]=Un client per a Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[ca@valencia]=Un client per a Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[ca@valencia]=Un client per al Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[cs]=Klient pro decentralizovaný komunikační protokol matrix
|
||||
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
|
||||
Comment[en_GB]=A client for matrix, the decentralised communication protocol
|
||||
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
|
||||
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
|
||||
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
|
||||
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
|
||||
Comment[hu]=Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz
|
||||
Comment[ia]=Un cliente per Matrix, le protocollo de communication decentralisate
|
||||
Comment[ia]=Un cliente per matrix, le protocollo de communication decentralisate
|
||||
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
|
||||
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
|
||||
Comment[lt]=Matrix decentralizuoto bendravimo protokolo kliento programa
|
||||
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
|
||||
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldingsprotokollen.
|
||||
Comment[pa]=ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ
|
||||
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
|
||||
Comment[pt]=Um cliente para o Matrix, o protocolo descentralizado de comunicações
|
||||
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado
|
||||
Comment[ro]=Client pentru Matrix, protocolul de comunicare descentralizată
|
||||
Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol
|
||||
@@ -77,7 +72,6 @@ Name[hu]=Új üzenet
|
||||
Name[ia]=Nove message
|
||||
Name[it]=Nuovo messaggio
|
||||
Name[ko]=새 메시지
|
||||
Name[lt]=Nauja žinutė
|
||||
Name[nl]=Nieuw bericht
|
||||
Name[nn]=Ny melding
|
||||
Name[pa]=ਨਵਾਂ ਸੁਨੇਹਾ
|
||||
@@ -103,10 +97,9 @@ Comment[eu]=Mezu berri bat dago
|
||||
Comment[fi]=Saapui uusi viesti
|
||||
Comment[fr]=Il y a un nouveau message
|
||||
Comment[hu]=Új üzenet érkezett
|
||||
Comment[ia]=Il ha un nove message
|
||||
Comment[ia]=Isto es un nove message
|
||||
Comment[it]=È presente un nuovo messaggio
|
||||
Comment[ko]=새 메시지가 있음
|
||||
Comment[lt]=Yra nauja žinutė
|
||||
Comment[nl]=Er is een nieuw bericht
|
||||
Comment[nn]=Du har ei ny melding
|
||||
Comment[pa]=ਨਵਾਂ ਸੁਨੇਹਾ ਹੈ
|
||||
@@ -122,40 +115,3 @@ Comment[uk]=Надійшло нове повідомлення
|
||||
Comment[x-test]=xxThere is a new messagexx
|
||||
Comment[zh_CN]=有新消息
|
||||
Action=Popup
|
||||
|
||||
[Event/invite]
|
||||
Name=New Invitation
|
||||
Name[az]=Yeni dəvət
|
||||
Name[ca]=Invitació nova
|
||||
Name[ca@valencia]=Invitació nova
|
||||
Name[es]=Nueva invitación
|
||||
Name[fr]=Nouvelle invitation
|
||||
Name[ia]=Nove invitation
|
||||
Name[it]=Nuovo invito
|
||||
Name[ko]=새 초대장
|
||||
Name[nl]=Nieuwe uitnodiging
|
||||
Name[pl]=Nowe zaproszenie
|
||||
Name[pt]=Novo Convite
|
||||
Name[pt_BR]=Novo convite
|
||||
Name[sl]=Novo povabilo
|
||||
Name[sv]=Ny inbjudan
|
||||
Name[uk]=Нове запрошення
|
||||
Name[x-test]=xxNew Invitationxx
|
||||
Comment=There is a new invitation to a room
|
||||
Comment[az]=Otağa bir yeni dəvət var
|
||||
Comment[ca]=Hi ha una invitació nova a una sala
|
||||
Comment[ca@valencia]=Hi ha una invitació nova a una sala
|
||||
Comment[es]=Hay una nueva invitación a una sala
|
||||
Comment[fr]=Il y a une nouvelle invitation dans un salon.
|
||||
Comment[ia]=Il ha un nove invitation a un sala
|
||||
Comment[it]=È presente un nuovo invito a una stanza
|
||||
Comment[ko]=새로운 대화방 초대장을 받음
|
||||
Comment[nl]=Er is een nieuwe uitnodiging naar een room
|
||||
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
|
||||
Comment[pt]=Existe um novo convite para uma sala
|
||||
Comment[pt_BR]=Existe um novo convite para uma sala
|
||||
Comment[sl]=Tam je novo povabilo v sobo
|
||||
Comment[sv]=Det finns en ny inbjudan till ett rum
|
||||
Comment[uk]=У кімнаті нове запрошення
|
||||
Comment[x-test]=xxThere is a new invitation to a roomxx
|
||||
Action=Popup
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
- SPDX-License-Identifier: CC0-1.0
|
||||
- SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
|
||||
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
-->
|
||||
<component type="desktop">
|
||||
<id>org.kde.neochat</id>
|
||||
<provides>
|
||||
@@ -20,16 +15,13 @@
|
||||
<name xml:lang="eu">NeoChat</name>
|
||||
<name xml:lang="fi">NeoChat</name>
|
||||
<name xml:lang="fr">NeoChat</name>
|
||||
<name xml:lang="hu">NeoChat</name>
|
||||
<name xml:lang="ia">Neochat</name>
|
||||
<name xml:lang="id">NeoChat</name>
|
||||
<name xml:lang="it">NeoChat</name>
|
||||
<name xml:lang="ko">NeoChat</name>
|
||||
<name xml:lang="nl">NeoChat</name>
|
||||
<name xml:lang="nn">NeoChat</name>
|
||||
<name xml:lang="pa">ਨਿਓ-ਚੈਟ</name>
|
||||
<name xml:lang="pl">NeoChat</name>
|
||||
<name xml:lang="pt">NeoChat</name>
|
||||
<name xml:lang="pt-BR">NeoChat</name>
|
||||
<name xml:lang="sk">NeoChat</name>
|
||||
<name xml:lang="sl">NeoChat</name>
|
||||
@@ -50,7 +42,7 @@
|
||||
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
|
||||
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
|
||||
<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="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="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
|
||||
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
|
||||
@@ -77,13 +69,11 @@
|
||||
<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>
|
||||
<p xml:lang="fi">NeoChat on Matrix-asiakas. Sillä voi lähettää perheelle, tuttaville ja kavereille tekstiviestejä sekä video- ja äänitiedostoja Matrix-yhteyskäytännöllä.</p>
|
||||
<p xml:lang="fr">NeoChat est un client Matrix. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis en utilisant le protocole Matrix.</p>
|
||||
<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="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="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="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>
|
||||
@@ -100,14 +90,12 @@
|
||||
<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>
|
||||
<p xml:lang="fi">Matrix on hajautettu viestintäyhteyskäytäntö, joka antaa hallinnan takaisin käyttäjille. NeoChat tarjoaa nykyisellään valtaosan yhteyskäytännöstä salattuja keskustelu- ja videokeskusteluja lukuun ottamatta.</p>
|
||||
<p xml:lang="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions cryptées et du chat vidéo.</p>
|
||||
<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="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions chiffrées et du chat vidéo.</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="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="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="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>
|
||||
@@ -125,13 +113,11 @@
|
||||
<p xml:lang="eu">NeoChat mugikorretan eta mahaigainean dabil, erabiltzaile esperientzia koherentea eskainiz.</p>
|
||||
<p xml:lang="fi">NeoChat toimii sekä mobiili- että työpöytäalustoilla tarjoten yhdenmukaisen käyttökokemuksen.</p>
|
||||
<p xml:lang="fr">NeoChat fonctionne aussi bien sur les mobiles que sur les ordinateurs de bureau, tout en offrant une expérience utilisateur cohérente.</p>
|
||||
<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="it">NeoChat funziona sia su dispositivi mobili che desktop, fornendo un'esperienza utente coerente.</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="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>
|
||||
@@ -188,29 +174,6 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="21.12" date="2021-12-07">
|
||||
<description>
|
||||
<p>NeoChat 21.12 brings lots of new features and fixes</p>
|
||||
<ul>
|
||||
<li>Solved various problems related to login, logout and account switching</li>
|
||||
<li>Fixed a few problems in the timeline layout</li>
|
||||
<li>Added Spell checking while writing a message</li>
|
||||
<li>Improved Settings pages</li>
|
||||
<li>Many improvements to the android and general mobile support</li>
|
||||
<li>Show blurhashes while images load</li>
|
||||
<li>Support showing custom emojis</li>
|
||||
<li>Added a global menu</li>
|
||||
<li>Added support for spoilers</li>
|
||||
<li>Added a quick switcher to switch between rooms</li>
|
||||
<li>Added support for an optional fancy blur background effect</li>
|
||||
<li>Resizable left and right drawers</li>
|
||||
<li>Added Syntax highlighting in raw json messages</li>
|
||||
<li>Better wayland support</li>
|
||||
<li>Improved file reception and download</li>
|
||||
</ul>
|
||||
</description>
|
||||
<url>https://www.plasma-mobile.org/2021/12/07/plasma-mobile-gear-21-12/</url>
|
||||
</release>
|
||||
<release version="1.2.0" date="2021-06-01">
|
||||
<description>
|
||||
<p>NeoChat 1.2 brings a major redesign of the user interface. The chat page is now using bubbles for the messages and the input component was completely rewritten with a nicer look as well.</p>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
[Desktop Entry]
|
||||
Name=NeoChat
|
||||
Name[az]=NeoChat
|
||||
@@ -12,16 +10,13 @@ Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[it]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
Name[nl]=NeoChat
|
||||
Name[nn]=NeoChat
|
||||
Name[pa]=ਨਿਓ-ਚੈਟ
|
||||
Name[pl]=NeoChat
|
||||
Name[pt]=NeoChat
|
||||
Name[pt_BR]=NeoChat
|
||||
Name[ro]=NeoChat
|
||||
Name[sk]=NeoChat
|
||||
@@ -33,8 +28,8 @@ Name[x-test]=xxNeoChatxx
|
||||
Name[zh_CN]=NeoChat
|
||||
GenericName=Matrix Client
|
||||
GenericName[az]=Matrix Müştərisi
|
||||
GenericName[ca]=Client de Matrix
|
||||
GenericName[ca@valencia]=Client de Matrix
|
||||
GenericName[ca]=Client del Matrix
|
||||
GenericName[ca@valencia]=Client del Matrix
|
||||
GenericName[cs]=Klient protokolu Matrix
|
||||
GenericName[de]=Matrix-Programm
|
||||
GenericName[en_GB]=Matrix Client
|
||||
@@ -43,10 +38,9 @@ GenericName[eu]=Matrix bezeroa
|
||||
GenericName[fi]=Matrix-asiakas
|
||||
GenericName[fr]=Client « Matrix »
|
||||
GenericName[hu]=Matrix kliens
|
||||
GenericName[ia]=Cliente de Matrice
|
||||
GenericName[ia]=Cliente de Matrix
|
||||
GenericName[it]=Client Matrix
|
||||
GenericName[ko]=Matrix 클라이언트
|
||||
GenericName[lt]=Matrix kliento programą
|
||||
GenericName[nl]=Matrix-client
|
||||
GenericName[nn]=Matrix-klient
|
||||
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ
|
||||
@@ -75,7 +69,6 @@ Comment[hu]=Kliens a Matrix protokollhoz
|
||||
Comment[ia]=Cliente per le protocollo de Matrix
|
||||
Comment[it]=Client per il protocollo Matrix
|
||||
Comment[ko]=Matrix 프로토콜용 클라이언트
|
||||
Comment[lt]=Matrix protokolo kliento programa
|
||||
Comment[nl]=Client voor het Matrix-protocol
|
||||
Comment[nn]=Lynmeldingsklient for Matrix-protokollen
|
||||
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
|
||||
|
||||
208
qml/main.qml
208
qml/main.qml
@@ -27,12 +27,9 @@ Kirigami.ApplicationWindow {
|
||||
onClosing: Controller.saveWindowGeometry(root)
|
||||
|
||||
pageStack.initialPage: LoadingPage {}
|
||||
pageStack.globalToolBar.canContainHandles: true
|
||||
|
||||
property bool roomListLoaded: false
|
||||
|
||||
property RoomPage roomPage
|
||||
|
||||
Connections {
|
||||
target: root.quitAction
|
||||
function onTriggered() {
|
||||
@@ -40,11 +37,6 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !Kirigami.Settings.isMobile
|
||||
source: Qt.resolvedUrl("qrc:/imports/NeoChat/Menu/GlobalMenu.qml")
|
||||
}
|
||||
|
||||
// This timer allows to batch update the window size change to reduce
|
||||
// the io load and also work around the fact that x/y/width/height are
|
||||
// changed when loading the page and overwrite the saved geometry from
|
||||
@@ -60,35 +52,39 @@ Kirigami.ApplicationWindow {
|
||||
onXChanged: saveWindowGeometryTimer.restart()
|
||||
onYChanged: saveWindowGeometryTimer.restart()
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
quickView.item.open()
|
||||
|
||||
/// Setup keyboard navigation to the room page.
|
||||
function connectRoomToSignal(item) {
|
||||
if (!roomListLoaded) {
|
||||
console.log("Should not happen: no room list page but room page");
|
||||
}
|
||||
}
|
||||
const roomList = pageStack.get(0);
|
||||
item.switchRoomUp.connect(function() {
|
||||
roomList.goToNextRoom();
|
||||
});
|
||||
|
||||
Loader {
|
||||
id: quickView
|
||||
|
||||
active: !Kirigami.Settings.isMobile
|
||||
sourceComponent: QuickSwitcher { }
|
||||
item.switchRoomDown.connect(function() {
|
||||
roomList.goToPreviousRoom();
|
||||
});
|
||||
item.forceActiveFocus();
|
||||
item.KeyNavigation.left = pageStack.get(0);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
|
||||
function onPushRoom(room, event) {
|
||||
root.roomPage = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
|
||||
root.roomPage.forceActiveFocus();
|
||||
const roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
|
||||
connectRoomToSignal(roomItem);
|
||||
if (event.length > 0) {
|
||||
roomPage.goToEvent(event);
|
||||
roomItem.goToEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
function onReplaceRoom(room, event) {
|
||||
const roomItem = pageStack.get(pageStack.depth - 1);
|
||||
pageStack.currentIndex = pageStack.depth - 1;
|
||||
root.roomPage.forceActiveFocus();
|
||||
connectRoomToSignal(roomItem);
|
||||
if (event.length > 0) {
|
||||
roomItem.goToEvent(event);
|
||||
}
|
||||
@@ -144,11 +140,11 @@ Kirigami.ApplicationWindow {
|
||||
root.show()
|
||||
root.raise()
|
||||
root.requestActivate()
|
||||
Controller.raiseWindow(root)
|
||||
}
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
id: contextDrawer
|
||||
contentItem.implicitWidth: columnWidth
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
modal: !root.wideScreen || !enabled
|
||||
onEnabledChanged: drawerOpen = enabled && !modal
|
||||
@@ -157,70 +153,7 @@ Kirigami.ApplicationWindow {
|
||||
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||
}
|
||||
|
||||
readonly property int defaultPageWidth: Kirigami.Units.gridUnit * 17
|
||||
readonly property int minPageWidth: Kirigami.Units.gridUnit * 10
|
||||
readonly property int collapsedPageWidth: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3
|
||||
readonly property bool shouldUseSidebars: RoomManager.hasOpenRoom && (Config.roomListPageWidth > minPageWidth ? root.width >= Kirigami.Units.gridUnit * 35 : root.width > Kirigami.Units.gridUnit * 27) && roomListLoaded
|
||||
readonly property int pageWidth: {
|
||||
if (Config.roomListPageWidth === -1) {
|
||||
return defaultPageWidth;
|
||||
} else if (Config.roomListPageWidth < minPageWidth) {
|
||||
return collapsedPageWidth;
|
||||
} else {
|
||||
return Config.roomListPageWidth;
|
||||
}
|
||||
}
|
||||
|
||||
pageStack.defaultColumnWidth: pageWidth
|
||||
pageStack.columnView.columnResizeMode: shouldUseSidebars ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn
|
||||
|
||||
MouseArea {
|
||||
visible: root.pageStack.wideMode
|
||||
z: 500
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
x: root.pageStack.defaultColumnWidth - (width / 2)
|
||||
width: 2
|
||||
|
||||
property int _lastX: -1
|
||||
enabled: !Kirigami.Settings.isMobile
|
||||
|
||||
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
|
||||
|
||||
onPressed: _lastX = mouseX
|
||||
onReleased: Config.save();
|
||||
|
||||
onPositionChanged: {
|
||||
if (_lastX == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouse.x > _lastX) {
|
||||
// we moved to the right
|
||||
if (Config.roomListPageWidth === root.collapsedPageWidth && root.pageWidth + (mouse.x - _lastX) >= root.minPageWidth) {
|
||||
// Here we get back directly to a more wide mode.
|
||||
Config.roomListPageWidth = root.minPageWidth;
|
||||
if (root.width < Kirigami.Units.gridUnit * 35) {
|
||||
root.width = Kirigami.Units.gridUnit * 35;
|
||||
}
|
||||
} else if (Config.roomListPageWidth !== root.collapsedPageWidth) {
|
||||
// Increase page width
|
||||
Config.roomListPageWidth = Math.min(root.defaultPageWidth, root.pageWidth + (mouse.x - _lastX));
|
||||
}
|
||||
} else if (mouse.x < _lastX) {
|
||||
const tmpWidth = root.pageWidth - (_lastX - mouse.x);
|
||||
|
||||
if (tmpWidth < root.minPageWidth) {
|
||||
Config.roomListPageWidth = root.collapsedPageWidth;
|
||||
} else {
|
||||
Config.roomListPageWidth = tmpWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageStack.columnView.columnWidth: Kirigami.Units.gridUnit * 17
|
||||
|
||||
globalDrawer: Kirigami.GlobalDrawer {
|
||||
property bool hasLayer
|
||||
@@ -249,13 +182,32 @@ Kirigami.ApplicationWindow {
|
||||
shortcut: StandardKey.New
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Accounts")
|
||||
icon.name: "im-user"
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/AccountsPage.qml")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Accounts") && Controller.accountCount > 0
|
||||
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Devices")
|
||||
iconName: "network-connect"
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/DevicesPage.qml")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Devices") && Controller.accountCount > 0
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Settings")
|
||||
icon.name: "settings-configure"
|
||||
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
|
||||
shortcut: StandardKey.Preferences
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("About NeoChat")
|
||||
icon.name: "help-about"
|
||||
onTriggered: pushReplaceLayer(aboutPage)
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("About")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
icon.name: "list-remove-user"
|
||||
@@ -271,51 +223,27 @@ Kirigami.ApplicationWindow {
|
||||
]
|
||||
}
|
||||
|
||||
Component.onCompleted: Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
Connections {
|
||||
target: Config
|
||||
function onBlurChanged() {
|
||||
Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
}
|
||||
function onCompactLayoutChanged() {
|
||||
Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
Component {
|
||||
id: aboutPage
|
||||
Kirigami.AboutPage {
|
||||
aboutData: Controller.aboutData
|
||||
}
|
||||
}
|
||||
|
||||
// blur effect
|
||||
color: Config.blur && !Config.compactLayout ? "transparent" : Kirigami.Theme.backgroundColor
|
||||
|
||||
// we need to apply the translucency effect separately on top of the color
|
||||
background: Rectangle {
|
||||
color: Config.blur && !Config.compactLayout ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1 - Config.transparency) : "transparent"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListComponent
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
|
||||
Connections {
|
||||
target: root.roomPage
|
||||
function onSwitchRoomUp() {
|
||||
roomList.goToNextRoom();
|
||||
}
|
||||
function onSwitchRoomDown() {
|
||||
roomList.goToPreviousRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AccountRegistry
|
||||
function onRowsRemoved() {
|
||||
if (AccountRegistry.rowCount() === 0) {
|
||||
RoomManager.reset();
|
||||
pageStack.clear();
|
||||
roomListLoaded = false;
|
||||
pageStack.push("qrc:/imports/NeoChat/Page/WelcomePage.qml");
|
||||
}
|
||||
target: LoginHelper
|
||||
function onInitialSyncFinished() {
|
||||
pageStack.replace(roomListComponent, {
|
||||
activeConnection: Controller.activeConnection
|
||||
});
|
||||
roomListLoaded = true;
|
||||
RoomManager.loadInitialRoom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,9 +251,12 @@ Kirigami.ApplicationWindow {
|
||||
target: Controller
|
||||
|
||||
function onInitiated() {
|
||||
if (RoomManager.hasOpenRoom) {
|
||||
return;
|
||||
}
|
||||
if (Controller.accountCount === 0) {
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
|
||||
} else if (!roomListLoaded) {
|
||||
} else {
|
||||
pageStack.replace(roomListComponent, {
|
||||
activeConnection: Controller.activeConnection
|
||||
});
|
||||
@@ -334,13 +265,28 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function onBusyChanged() {
|
||||
if(!Controller.busy && roomListLoaded === false) {
|
||||
pageStack.replace(roomListComponent);
|
||||
roomListLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onConnectionDropped() {
|
||||
if (Controller.accountCount === 0) {
|
||||
RoomManager.reset();
|
||||
pageStack.clear();
|
||||
roomListLoaded = false;
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
|
||||
}
|
||||
}
|
||||
|
||||
function onGlobalErrorOccured(error, detail) {
|
||||
showPassiveNotification(i18n("%1: %2", error, detail));
|
||||
showPassiveNotification(i18nc("%1: %2", error, detail));
|
||||
}
|
||||
|
||||
function onShowWindow() {
|
||||
root.showWindow()
|
||||
root.raise()
|
||||
}
|
||||
|
||||
function onUserConsentRequired(url) {
|
||||
@@ -351,7 +297,7 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
function onDirectChatAvailable(directChat) {
|
||||
onDirectChatAvailable: {
|
||||
RoomManager.enterRoom(Controller.activeConnection.room(directChat.id));
|
||||
}
|
||||
}
|
||||
@@ -361,7 +307,9 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
property string url: ""
|
||||
|
||||
title: i18n("User consent")
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("User consent")
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: label
|
||||
@@ -400,7 +348,9 @@ Kirigami.ApplicationWindow {
|
||||
required property var user;
|
||||
|
||||
parent: QQC2.ApplicationWindow.overlay
|
||||
title: i18n("Start a chat")
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Start a chat")
|
||||
}
|
||||
contentItem: QQC2.Label {
|
||||
text: i18n("Do you want to start a chat with %1?", user.displayName)
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
23
res.qrc
23
res.qrc
@@ -6,20 +6,18 @@
|
||||
<file>imports/NeoChat/Page/RoomListPage.qml</file>
|
||||
<file>imports/NeoChat/Page/RoomPage.qml</file>
|
||||
<file>imports/NeoChat/Page/RoomWindow.qml</file>
|
||||
<file>imports/NeoChat/Page/AccountsPage.qml</file>
|
||||
<file>imports/NeoChat/Page/JoinRoomPage.qml</file>
|
||||
<file>imports/NeoChat/Page/InviteUserPage.qml</file>
|
||||
<file>imports/NeoChat/Page/SettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Page/StartChatPage.qml</file>
|
||||
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
|
||||
<file>imports/NeoChat/Page/DevicesPage.qml</file>
|
||||
<file>imports/NeoChat/Page/WelcomePage.qml</file>
|
||||
<file>imports/NeoChat/RoomSettings/General.qml</file>
|
||||
<file>imports/NeoChat/RoomSettings/Security.qml</file>
|
||||
<file>imports/NeoChat/RoomSettings/Categories.qml</file>
|
||||
<file>imports/NeoChat/Component/qmldir</file>
|
||||
<file>imports/NeoChat/Component/FullScreenImage.qml</file>
|
||||
<file>imports/NeoChat/Component/FancyEffectsContainer.qml</file>
|
||||
<file>imports/NeoChat/Component/TypingPane.qml</file>
|
||||
<file>imports/NeoChat/Component/ShimmerGradient.qml</file>
|
||||
<file>imports/NeoChat/Component/QuickSwitcher.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/ChatBox.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/ChatBar.qml</file>
|
||||
@@ -40,7 +38,6 @@
|
||||
<file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file>
|
||||
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
|
||||
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file>
|
||||
<file>imports/NeoChat/Component/Timeline/EncryptedDelegate.qml</file>
|
||||
<file>imports/NeoChat/Component/Login/qmldir</file>
|
||||
<file>imports/NeoChat/Component/Login/LoginStep.qml</file>
|
||||
<file>imports/NeoChat/Component/Login/Login.qml</file>
|
||||
@@ -53,13 +50,12 @@
|
||||
<file>imports/NeoChat/Panel/qmldir</file>
|
||||
<file>imports/NeoChat/Panel/RoomDrawer.qml</file>
|
||||
<file>imports/NeoChat/Dialog/qmldir</file>
|
||||
<file>imports/NeoChat/Dialog/RoomSettingsDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/UserDetailDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/EmojiDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/OpenFileDialog.qml</file>
|
||||
<file>imports/NeoChat/Menu/qmldir</file>
|
||||
<file>imports/NeoChat/Menu/GlobalMenu.qml</file>
|
||||
<file>imports/NeoChat/Menu/EditMenu.qml</file>
|
||||
<file>imports/NeoChat/Menu/Timeline/qmldir</file>
|
||||
<file>imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml</file>
|
||||
<file>imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml</file>
|
||||
@@ -68,16 +64,5 @@
|
||||
<file>qtquickcontrols2.conf</file>
|
||||
<file>imports/NeoChat/Component/glowdot.png</file>
|
||||
<file>imports/NeoChat/Component/confetti.png</file>
|
||||
<file>imports/NeoChat/Settings/SettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/ThemeRadioButton.qml</file>
|
||||
<file>imports/NeoChat/Settings/ColorScheme.qml</file>
|
||||
<file>imports/NeoChat/Settings/GeneralSettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/Emoticons.qml</file>
|
||||
<file>imports/NeoChat/Settings/AppearanceSettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/AccountsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/DevicesPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/About.qml</file>
|
||||
<file>imports/NeoChat/Settings/SonnetConfigPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/qmldir</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
add_executable(neochat
|
||||
accountlistmodel.cpp
|
||||
controller.cpp
|
||||
actionshandler.cpp
|
||||
emojimodel.cpp
|
||||
customemojimodel.cpp
|
||||
customemojimodel+network.cpp
|
||||
clipboard.cpp
|
||||
matriximageprovider.cpp
|
||||
messageeventmodel.cpp
|
||||
@@ -31,18 +30,11 @@ add_executable(neochat
|
||||
stickerevent.cpp
|
||||
chatboxhelper.cpp
|
||||
commandmodel.cpp
|
||||
webshortcutmodel.cpp
|
||||
spellcheckhighlighter.cpp
|
||||
blurhash.cpp
|
||||
blurhashimageprovider.cpp
|
||||
joinrulesevent.cpp
|
||||
../res.qrc
|
||||
)
|
||||
|
||||
if(Quotient_VERSION_MINOR GREATER 6)
|
||||
target_compile_definitions(neochat PRIVATE QUOTIENT_07)
|
||||
else()
|
||||
target_sources(neochat PRIVATE accountregistry.cpp)
|
||||
endif()
|
||||
|
||||
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
||||
@@ -50,19 +42,11 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
||||
target_sources(neochat PRIVATE ${NEOCHAT_ICON})
|
||||
|
||||
if(NOT ANDROID)
|
||||
target_sources(neochat PRIVATE colorschemer.cpp)
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat PRIVATE trayicon_sni.cpp)
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp)
|
||||
endif()
|
||||
target_link_libraries(neochat PRIVATE KF5::ConfigWidgets KF5::WindowSystem KF5::SonnetCore)
|
||||
target_compile_definitions(neochat PRIVATE -DHAVE_COLORSCHEME)
|
||||
target_compile_definitions(neochat PRIVATE -DHAVE_WINDOWSYSTEM)
|
||||
target_sources(neochat PRIVATE trayicon.cpp)
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PRIVATE Qt::Quick Qt::Qml Qt::Gui Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::QCoro)
|
||||
target_link_libraries(neochat PRIVATE Qt::Quick Qt::Qml Qt::Gui Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES})
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
@@ -72,7 +56,6 @@ endif()
|
||||
if(ANDROID)
|
||||
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
|
||||
kirigami_package_breeze_icons(ICONS
|
||||
"arrow-down"
|
||||
"help-about"
|
||||
"im-user"
|
||||
"im-invisible-user"
|
||||
@@ -106,13 +89,9 @@ if(ANDROID)
|
||||
"gtk-quit"
|
||||
"compass"
|
||||
"network-connect"
|
||||
"list-remove-user"
|
||||
"org.kde.neochat"
|
||||
"preferences-system-users"
|
||||
"preferences-desktop-theme-global"
|
||||
)
|
||||
else()
|
||||
target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::KIOWidgets)
|
||||
target_link_libraries(neochat PRIVATE Qt5::Widgets)
|
||||
endif()
|
||||
|
||||
if(TARGET KF5::DBusAddons)
|
||||
@@ -120,8 +99,4 @@ if(TARGET KF5::DBusAddons)
|
||||
target_compile_definitions(neochat PRIVATE -DHAVE_KDBUSADDONS)
|
||||
endif()
|
||||
|
||||
if (TARGET KF5::KIOWidgets)
|
||||
target_compile_definitions(neochat PRIVATE -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
80
src/accountlistmodel.cpp
Normal file
80
src/accountlistmodel.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "accountlistmodel.h"
|
||||
|
||||
#include "room.h"
|
||||
|
||||
AccountListModel::AccountListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
m_connections.clear();
|
||||
m_connections += Controller::instance().connections();
|
||||
|
||||
connect(&Controller::instance(), &Controller::connectionAdded, this, [=](Connection *conn) {
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count());
|
||||
m_connections.append(conn);
|
||||
endInsertRows();
|
||||
});
|
||||
connect(&Controller::instance(), &Controller::connectionDropped, this, [=](Connection *conn) {
|
||||
if (!conn) {
|
||||
qDebug() << "Trying to remove null connection";
|
||||
return;
|
||||
}
|
||||
conn->disconnect(this);
|
||||
const auto it = std::find(m_connections.begin(), m_connections.end(), conn);
|
||||
if (it == m_connections.end()) {
|
||||
return; // Already deleted, nothing to do
|
||||
}
|
||||
const int row = it - m_connections.begin();
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_connections.erase(it);
|
||||
endRemoveRows();
|
||||
});
|
||||
}
|
||||
|
||||
QVariant AccountListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= m_connections.count()) {
|
||||
qDebug() << "AccountListModel, something's wrong: index.row() >= "
|
||||
"m_users.count()";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto m_connection = m_connections.at(index.row());
|
||||
|
||||
if (role == UserRole) {
|
||||
return QVariant::fromValue(m_connection->user());
|
||||
}
|
||||
if (role == ConnectionRole) {
|
||||
return QVariant::fromValue(m_connection);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int AccountListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_connections.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[UserRole] = "user";
|
||||
roles[ConnectionRole] = "connection";
|
||||
|
||||
return roles;
|
||||
}
|
||||
29
src/accountlistmodel.h
Normal file
29
src/accountlistmodel.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
|
||||
class AccountListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum EventRoles {
|
||||
UserRole = Qt::UserRole + 1,
|
||||
ConnectionRole,
|
||||
};
|
||||
|
||||
AccountListModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = UserRole) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
QVector<Connection *> m_connections;
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "accountregistry.h"
|
||||
|
||||
#include "connection.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
void AccountRegistry::add(Connection *c)
|
||||
{
|
||||
if (m_accounts.contains(c))
|
||||
return;
|
||||
beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
|
||||
m_accounts += c;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void AccountRegistry::drop(Connection *c)
|
||||
{
|
||||
beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
|
||||
m_accounts.removeOne(c);
|
||||
endRemoveRows();
|
||||
Q_ASSERT(!m_accounts.contains(c));
|
||||
}
|
||||
|
||||
bool AccountRegistry::isLoggedIn(const QString &userId) const
|
||||
{
|
||||
return std::any_of(m_accounts.cbegin(), m_accounts.cend(), [&userId](Connection *a) {
|
||||
return a->userId() == userId;
|
||||
});
|
||||
}
|
||||
|
||||
bool AccountRegistry::contains(Connection *c) const
|
||||
{
|
||||
return m_accounts.contains(c);
|
||||
}
|
||||
|
||||
AccountRegistry::AccountRegistry() = default;
|
||||
|
||||
QVariant AccountRegistry::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= m_accounts.count()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto account = m_accounts[index.row()];
|
||||
|
||||
if (role == ConnectionRole) {
|
||||
return QVariant::fromValue(account);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int AccountRegistry::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountRegistry::roleNames() const
|
||||
{
|
||||
return {{ConnectionRole, "connection"}};
|
||||
}
|
||||
|
||||
bool AccountRegistry::isEmpty() const
|
||||
{
|
||||
return m_accounts.isEmpty();
|
||||
}
|
||||
|
||||
int AccountRegistry::count() const
|
||||
{
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
const QVector<Connection *> AccountRegistry::accounts() const
|
||||
{
|
||||
return m_accounts;
|
||||
}
|
||||
|
||||
Connection *AccountRegistry::get(const QString &userId)
|
||||
{
|
||||
for (const auto &connection : m_accounts) {
|
||||
if (connection->userId() == userId) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
|
||||
class AccountRegistry : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum EventRoles {
|
||||
ConnectionRole = Qt::UserRole + 1,
|
||||
};
|
||||
|
||||
static AccountRegistry &instance()
|
||||
{
|
||||
static AccountRegistry _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
const QVector<Connection *> accounts() const;
|
||||
void add(Connection *a);
|
||||
void drop(Connection *a);
|
||||
bool isLoggedIn(const QString &userId) const;
|
||||
bool isEmpty() const;
|
||||
int count() const;
|
||||
bool contains(Connection *) const;
|
||||
Connection *get(const QString &userId);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
AccountRegistry();
|
||||
|
||||
QVector<Connection *> m_accounts;
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include "controller.h"
|
||||
#include "customemojimodel.h"
|
||||
#include "roommanager.h"
|
||||
|
||||
ActionsHandler::ActionsHandler(QObject *parent)
|
||||
@@ -62,36 +61,28 @@ void ActionsHandler::setConnection(Connection *connection)
|
||||
|
||||
void ActionsHandler::postEdit(const QString &text)
|
||||
{
|
||||
|
||||
const auto localId = Controller::instance().activeConnection()->userId();
|
||||
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); ++it) {
|
||||
const auto &evt = **it;
|
||||
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (event->senderId() == localId && event->hasTextContent()) {
|
||||
static QRegularExpression re("^s/([^/]*)/([^/]*)(/g)?");
|
||||
static QRegularExpression re("^s/([^/]*)/([^/]*)");
|
||||
auto match = re.match(text);
|
||||
if (!match.hasMatch()) {
|
||||
// should not happen but still make sure to send the message normally
|
||||
// just in case.
|
||||
postMessage(text, QString(), QString(), QString(), QVariantMap(), nullptr);
|
||||
postMessage(text, QString(), QString(), QString(), QVariantMap());
|
||||
}
|
||||
const QString regex = match.captured(1);
|
||||
const QString replacement = match.captured(2);
|
||||
const QString flags = match.captured(3);
|
||||
QString originalString;
|
||||
if (event->content()) {
|
||||
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
|
||||
} else {
|
||||
originalString = event->plainBody();
|
||||
}
|
||||
if (flags == "/g") {
|
||||
m_room->postHtmlMessage(text, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
||||
} else {
|
||||
m_room->postHtmlMessage(text,
|
||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||
event->msgtype(),
|
||||
"",
|
||||
event->id());
|
||||
}
|
||||
m_room->postHtmlMessage(text, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -99,41 +90,20 @@ void ActionsHandler::postEdit(const QString &text)
|
||||
}
|
||||
|
||||
void ActionsHandler::postMessage(const QString &text,
|
||||
const QString &attachmentPath,
|
||||
const QString &attachementPath,
|
||||
const QString &replyEventId,
|
||||
const QString &editEventId,
|
||||
const QVariantMap &usernames,
|
||||
CustomEmojiModel *cem)
|
||||
const QVariantMap &usernames)
|
||||
{
|
||||
QString rawText = text;
|
||||
auto stringList = text.split(QStringLiteral("```"));
|
||||
QString cleanedText;
|
||||
const auto count = stringList.count();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i % 2 == 0) {
|
||||
if (i + 1 != count) {
|
||||
cleanedText += stringList[i].toHtmlEscaped() + QStringLiteral("```");
|
||||
} else {
|
||||
cleanedText += stringList[i].toHtmlEscaped();
|
||||
}
|
||||
} else {
|
||||
cleanedText += stringList[i] + QStringLiteral("```");
|
||||
}
|
||||
}
|
||||
|
||||
auto preprocess = [cem](const QString &it) -> QString {
|
||||
if (cem == nullptr) {
|
||||
return it;
|
||||
}
|
||||
return cem->preprocessText(it);
|
||||
};
|
||||
QString cleanedText = text;
|
||||
|
||||
for (auto it = usernames.constBegin(); it != usernames.constEnd(); it++) {
|
||||
cleanedText = cleanedText.replace(it.key(), "[" + it.key() + "](https://matrix.to/#/" + it.value().toString() + ")");
|
||||
}
|
||||
|
||||
if (attachmentPath.length() > 0) {
|
||||
m_room->uploadFile(attachmentPath, cleanedText);
|
||||
if (attachementPath.length() > 0) {
|
||||
m_room->uploadFile(attachementPath, cleanedText);
|
||||
}
|
||||
|
||||
if (cleanedText.length() == 0) {
|
||||
@@ -145,32 +115,27 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
// Message commands
|
||||
static const QString shrugPrefix = QStringLiteral("/shrug");
|
||||
static const QString lennyPrefix = QStringLiteral("/lenny");
|
||||
static const QString tableflipPrefix = QStringLiteral("/tableflip");
|
||||
static const QString unflipPrefix = QStringLiteral("/unflip");
|
||||
// static const QString plainPrefix = QStringLiteral("/plain "); // TODO
|
||||
// static const QString htmlPrefix = QStringLiteral("/html "); // TODO
|
||||
static const QString plainPrefix = QStringLiteral("/plain "); // TODO
|
||||
static const QString htmlPrefix = QStringLiteral("/html "); // TODO
|
||||
static const QString rainbowPrefix = QStringLiteral("/rainbow ");
|
||||
static const QString rainbowmePrefix = QStringLiteral("/rainbowme ");
|
||||
static const QString spoilerPrefix = QStringLiteral("/spoiler ");
|
||||
static const QString mePrefix = QStringLiteral("/me ");
|
||||
static const QString noticePrefix = QStringLiteral("/notice ");
|
||||
|
||||
// Actions commands
|
||||
// static const QString ddgPrefix = QStringLiteral("/ddg "); // TODO
|
||||
// static const QString nickPrefix = QStringLiteral("/nick "); // TODO
|
||||
// static const QString meroomnickPrefix = QStringLiteral("/myroomnick "); // TODO
|
||||
// static const QString roomavatarPrefix = QStringLiteral("/roomavatar "); // TODO
|
||||
// static const QString myroomavatarPrefix = QStringLiteral("/myroomavatar "); // TODO
|
||||
// static const QString myavatarPrefix = QStringLiteral("/myavatar "); // TODO
|
||||
static const QString ddgPrefix = QStringLiteral("/ddg "); // TODO
|
||||
static const QString nickPrefix = QStringLiteral("/nick "); // TODO
|
||||
static const QString meroomnickPrefix = QStringLiteral("/myroomnick "); // TODO
|
||||
static const QString roomavatarPrefix = QStringLiteral("/roomavatar "); // TODO
|
||||
static const QString myroomavatarPrefix = QStringLiteral("/myroomavatar "); // TODO
|
||||
static const QString myavatarPrefix = QStringLiteral("/myavatar "); // TODO
|
||||
static const QString invitePrefix = QStringLiteral("/invite ");
|
||||
static const QString joinPrefix = QStringLiteral("/join ");
|
||||
static const QString joinShortPrefix = QStringLiteral("/j ");
|
||||
static const QString partPrefix = QStringLiteral("/part");
|
||||
static const QString leavePrefix = QStringLiteral("/leave");
|
||||
static const QString ignorePrefix = QStringLiteral("/ignore ");
|
||||
static const QString unignorePrefix = QStringLiteral("/unignore ");
|
||||
// static const QString queryPrefix = QStringLiteral("/query "); // TODO
|
||||
// static const QString msgPrefix = QStringLiteral("/msg "); // TODO
|
||||
static const QString queryPrefix = QStringLiteral("/query "); // TODO
|
||||
static const QString msgPrefix = QStringLiteral("/msg "); // TODO
|
||||
static const QString reactPrefix = QStringLiteral("/react ");
|
||||
|
||||
// Admin commands
|
||||
@@ -192,33 +157,13 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedText.indexOf(tableflipPrefix) == 0) {
|
||||
cleanedText = QStringLiteral("(╯°□°)╯︵ ┻━┻") % cleanedText.remove(0, tableflipPrefix.length());
|
||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedText.indexOf(unflipPrefix) == 0) {
|
||||
cleanedText = QStringLiteral("┬──┬ ノ( ゜-゜ノ)") % cleanedText.remove(0, unflipPrefix.length());
|
||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedText.indexOf(rainbowPrefix) == 0) {
|
||||
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
|
||||
QString rainbowText;
|
||||
for (int i = 0; i < cleanedText.length(); i++) {
|
||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
||||
}
|
||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedText.indexOf(spoilerPrefix) == 0) {
|
||||
cleanedText = cleanedText.remove(0, spoilerPrefix.length());
|
||||
const QStringList splittedText = rawText.split(" ");
|
||||
QString spoilerHtml = QStringLiteral("<span data-mx-spoiler>") % preprocess(cleanedText) % QStringLiteral("</span>");
|
||||
m_room->postHtmlMessage(cleanedText, spoilerHtml, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
||||
m_room->postHtmlMessage(cleanedText, rainbowText, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,16 +173,12 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
for (int i = 0; i < cleanedText.length(); i++) {
|
||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
||||
}
|
||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), messageEventType, replyEventId, editEventId);
|
||||
m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawText.indexOf(joinPrefix) == 0 || rawText.indexOf(joinShortPrefix) == 0) {
|
||||
if (rawText.indexOf(joinPrefix) == 0) {
|
||||
rawText = rawText.remove(0, joinPrefix.length());
|
||||
} else {
|
||||
rawText = rawText.remove(0, joinShortPrefix.length());
|
||||
}
|
||||
if (rawText.indexOf(joinPrefix) == 0) {
|
||||
rawText = rawText.remove(0, joinPrefix.length());
|
||||
const QStringList splittedText = rawText.split(" ");
|
||||
if (text.count() == 0) {
|
||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
||||
@@ -266,12 +207,8 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawText.indexOf(partPrefix) == 0 || rawText.indexOf(leavePrefix) == 0) {
|
||||
if (rawText.indexOf(partPrefix) == 0) {
|
||||
rawText = rawText.remove(0, partPrefix.length());
|
||||
} else {
|
||||
rawText = rawText.remove(0, leavePrefix.length());
|
||||
}
|
||||
if (rawText.indexOf(partPrefix) == 0) {
|
||||
rawText = rawText.remove(0, partPrefix.length());
|
||||
const QStringList splittedText = rawText.split(" ");
|
||||
if (splittedText.count() == 0 || splittedText[0].isEmpty()) {
|
||||
// leave current room
|
||||
@@ -319,18 +256,10 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
}
|
||||
|
||||
if (rawText.indexOf(reactPrefix) == 0) {
|
||||
rawText = rawText.remove(0, reactPrefix.length());
|
||||
if (replyEventId.isEmpty()) {
|
||||
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); ++it) {
|
||||
const auto &evt = **it;
|
||||
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
m_room->toggleReaction(event->id(), rawText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Q_EMIT showMessage(MessageType::Error, i18n("Couldn't find a message to react to"));
|
||||
return;
|
||||
}
|
||||
rawText = rawText.remove(0, reactPrefix.length());
|
||||
m_room->toggleReaction(replyEventId, rawText);
|
||||
return;
|
||||
}
|
||||
@@ -343,5 +272,5 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
cleanedText = cleanedText.remove(0, noticePrefix.length());
|
||||
messageEventType = RoomMessageEvent::MsgType::Notice;
|
||||
}
|
||||
m_room->postMessage(rawText, preprocess(m_room->preprocessText(cleanedText)), messageEventType, replyEventId, editEventId);
|
||||
m_room->postMessage(rawText, cleanedText, messageEventType, replyEventId, editEventId);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CustomEmojiModel;
|
||||
|
||||
/// \brief Handles user interactions with NeoChat (joining room, creating room,
|
||||
/// sending message). Account management is handled by Controller.
|
||||
class ActionsHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
/// \brief The connection that will handle sending the message.
|
||||
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
@@ -34,6 +33,8 @@ public:
|
||||
explicit ActionsHandler(QObject *parent = nullptr);
|
||||
~ActionsHandler();
|
||||
|
||||
|
||||
|
||||
[[nodiscard]] Connection *connection() const;
|
||||
void setConnection(Connection *connection);
|
||||
|
||||
@@ -44,7 +45,7 @@ Q_SIGNALS:
|
||||
/// \brief Show error or information message.
|
||||
///
|
||||
/// These messages will be displayed in the room view header.
|
||||
void showMessage(ActionsHandler::MessageType messageType, QString message);
|
||||
void showMessage(MessageType messageType, QString message);
|
||||
|
||||
void roomChanged();
|
||||
void connectionChanged();
|
||||
@@ -54,12 +55,8 @@ public Q_SLOTS:
|
||||
/// \brief Post a message.
|
||||
///
|
||||
/// This also interprets commands if any.
|
||||
void postMessage(const QString &text,
|
||||
const QString &attachementPath,
|
||||
const QString &replyEventId,
|
||||
const QString &editEventId,
|
||||
const QVariantMap &usernames,
|
||||
CustomEmojiModel *cem);
|
||||
void
|
||||
postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId, const QVariantMap &usernames);
|
||||
|
||||
/// \brief Send edit instructions (.e.g s/hallo/hello/)
|
||||
///
|
||||
|
||||
194
src/blurhash.cpp
194
src/blurhash.cpp
@@ -1,194 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "blurhash.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
const char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
||||
|
||||
struct Color {
|
||||
float r = 0;
|
||||
float g = 0;
|
||||
float b = 0;
|
||||
};
|
||||
|
||||
inline int linearTosRGB(float value)
|
||||
{
|
||||
float v = fmaxf(0, fminf(1, value));
|
||||
if (v <= 0.0031308)
|
||||
return v * 12.92 * 255 + 0.5;
|
||||
else
|
||||
return (1.055 * powf(v, 1 / 2.4) - 0.055) * 255 + 0.5;
|
||||
}
|
||||
|
||||
inline float sRGBToLinear(int value)
|
||||
{
|
||||
float v = (float)value / 255;
|
||||
if (v <= 0.04045)
|
||||
return v / 12.92;
|
||||
else
|
||||
return powf((v + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
inline float signPow(float value, float exp)
|
||||
{
|
||||
return copysignf(powf(fabsf(value), exp), value);
|
||||
}
|
||||
|
||||
inline uint8_t clampToUByte(int *src)
|
||||
{
|
||||
if (*src >= 0 && *src <= 255) {
|
||||
return *src;
|
||||
}
|
||||
return (*src < 0) ? 0 : 255;
|
||||
}
|
||||
|
||||
inline uint8_t *createByteArray(int size)
|
||||
{
|
||||
return (uint8_t *)malloc(size * sizeof(uint8_t));
|
||||
}
|
||||
|
||||
int decodeToInt(const char *string, int start, int end)
|
||||
{
|
||||
int value = 0;
|
||||
for (int iter1 = start; iter1 < end; iter1++) {
|
||||
int index = -1;
|
||||
for (int iter2 = 0; iter2 < 83; iter2++) {
|
||||
if (chars[iter2] == string[iter1]) {
|
||||
index = iter2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index == -1) {
|
||||
return -1;
|
||||
}
|
||||
value = value * 83 + index;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void decodeDC(int value, Color *color)
|
||||
{
|
||||
color->r = sRGBToLinear(value >> 16);
|
||||
color->g = sRGBToLinear((value >> 8) & 255);
|
||||
color->b = sRGBToLinear(value & 255);
|
||||
}
|
||||
|
||||
void decodeAC(int value, float maximumValue, Color *color)
|
||||
{
|
||||
int quantR = (int)floorf(value / (19 * 19));
|
||||
int quantG = (int)floorf(value / 19) % 19;
|
||||
int quantB = (int)value % 19;
|
||||
|
||||
color->r = signPow(((float)quantR - 9) / 9, 2.0) * maximumValue;
|
||||
color->g = signPow(((float)quantG - 9) / 9, 2.0) * maximumValue;
|
||||
color->b = signPow(((float)quantB - 9) / 9, 2.0) * maximumValue;
|
||||
}
|
||||
|
||||
int decodeToArray(const char *blurhash, int width, int height, int punch, int nChannels, uint8_t *pixelArray)
|
||||
{
|
||||
if (!isValidBlurhash(blurhash)) {
|
||||
return -1;
|
||||
}
|
||||
if (punch < 1) {
|
||||
punch = 1;
|
||||
}
|
||||
|
||||
int sizeFlag = decodeToInt(blurhash, 0, 1);
|
||||
int numY = (int)floorf(sizeFlag / 9) + 1;
|
||||
int numX = (sizeFlag % 9) + 1;
|
||||
int iter = 0;
|
||||
|
||||
Color color;
|
||||
int quantizedMaxValue = decodeToInt(blurhash, 1, 2);
|
||||
if (quantizedMaxValue == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float maxValue = ((float)(quantizedMaxValue + 1)) / 166;
|
||||
|
||||
const int colors_size = numX * numY;
|
||||
|
||||
std::vector<Color> colors(colors_size, {0, 0, 0});
|
||||
|
||||
for (iter = 0; iter < colors_size; iter++) {
|
||||
if (iter == 0) {
|
||||
int value = decodeToInt(blurhash, 2, 6);
|
||||
if (value == -1) {
|
||||
return -1;
|
||||
}
|
||||
decodeDC(value, &color);
|
||||
colors[iter] = color;
|
||||
} else {
|
||||
int value = decodeToInt(blurhash, 4 + iter * 2, 6 + iter * 2);
|
||||
if (value == -1) {
|
||||
return -1;
|
||||
}
|
||||
decodeAC(value, maxValue * punch, &color);
|
||||
colors[iter] = color;
|
||||
}
|
||||
}
|
||||
|
||||
int bytesPerRow = width * nChannels;
|
||||
int x = 0, y = 0, i = 0, j = 0;
|
||||
int intR = 0, intG = 0, intB = 0;
|
||||
|
||||
for (y = 0; y < height; y++) {
|
||||
for (x = 0; x < width; x++) {
|
||||
float r = 0, g = 0, b = 0;
|
||||
|
||||
for (j = 0; j < numY; j++) {
|
||||
for (i = 0; i < numX; i++) {
|
||||
float basics = cos((M_PI * x * i) / width) * cos((M_PI * y * j) / height);
|
||||
int idx = i + j * numX;
|
||||
r += colors[idx].r * basics;
|
||||
g += colors[idx].g * basics;
|
||||
b += colors[idx].b * basics;
|
||||
}
|
||||
}
|
||||
|
||||
intR = linearTosRGB(r);
|
||||
intG = linearTosRGB(g);
|
||||
intB = linearTosRGB(b);
|
||||
|
||||
pixelArray[nChannels * x + 0 + y * bytesPerRow] = clampToUByte(&intR);
|
||||
pixelArray[nChannels * x + 1 + y * bytesPerRow] = clampToUByte(&intG);
|
||||
pixelArray[nChannels * x + 2 + y * bytesPerRow] = clampToUByte(&intB);
|
||||
|
||||
if (nChannels == 4) {
|
||||
pixelArray[nChannels * x + 3 + y * bytesPerRow] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels)
|
||||
{
|
||||
int bytesPerRow = width * nChannels;
|
||||
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
|
||||
|
||||
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
|
||||
return NULL;
|
||||
}
|
||||
return pixelArray;
|
||||
}
|
||||
|
||||
bool isValidBlurhash(const char *blurhash)
|
||||
{
|
||||
const int hashLength = strlen(blurhash);
|
||||
|
||||
if (!blurhash || strlen(blurhash) < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sizeFlag = decodeToInt(blurhash, 0, 1);
|
||||
int numY = (int)floorf(sizeFlag / 9) + 1;
|
||||
int numX = (sizeFlag % 9) + 1;
|
||||
|
||||
return hashLength == 4 + 2 * numX * numY;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels);
|
||||
|
||||
bool isValidBlurhash(const char *blurhash);
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "blurhashimageprovider.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
|
||||
#include "blurhash.h"
|
||||
|
||||
BlurhashImageProvider::BlurhashImageProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image)
|
||||
{
|
||||
}
|
||||
|
||||
QImage BlurhashImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
|
||||
{
|
||||
if (id.isEmpty()) {
|
||||
return QImage();
|
||||
}
|
||||
*size = requestedSize;
|
||||
if (size->width() == -1) {
|
||||
size->setWidth(256);
|
||||
}
|
||||
if (size->height() == -1) {
|
||||
size->setHeight(256);
|
||||
}
|
||||
auto data = decode(QUrl::fromPercentEncoding(id.toLatin1()).toLatin1().data(), size->width(), size->height(), 1, 3);
|
||||
QImage image(data, size->width(), size->height(), size->width() * 3, QImage::Format_RGB888, free, data);
|
||||
return image;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class BlurhashImageProvider : public QQuickImageProvider
|
||||
{
|
||||
public:
|
||||
BlurhashImageProvider();
|
||||
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||
};
|
||||
@@ -16,8 +16,6 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_document(nullptr)
|
||||
, m_cursorPosition(-1)
|
||||
, m_selectionStart(-1)
|
||||
, m_selectionEnd(-1)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -125,7 +123,7 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
QVariantMap ChatDocumentHandler::getAutocompletionInfo(bool isAutocompleting)
|
||||
QVariantMap ChatDocumentHandler::getAutocompletionInfo()
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
@@ -135,6 +133,9 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo(bool isAutocompleting)
|
||||
{"type", AutoCompletionType::Ignore},
|
||||
};
|
||||
}
|
||||
if (m_cursorPosition != m_autoCompleteBeginPosition && m_cursorPosition != m_autoCompleteEndPosition) {
|
||||
// we moved our cursor, so cancel autocompletion
|
||||
}
|
||||
|
||||
QString text = cursor.block().text();
|
||||
QString textBeforeCursor = text;
|
||||
@@ -166,17 +167,10 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo(bool isAutocompleting)
|
||||
};
|
||||
}
|
||||
|
||||
if (!isAutocompleting) {
|
||||
return QVariantMap{
|
||||
{"keyword", autoCompletePrefix},
|
||||
{"type", AutoCompletionType::Emoji},
|
||||
};
|
||||
} else {
|
||||
return QVariantMap{
|
||||
{"type", AutoCompletionType::Ignore},
|
||||
{"keyword", autoCompletePrefix},
|
||||
};
|
||||
}
|
||||
return QVariantMap{
|
||||
{"keyword", autoCompletePrefix},
|
||||
{"type", AutoCompletionType::Emoji},
|
||||
};
|
||||
}
|
||||
|
||||
return QVariantMap{
|
||||
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
|
||||
/// This function will look at the current QTextCursor and determine if there
|
||||
/// is the possibility to autocomplete it.
|
||||
Q_INVOKABLE QVariantMap getAutocompletionInfo(bool isAutocompleting);
|
||||
Q_INVOKABLE QVariantMap getAutocompletionInfo();
|
||||
Q_INVOKABLE void replaceAutoComplete(const QString &word);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include <KColorSchemeManager>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
#include "colorschemer.h"
|
||||
|
||||
ColorSchemer::ColorSchemer(QObject *parent)
|
||||
: QObject(parent)
|
||||
, c(new KColorSchemeManager(this))
|
||||
{
|
||||
}
|
||||
|
||||
ColorSchemer::~ColorSchemer()
|
||||
{
|
||||
}
|
||||
|
||||
QAbstractItemModel *ColorSchemer::model() const
|
||||
{
|
||||
return c->model();
|
||||
}
|
||||
|
||||
void ColorSchemer::apply(int idx)
|
||||
{
|
||||
c->activateScheme(c->model()->index(idx, 0));
|
||||
}
|
||||
|
||||
void ColorSchemer::apply(const QString &name)
|
||||
{
|
||||
c->activateScheme(c->indexForScheme(name));
|
||||
}
|
||||
|
||||
int ColorSchemer::indexForScheme(const QString &name) const
|
||||
{
|
||||
auto index = c->indexForScheme(name).row();
|
||||
if (index == -1) {
|
||||
index = 0;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
QString ColorSchemer::nameForIndex(int index) const
|
||||
{
|
||||
return c->model()->data(c->model()->index(index, 0), Qt::DisplayRole).toString();
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QIdentityProxyModel>
|
||||
#include <QObject>
|
||||
|
||||
class QAbstractItemModel;
|
||||
class KColorSchemeManager;
|
||||
|
||||
class ColorSchemer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QAbstractItemModel *model READ model CONSTANT)
|
||||
public:
|
||||
ColorSchemer(QObject *parent = nullptr);
|
||||
~ColorSchemer();
|
||||
|
||||
QAbstractItemModel *model() const;
|
||||
Q_INVOKABLE void apply(int idx);
|
||||
Q_INVOKABLE void apply(const QString &name);
|
||||
Q_INVOKABLE int indexForScheme(const QString &name) const;
|
||||
Q_INVOKABLE QString nameForIndex(int index) const;
|
||||
|
||||
private:
|
||||
KColorSchemeManager *c;
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "actionshandler.h"
|
||||
#include "commandmodel.h"
|
||||
|
||||
|
||||
QVariantList CommandModel::filterModel(const QString &filter)
|
||||
{
|
||||
QVariantList result;
|
||||
@@ -23,47 +24,70 @@ QVariantList CommandModel::filterModel(const QString &filter)
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
QVariantList CommandModel::commands()
|
||||
{
|
||||
QVariantList commands;
|
||||
|
||||
// Messages commands
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/me "), QStringLiteral("<message>"), i18n("Displays action")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/me "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Displays action")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/shrug "), QStringLiteral("<message>"), i18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/shrug "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/lenny "), QStringLiteral("<message>"), i18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/lenny "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message")}));
|
||||
|
||||
commands.append(
|
||||
QVariant::fromValue(Command{QStringLiteral("/tableflip "), QStringLiteral("<message>"), i18n("Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/plain "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Sends a message as plain text, without interpreting it as markdown")}));
|
||||
|
||||
commands.append(
|
||||
QVariant::fromValue(Command{QStringLiteral("/unflip "), QStringLiteral("<message>"), i18n("Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/html "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Sends a message as html, without interpreting it as markdown")}));
|
||||
|
||||
commands.append(QVariant::fromValue(
|
||||
Command{QStringLiteral("/plain "), QStringLiteral("<message>"), i18n("Sends a message as plain text, without interpreting it as markdown")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/rainbow "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Sends the given message coloured as a rainbow")}));
|
||||
|
||||
commands.append(QVariant::fromValue(
|
||||
Command{QStringLiteral("/html "), QStringLiteral("<message>"), i18n("Sends a message as html, without interpreting it as markdown")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/rainbowme "),
|
||||
QStringLiteral("<message>"),
|
||||
i18n("Sends the given emote coloured as a rainbow")}));
|
||||
|
||||
commands.append(
|
||||
QVariant::fromValue(Command{QStringLiteral("/rainbow "), QStringLiteral("<message>"), i18n("Sends the given message coloured as a rainbow")}));
|
||||
|
||||
commands.append(
|
||||
QVariant::fromValue(Command{QStringLiteral("/rainbowme "), QStringLiteral("<message>"), i18n("Sends the given emote coloured as a rainbow")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/spoiler "), QStringLiteral("<message>"), i18n("Sends the given message as a spoiler")}));
|
||||
|
||||
// Actions commands
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/join "), QStringLiteral("<room-address>"), i18n("Joins room with given address")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/join "), QStringLiteral("<room-address>"),
|
||||
i18n("Joins room with given address")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/part "), QStringLiteral("[<room-address>]"), i18n("Leave room")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/part "),
|
||||
QStringLiteral("[<room-address>]"),
|
||||
i18n("Leave room")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/invite "), QStringLiteral("<user-id>"), i18n("Invites user with given id to current room")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/invite "),
|
||||
QStringLiteral("<user-id>"),
|
||||
i18n("Invites user with given id to current room")}));
|
||||
|
||||
commands.append(QVariant::fromValue(Command{QStringLiteral("/react "), QStringLiteral("<reaction text>"), i18n("React to this message with a text")}));
|
||||
commands.append(QVariant::fromValue(Command{
|
||||
QStringLiteral("/react "),
|
||||
QStringLiteral("<reaction text>"),
|
||||
i18n("React to this message with a text")}));
|
||||
|
||||
// TODO more see elements /help action
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ struct Command {
|
||||
QString parameter;
|
||||
QString help;
|
||||
|
||||
Q_GADGET
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString command MEMBER command)
|
||||
Q_PROPERTY(QString parameter MEMBER parameter)
|
||||
Q_PROPERTY(QString help MEMBER help)
|
||||
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Command)
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
#include <KConfigGroup>
|
||||
#include <KLocalizedString>
|
||||
#include <KWindowConfig>
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
#include <KWindowEffects>
|
||||
#include <KWindowSystem>
|
||||
#endif
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QClipboard>
|
||||
#include <QCloseEvent>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
@@ -24,7 +25,6 @@
|
||||
#include <QNetworkConfigurationManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickWindow>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringBuilder>
|
||||
@@ -34,26 +34,25 @@
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#include "accountregistry.h"
|
||||
#include "csapi/account-data.h"
|
||||
#include "csapi/content-repo.h"
|
||||
#include "csapi/joining.h"
|
||||
#include "csapi/logout.h"
|
||||
#include "csapi/profile.h"
|
||||
#include "csapi/registration.h"
|
||||
#include "csapi/wellknown.h"
|
||||
#include "events/eventcontent.h"
|
||||
#include "events/roommessageevent.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "roommanager.h"
|
||||
#include "settings.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "roommanager.h"
|
||||
#include <KStandardShortcut>
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include "trayicon.h"
|
||||
#elif !defined(Q_OS_ANDROID)
|
||||
#include "trayicon_sni.h"
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -72,7 +71,7 @@ Controller::Controller(QObject *parent)
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||
}
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this, trayIcon]() {
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [=]() {
|
||||
if (NeoChatConfig::self()->systemTray()) {
|
||||
trayIcon->show();
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
@@ -84,7 +83,7 @@ Controller::Controller(QObject *parent)
|
||||
});
|
||||
#endif
|
||||
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
QTimer::singleShot(0, this, [=] {
|
||||
invokeLogin();
|
||||
});
|
||||
|
||||
@@ -116,12 +115,13 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
connect(m_mgr, &QNetworkConfigurationManager::onlineStateChanged, this, &Controller::isOnlineChanged);
|
||||
connect(m_mgr, &QNetworkConfigurationManager::onlineStateChanged,
|
||||
this, &Controller::isOnlineChanged);
|
||||
}
|
||||
|
||||
Controller::~Controller()
|
||||
{
|
||||
for (auto c : AccountRegistry::instance().accounts()) {
|
||||
for (auto c : qAsConst(m_connections)) {
|
||||
c->saveState();
|
||||
}
|
||||
}
|
||||
@@ -147,14 +147,15 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
|
||||
|
||||
QUrl serverUrl(serverAddr);
|
||||
|
||||
auto conn = new Connection();
|
||||
auto conn = new Connection(this);
|
||||
if (serverUrl.isValid()) {
|
||||
conn->setHomeserver(serverUrl);
|
||||
}
|
||||
|
||||
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
|
||||
connect(conn, &Connection::connected, this, [=] {
|
||||
AccountSettings account(conn->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(conn->homeserver());
|
||||
account.setDeviceId(conn->deviceId());
|
||||
account.setDeviceName(deviceName);
|
||||
@@ -165,10 +166,10 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
|
||||
addConnection(conn);
|
||||
setActiveConnection(conn);
|
||||
});
|
||||
connect(conn, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
|
||||
connect(conn, &Connection::networkError, this, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
});
|
||||
conn->assumeIdentity(user, token, deviceName);
|
||||
conn->connectWithToken(user, token, deviceName);
|
||||
}
|
||||
|
||||
void Controller::logout(Connection *conn, bool serverSideLogout)
|
||||
@@ -189,28 +190,32 @@ void Controller::logout(Connection *conn, bool serverSideLogout)
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (conn == activeConnection() && AccountRegistry::instance().count() > 1) {
|
||||
setActiveConnection(AccountRegistry::instance().accounts()[0]);
|
||||
conn->stopSync();
|
||||
Q_EMIT conn->stateChanged();
|
||||
Q_EMIT conn->loggedOut();
|
||||
if (conn == activeConnection() && !m_connections.isEmpty()) {
|
||||
setActiveConnection(m_connections[0]);
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
}
|
||||
if (!serverSideLogout) {
|
||||
return;
|
||||
}
|
||||
conn->logout();
|
||||
auto logoutJob = conn->callApi<LogoutJob>();
|
||||
connect(logoutJob, &LogoutJob::failure, this, [=] {
|
||||
Q_EMIT errorOccured(i18n("Server-side Logout Failed: %1", logoutJob->errorString()));
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::addConnection(Connection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
AccountRegistry::instance().add(c);
|
||||
#endif
|
||||
m_connections += c;
|
||||
|
||||
c->setLazyLoading(true);
|
||||
|
||||
connect(c, &Connection::syncDone, this, [this, c] {
|
||||
connect(c, &Connection::syncDone, this, [=] {
|
||||
setBusy(false);
|
||||
|
||||
Q_EMIT syncDone();
|
||||
@@ -218,11 +223,11 @@ void Controller::addConnection(Connection *c)
|
||||
c->sync(30000);
|
||||
c->saveState();
|
||||
});
|
||||
connect(c, &Connection::loggedOut, this, [this, c] {
|
||||
connect(c, &Connection::loggedOut, this, [=] {
|
||||
dropConnection(c);
|
||||
});
|
||||
|
||||
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
|
||||
connect(c, &Connection::requestFailed, this, [=](BaseJob *job) {
|
||||
if (job->error() == BaseJob::UserConsentRequiredError) {
|
||||
Q_EMIT userConsentRequired(job->errorUrl());
|
||||
}
|
||||
@@ -239,16 +244,11 @@ void Controller::addConnection(Connection *c)
|
||||
void Controller::dropConnection(Connection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
AccountRegistry::instance().drop(c);
|
||||
#endif
|
||||
m_connections.removeOne(c);
|
||||
|
||||
Q_EMIT connectionDropped(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
#ifndef QUOTIENT_07
|
||||
c->deleteLater();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::invokeLogin()
|
||||
@@ -264,29 +264,29 @@ void Controller::invokeLogin()
|
||||
if (!account.homeserver().isEmpty()) {
|
||||
auto accessToken = loadAccessTokenFromKeyChain(account);
|
||||
|
||||
auto connection = new Connection(account.homeserver());
|
||||
connect(connection, &Connection::connected, this, [this, connection, id] {
|
||||
auto connection = new Connection(account.homeserver(), this);
|
||||
connect(connection, &Connection::connected, this, [=] {
|
||||
connection->loadState();
|
||||
addConnection(connection);
|
||||
if (connection->userId() == id) {
|
||||
setActiveConnection(connection);
|
||||
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
|
||||
Q_EMIT initiated();
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
|
||||
connect(connection, &Connection::loginError, this, [=](const QString &error, const QString &) {
|
||||
if (error == "Unrecognised access token") {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
|
||||
logout(connection, false);
|
||||
} else {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
Q_EMIT errorOccured(i18n("Login Failed", error));
|
||||
logout(connection, true);
|
||||
}
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
|
||||
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
});
|
||||
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
|
||||
connection->connectWithToken(account.userId(), accessToken, account.deviceId());
|
||||
}
|
||||
}
|
||||
if (accounts.isEmpty()) {
|
||||
@@ -311,30 +311,23 @@ QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
|
||||
|
||||
QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
|
||||
{
|
||||
QKeychain::Error error;
|
||||
QString errorString;
|
||||
do {
|
||||
qDebug() << "Reading access token from the keychain for" << account.userId();
|
||||
QKeychain::ReadPasswordJob job(qAppName());
|
||||
job.setAutoDelete(false);
|
||||
job.setKey(account.userId());
|
||||
QEventLoop loop;
|
||||
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
qDebug() << "Read the access token from the keychain for " << account.userId();
|
||||
QKeychain::ReadPasswordJob job(qAppName());
|
||||
job.setAutoDelete(false);
|
||||
job.setKey(account.userId());
|
||||
QEventLoop loop;
|
||||
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (job.error() == QKeychain::Error::NoError) {
|
||||
return job.binaryData();
|
||||
}
|
||||
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), i18n("Please make sure that the keychain is opened."));
|
||||
error = job.error();
|
||||
errorString = job.errorString();
|
||||
} while (error == QKeychain::Error::OtherError);
|
||||
if (job.error() == QKeychain::Error::NoError) {
|
||||
return job.binaryData();
|
||||
}
|
||||
|
||||
qWarning() << "Could not read the access token from the keychain:" << errorString;
|
||||
qWarning() << "Could not read the access token from the keychain: " << qPrintable(job.errorString());
|
||||
// no access token from the keychain, try token file
|
||||
auto accessToken = loadAccessTokenFromFile(account);
|
||||
if (error == QKeychain::Error::EntryNotFound) {
|
||||
if (job.error() == QKeychain::Error::EntryNotFound) {
|
||||
if (!accessToken.isEmpty()) {
|
||||
qDebug() << "Migrating the access token from file to the keychain for " << account.userId();
|
||||
bool removed = false;
|
||||
@@ -393,7 +386,7 @@ void Controller::playAudio(const QUrl &localFile)
|
||||
auto player = new QMediaPlayer;
|
||||
player->setMedia(localFile);
|
||||
player->play();
|
||||
connect(player, &QMediaPlayer::stateChanged, [player] {
|
||||
connect(player, &QMediaPlayer::stateChanged, [=] {
|
||||
player->deleteLater();
|
||||
});
|
||||
}
|
||||
@@ -402,7 +395,7 @@ void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
|
||||
{
|
||||
auto job = conn->uploadFile(localFile.toLocalFile());
|
||||
#ifdef QUOTIENT_07
|
||||
if (isJobPending(job)) {
|
||||
if(isJobPending(job)) {
|
||||
#else
|
||||
if (isJobRunning(job)) {
|
||||
#endif
|
||||
@@ -474,11 +467,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
|
||||
User *localUser = connection->user();
|
||||
QString decoded = avatarSource.path();
|
||||
if (decoded.isEmpty()) {
|
||||
#ifdef QUOTIENT_07
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
|
||||
#else
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), QString());
|
||||
#endif
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), "");
|
||||
return true;
|
||||
}
|
||||
if (QImageReader(decoded).read().isNull()) {
|
||||
@@ -489,11 +478,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
|
||||
}
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
#ifdef QUOTIENT_07
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
#else
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), QStringLiteral("/_matrix/client/r0/account/password"))
|
||||
#endif
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), QStringLiteral("/_matrix/client/r0") % "/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
@@ -502,9 +487,14 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
|
||||
setRequestData(_data);
|
||||
}
|
||||
|
||||
QVector<Connection *> Controller::connections() const
|
||||
{
|
||||
return m_connections;
|
||||
}
|
||||
|
||||
int Controller::accountCount() const
|
||||
{
|
||||
return AccountRegistry::instance().count();
|
||||
return m_connections.count();
|
||||
}
|
||||
|
||||
bool Controller::quitOnLastWindowClosed()
|
||||
@@ -567,11 +557,7 @@ void Controller::saveWindowGeometry(QQuickWindow *window)
|
||||
}
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
#ifdef QUOTIENT_07
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
#else
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
|
||||
#endif
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
@@ -581,7 +567,7 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
|
||||
void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [this, createRoomJob] {
|
||||
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
|
||||
});
|
||||
}
|
||||
@@ -591,7 +577,6 @@ bool Controller::isOnline() const
|
||||
return m_mgr->isOnline();
|
||||
}
|
||||
|
||||
// TODO: Remove in favor of RoomManager::joinRoom
|
||||
void Controller::joinRoom(const QString &alias)
|
||||
{
|
||||
if (!alias.contains(":")) {
|
||||
@@ -600,7 +585,14 @@ void Controller::joinRoom(const QString &alias)
|
||||
}
|
||||
|
||||
const auto knownServer = alias.mid(alias.indexOf(":") + 1);
|
||||
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
|
||||
auto joinRoomJob = m_connection->joinRoom(alias, QStringList{knownServer});
|
||||
|
||||
connect(joinRoomJob, &JoinRoomJob::failure, [=] {
|
||||
Q_EMIT errorOccured(i18n("Server error when joining the room \"%1\": %2", joinRoomJob->errorString()));
|
||||
});
|
||||
connect(joinRoomJob, &JoinRoomJob::success, [this, joinRoomJob] {
|
||||
Q_EMIT errorOccured(joinRoomJob->roomId());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::openOrCreateDirectChat(NeoChatUser *user)
|
||||
@@ -616,43 +608,10 @@ void Controller::openOrCreateDirectChat(NeoChatUser *user)
|
||||
|
||||
QString Controller::formatByteSize(double size, int precision) const
|
||||
{
|
||||
return QLocale().formattedDataSize(size, precision);
|
||||
return KFormat().formatByteSize(size, precision);
|
||||
}
|
||||
|
||||
QString Controller::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const
|
||||
{
|
||||
return KFormat().formatDuration(msecs, options);
|
||||
}
|
||||
|
||||
void Controller::setBlur(QQuickItem *item, bool blur)
|
||||
{
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
auto setWindows = [item, blur]() {
|
||||
auto reg = QRect(QPoint(0, 0), item->window()->size());
|
||||
KWindowEffects::enableBackgroundContrast(item->window(), blur, 1, 1, 1, reg);
|
||||
KWindowEffects::enableBlurBehind(item->window(), blur, reg);
|
||||
};
|
||||
|
||||
disconnect(item->window(), &QQuickWindow::heightChanged, this, nullptr);
|
||||
disconnect(item->window(), &QQuickWindow::widthChanged, this, nullptr);
|
||||
connect(item->window(), &QQuickWindow::heightChanged, this, setWindows);
|
||||
connect(item->window(), &QQuickWindow::widthChanged, this, setWindows);
|
||||
setWindows();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::raiseWindow(QWindow *window)
|
||||
{
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
KWindowSystem::activateWindow(window->winId());
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Controller::hasWindowSystem() const
|
||||
{
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <QMediaPlayer>
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KFormat>
|
||||
@@ -34,12 +33,13 @@ class Controller : public QObject
|
||||
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
|
||||
Q_PROPERTY(KAboutData aboutData READ aboutData WRITE setAboutData NOTIFY aboutDataChanged)
|
||||
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
|
||||
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
|
||||
|
||||
public:
|
||||
static Controller &instance();
|
||||
|
||||
[[nodiscard]] QVector<Connection *> connections() const;
|
||||
|
||||
void setActiveConnection(Connection *connection);
|
||||
[[nodiscard]] Connection *activeConnection() const;
|
||||
|
||||
@@ -87,14 +87,11 @@ public:
|
||||
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
|
||||
|
||||
Q_INVOKABLE void openOrCreateDirectChat(NeoChatUser *user);
|
||||
|
||||
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
|
||||
Q_INVOKABLE void raiseWindow(QWindow *window);
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
~Controller() override;
|
||||
|
||||
QVector<Connection *> m_connections;
|
||||
QPointer<Connection> m_connection;
|
||||
bool m_busy = false;
|
||||
|
||||
@@ -105,7 +102,6 @@ private:
|
||||
void saveSettings() const;
|
||||
|
||||
KAboutData m_aboutData;
|
||||
bool hasWindowSystem() const;
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <csapi/account-data.h>
|
||||
#include <csapi/content-repo.h>
|
||||
#include <csapi/profile.h>
|
||||
|
||||
#include "customemojimodel_p.h"
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#define running isJobPending
|
||||
#else
|
||||
#define running isJobRunning
|
||||
#endif
|
||||
|
||||
void CustomEmojiModel::fetchEmojies()
|
||||
{
|
||||
if (d->conn == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
QJsonObject emojies = data->contentJson()["images"].toObject();
|
||||
|
||||
// TODO: Remove with stable migration
|
||||
const auto legacyEmojies = data->contentJson()["emoticons"].toObject();
|
||||
for (const auto &emoji : legacyEmojies.keys()) {
|
||||
if (!emojies.contains(emoji)) {
|
||||
emojies[emoji] = legacyEmojies[emoji];
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
d->emojies.clear();
|
||||
|
||||
for (const auto &emoji : emojies.keys()) {
|
||||
const auto &data = emojies[emoji];
|
||||
|
||||
const auto e = emoji.startsWith(":") ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
||||
|
||||
d->emojies << CustomEmoji{e, data.toObject()["url"].toString(), QRegularExpression(QStringLiteral(R"((^|[^\\]))") + e)};
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
auto job = d->conn->uploadFile(location.toLocalFile());
|
||||
|
||||
if (running(job)) {
|
||||
connect(job, &BaseJob::success, this, [this, name, job] {
|
||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||
#ifdef QUOTIENT_07
|
||||
{QStringLiteral("url"), job->contentUri().toString()}
|
||||
#else
|
||||
{QStringLiteral("url"), job->contentUri()}
|
||||
#endif
|
||||
});
|
||||
json["images"] = emojiData;
|
||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CustomEmojiModel::removeEmoji(const QString &name)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
||||
Q_ASSERT(data != nullptr); // something's screwed if we get here with a nullptr
|
||||
auto json = data->contentJson();
|
||||
const QString _name = name.mid(1).chopped(1);
|
||||
auto emojiData = json["images"].toObject();
|
||||
|
||||
if (emojiData.contains(name)) {
|
||||
emojiData.remove(name);
|
||||
json["images"] = emojiData;
|
||||
}
|
||||
if (emojiData.contains(_name)) {
|
||||
emojiData.remove(_name);
|
||||
json["images"] = emojiData;
|
||||
}
|
||||
emojiData = json["emoticons"].toObject();
|
||||
if (emojiData.contains(name)) {
|
||||
emojiData.remove(name);
|
||||
json["emoticons"] = emojiData;
|
||||
}
|
||||
if (emojiData.contains(_name)) {
|
||||
emojiData.remove(_name);
|
||||
json["emoticons"] = emojiData;
|
||||
}
|
||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "customemojimodel_p.h"
|
||||
#include "emojimodel.h"
|
||||
|
||||
enum Roles {
|
||||
Name,
|
||||
ImageURL,
|
||||
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
||||
};
|
||||
|
||||
CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, d(new Private)
|
||||
{
|
||||
connect(this, &CustomEmojiModel::connectionChanged, this, &CustomEmojiModel::fetchEmojies);
|
||||
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
|
||||
if (!d->conn)
|
||||
return;
|
||||
|
||||
connect(d->conn, &Connection::accountDataChanged, this, [this](const QString &id) {
|
||||
if (id != QStringLiteral("im.ponies.user_emotes")) {
|
||||
return;
|
||||
}
|
||||
fetchEmojies();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
CustomEmojiModel::~CustomEmojiModel()
|
||||
{
|
||||
}
|
||||
|
||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
{
|
||||
const auto row = idx.row();
|
||||
if (row >= d->emojies.length()) {
|
||||
return QVariant();
|
||||
}
|
||||
const auto &data = d->emojies[row];
|
||||
|
||||
switch (Roles(role)) {
|
||||
case Roles::ModelData:
|
||||
return QVariant::fromValue(Emoji(QStringLiteral("image://mxc/") + data.url.mid(6), data.name, true));
|
||||
case Roles::Name:
|
||||
return data.name;
|
||||
case Roles::ImageURL:
|
||||
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
int CustomEmojiModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
||||
return d->emojies.length();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{Name, "name"},
|
||||
{ImageURL, "imageURL"},
|
||||
{ModelData, "modelData"},
|
||||
};
|
||||
}
|
||||
|
||||
Connection *CustomEmojiModel::connection() const
|
||||
{
|
||||
return d->conn;
|
||||
}
|
||||
|
||||
void CustomEmojiModel::setConnection(Connection *it)
|
||||
{
|
||||
if (d->conn == it) {
|
||||
return;
|
||||
}
|
||||
if (d->conn != nullptr) {
|
||||
disconnect(d->conn, nullptr, this, nullptr);
|
||||
}
|
||||
d->conn = it;
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
QString CustomEmojiModel::preprocessText(const QString &it)
|
||||
{
|
||||
auto cp = it;
|
||||
for (const auto &emoji : std::as_const(d->emojies)) {
|
||||
cp.replace(
|
||||
emoji.regexp,
|
||||
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
||||
{
|
||||
QVariantList results;
|
||||
for (const auto &emoji : std::as_const(d->emojies)) {
|
||||
if (results.length() >= 10)
|
||||
break;
|
||||
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
results << QVariant::fromValue(Emoji(QStringLiteral("image://mxc/") + emoji.url.mid(6), emoji.name, true));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "connection.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CustomEmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
// constructors
|
||||
|
||||
explicit CustomEmojiModel(QObject *parent = nullptr);
|
||||
~CustomEmojiModel();
|
||||
|
||||
// model
|
||||
|
||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// property setters
|
||||
|
||||
Connection *connection() const;
|
||||
void setConnection(Connection *it);
|
||||
Q_SIGNAL void connectionChanged();
|
||||
|
||||
// QML functions
|
||||
|
||||
Q_INVOKABLE QString preprocessText(const QString &it);
|
||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
||||
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
||||
Q_INVOKABLE void removeEmoji(const QString &name);
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
|
||||
void fetchEmojies();
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "customemojimodel.h"
|
||||
|
||||
struct CustomEmoji {
|
||||
QString name; // with :semicolons:
|
||||
QString url; // mxc://
|
||||
QRegularExpression regexp;
|
||||
};
|
||||
|
||||
struct CustomEmojiModel::Private {
|
||||
Connection *conn = nullptr;
|
||||
QList<CustomEmoji> emojies;
|
||||
};
|
||||
@@ -10,29 +10,18 @@
|
||||
DevicesModel::DevicesModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &DevicesModel::fetchDevices);
|
||||
|
||||
fetchDevices();
|
||||
}
|
||||
|
||||
void DevicesModel::fetchDevices()
|
||||
{
|
||||
if (Controller::instance().activeConnection()) {
|
||||
auto job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
|
||||
connect(job, &BaseJob::success, this, [this, job]() {
|
||||
beginResetModel();
|
||||
m_devices = job->devices();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
GetDevicesJob *job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
|
||||
connect(job, &BaseJob::success, this, [this, job]() {
|
||||
beginResetModel();
|
||||
m_devices = job->devices();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
QVariant DevicesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= rowCount(QModelIndex())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() < 0 || index.row() >= rowCount(QModelIndex()))
|
||||
return QVariant();
|
||||
switch (role) {
|
||||
case Id:
|
||||
return m_devices[index.row()].deviceId;
|
||||
@@ -44,7 +33,7 @@ QVariant DevicesModel::data(const QModelIndex &index, int role) const
|
||||
if (m_devices[index.row()].lastSeenTs)
|
||||
return *m_devices[index.row()].lastSeenTs;
|
||||
}
|
||||
return {};
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
int DevicesModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -73,9 +62,9 @@ void DevicesModel::logout(int index, const QString &password)
|
||||
authData["identifier"] = identifier;
|
||||
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
|
||||
connect(innerJob, &BaseJob::success, this, [this, index]() {
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
Q_EMIT beginRemoveRows(QModelIndex(), index, index);
|
||||
m_devices.remove(index);
|
||||
endRemoveRows();
|
||||
Q_EMIT endRemoveRows();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -85,12 +74,12 @@ void DevicesModel::setName(int index, const QString &name)
|
||||
{
|
||||
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
|
||||
QString oldName = m_devices[index].displayName;
|
||||
beginResetModel();
|
||||
Q_EMIT beginResetModel();
|
||||
m_devices[index].displayName = name;
|
||||
endResetModel();
|
||||
connect(job, &BaseJob::failure, this, [this, index, oldName]() {
|
||||
beginResetModel();
|
||||
Q_EMIT endResetModel();
|
||||
connect(job, &BaseJob::failure, this, [=]() {
|
||||
Q_EMIT beginResetModel();
|
||||
m_devices[index].displayName = oldName;
|
||||
endResetModel();
|
||||
Q_EMIT endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,5 @@ public:
|
||||
Q_INVOKABLE void setName(int index, const QString &name);
|
||||
|
||||
private:
|
||||
void fetchDevices();
|
||||
QVector<Quotient::Device> m_devices;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user