Compare commits
83 Commits
work/tfell
...
1.2
| 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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,3 @@ build
|
||||
neochat.kdev4
|
||||
compile_commands.json
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
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,22 +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(KF5_MIN_VERSION "5.86.0")
|
||||
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)
|
||||
@@ -28,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)
|
||||
@@ -38,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(1.2.80
|
||||
ecm_setup_version(1.2.0
|
||||
VARIABLE_PREFIX NEOCHAT
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
|
||||
)
|
||||
@@ -72,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)
|
||||
@@ -110,10 +105,6 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
PURPOSE "Add image editing capability to image attachments"
|
||||
)
|
||||
|
||||
find_package(QCoro REQUIRED)
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
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)
|
||||
@@ -131,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,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).
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
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.15 as Kirigami
|
||||
|
||||
@@ -68,8 +66,7 @@ ToolBar {
|
||||
id: fontMetrics
|
||||
font: inputField.font
|
||||
}
|
||||
|
||||
T.TextArea {
|
||||
TextArea {
|
||||
id: inputField
|
||||
focus: true
|
||||
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
|
||||
@@ -84,7 +81,6 @@ ToolBar {
|
||||
cursorShape: Qt.IBeamCursor
|
||||
z: 1
|
||||
}
|
||||
|
||||
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
topPadding: 0
|
||||
@@ -101,82 +97,6 @@ ToolBar {
|
||||
wrapMode: Text.Wrap
|
||||
readOnly: currentRoom.usesEncryption
|
||||
|
||||
palette: Kirigami.Theme.palette
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding,
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
placeholder.implicitWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding,
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
placeholder.implicitHeight + topPadding + bottomPadding)
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
hoverEnabled: !Kirigami.Settings.tabletMode
|
||||
|
||||
selectByMouse: !Kirigami.Settings.tabletMode
|
||||
|
||||
cursorDelegate: Loader {
|
||||
visible: inputField.activeFocus && !inputField.readOnly && inputField.selectionStart === inputField.selectionEnd
|
||||
active: visible
|
||||
sourceComponent: CursorDelegate { target: inputField }
|
||||
}
|
||||
|
||||
CursorHandle {
|
||||
id: selectionStartHandle
|
||||
target: inputField
|
||||
}
|
||||
|
||||
CursorHandle {
|
||||
id: selectionEndHandle
|
||||
target: inputField
|
||||
isSelectionEnd: true
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
// unfortunately, taphandler's pressed event only triggers when the press is lifted
|
||||
// we need to use the longpress signal since it triggers when the button is first pressed
|
||||
longPressThreshold: 0
|
||||
onLongPressed: TextFieldContextMenu.targetClick(point, inputField, spellcheckhighlighter, inputField.positionAt(point.position.x, point.position.y));
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
if (!Kirigami.Settings.tabletMode) {
|
||||
return;
|
||||
}
|
||||
forceActiveFocus();
|
||||
cursorPosition = positionAt(event.x, event.y);
|
||||
selectWord();
|
||||
}
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
MobileTextActionsToolBar.controlRoot = inputField;
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: placeholder
|
||||
x: inputField.leftPadding
|
||||
y: inputField.topPadding
|
||||
width: inputField.width - (inputField.leftPadding + inputField.rightPadding)
|
||||
height: inputField.height - (inputField.topPadding + inputField.bottomPadding)
|
||||
|
||||
text: inputField.placeholderText
|
||||
font: inputField.font
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
horizontalAlignment: inputField.horizontalAlignment
|
||||
verticalAlignment: inputField.verticalAlignment
|
||||
visible: !inputField.length && !inputField.preeditText && (!inputField.activeFocus || inputField.horizontalAlignment !== Qt.AlignHCenter)
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
@@ -187,18 +107,6 @@ ToolBar {
|
||||
room: currentRoom ?? null
|
||||
}
|
||||
|
||||
SpellcheckHighlighter {
|
||||
id: spellcheckhighlighter
|
||||
document: inputField.textDocument
|
||||
cursorPosition: inputField.cursorPosition
|
||||
selectionStart: inputField.selectionStart
|
||||
selectionEnd: inputField.selectionEnd
|
||||
onChangeCursorPosition: {
|
||||
inputField.cursorPosition = start;
|
||||
inputField.moveCursorSelection(end, TextEdit.SelectCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timeoutTimer
|
||||
repeat: false
|
||||
@@ -239,9 +147,6 @@ ToolBar {
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
// trigger if context menu button is pressed
|
||||
TextFieldContextMenu.targetKeyPressed(event, inputField)
|
||||
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
switchRoomDown();
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
@@ -278,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();
|
||||
@@ -325,27 +211,17 @@ ToolBar {
|
||||
chatBar.complete();
|
||||
}
|
||||
|
||||
onPressed: MobileTextActionsToolBar.shouldBeVisible = true;
|
||||
|
||||
onTextChanged: {
|
||||
MobileTextActionsToolBar.shouldBeVisible = false;
|
||||
timeoutTimer.restart()
|
||||
repeatTimer.start()
|
||||
currentRoom.cachedInput = text
|
||||
autoAppeared = false;
|
||||
|
||||
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
|
||||
const completionInfo = documentHandler.getAutocompletionInfo();
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.Ignore) {
|
||||
if (completionInfo.keyword) {
|
||||
// custom emojis
|
||||
const idx = completionMenu.currentIndex;
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
completionMenu.currentIndex = idx;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.None) {
|
||||
isCompleting = false;
|
||||
return;
|
||||
@@ -357,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,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)) {
|
||||
@@ -486,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;
|
||||
@@ -499,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
|
||||
@@ -96,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
|
||||
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick.Templates 2.15
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property alias target: root.parent
|
||||
|
||||
Rectangle {
|
||||
id: cursorLine
|
||||
property real previousX: 0
|
||||
property real previousY: 0
|
||||
parent: target
|
||||
implicitWidth: target.cursorRectangle.width
|
||||
implicitHeight: target.cursorRectangle.height
|
||||
x: Math.floor(target.cursorRectangle.x)
|
||||
y: Math.floor(target.cursorRectangle.y)
|
||||
|
||||
color: target.color
|
||||
SequentialAnimation {
|
||||
id: blinkAnimation
|
||||
running: root.visible && Qt.styleHints.cursorFlashTime != 0 && target.selectionStart === target.selectionEnd
|
||||
PropertyAction {
|
||||
target: cursorLine
|
||||
property: "opacity"
|
||||
value: 1
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: Qt.styleHints.cursorFlashTime/2
|
||||
}
|
||||
SequentialAnimation {
|
||||
loops: Animation.Infinite
|
||||
OpacityAnimator {
|
||||
target: cursorLine
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Qt.styleHints.cursorFlashTime/2
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
OpacityAnimator {
|
||||
target: cursorLine
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Qt.styleHints.cursorFlashTime/2
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.target
|
||||
function onCursorPositionChanged() {
|
||||
blinkAnimation.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
|
||||
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick.Templates 2.15
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
property Item target
|
||||
property bool isSelectionEnd: false
|
||||
visible: Kirigami.Settings.tabletMode && target.activeFocus && (isSelectionEnd ? target.selectionStart !== target.selectionEnd : true)
|
||||
active: visible
|
||||
sourceComponent: Kirigami.ShadowedRectangle {
|
||||
id: handle
|
||||
property real selectionStartX: Math.floor(Qt.inputMethod.anchorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
|
||||
property real selectionStartY: Math.floor(Qt.inputMethod.anchorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
|
||||
property real selectionEndX: Math.floor(Qt.inputMethod.cursorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
|
||||
property real selectionEndY: Math.floor(Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
|
||||
property real pointyBitVerticalOffset: Math.abs(pointyBit.y*2)
|
||||
parent: Overlay.overlay
|
||||
x: isSelectionEnd ? selectionEndX : selectionStartX
|
||||
y: isSelectionEnd ? selectionEndY : selectionStartY
|
||||
|
||||
// HACK: make it appear above most popups that show up in the
|
||||
// overlay in case any of them use TextField or TextArea
|
||||
z: 999
|
||||
|
||||
//opacity: target.activeFocus ? 1 : 0
|
||||
implicitHeight: {
|
||||
let h = Kirigami.Units.gridUnit
|
||||
return h - (h % 2 == 0 ? 1 : 0)
|
||||
}
|
||||
implicitWidth: implicitHeight
|
||||
radius: width/2
|
||||
|
||||
color: target.selectionColor
|
||||
|
||||
shadow {
|
||||
color: Qt.rgba(0,0,0,0.2)
|
||||
size: 3
|
||||
yOffset: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pointyBit
|
||||
x: (parent.width - width)/2
|
||||
y: -height/4 + 0.2 // magic number to get it to line up with the edge of the circle
|
||||
implicitHeight: parent.implicitHeight/2
|
||||
implicitWidth: implicitHeight
|
||||
antialiasing: true
|
||||
rotation: 45
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Kirigami.ShadowedRectangle {
|
||||
id: inner
|
||||
visible: target.selectionStart !== target.selectionEnd && (handle.y < selectionStartY || handle.y < selectionEndY)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallBorder
|
||||
color: target.selectedTextColor
|
||||
radius: height/2
|
||||
Rectangle {
|
||||
id: innerPointyBit
|
||||
x: (parent.width - width)/2
|
||||
y: -height/4 + 0.8 // magic number to get it to line up with the edge of the circle
|
||||
implicitHeight: pointyBit.implicitHeight
|
||||
implicitWidth: implicitHeight
|
||||
antialiasing: true
|
||||
rotation: 45
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
enabled: handle.visible
|
||||
anchors.fill: parent
|
||||
// preventStealing: true
|
||||
onPositionChanged: {
|
||||
let pos = mapToItem(root.target, mouse.x, mouse.y);
|
||||
pos = root.target.positionAt(pos.x, pos.y - handle.height - handle.pointyBitVerticalOffset);
|
||||
|
||||
if (target.selectionStart !== target.selectionEnd) {
|
||||
if (!isSelectionEnd) {
|
||||
root.target.select(Math.min(pos, root.target.selectionEnd - 1), root.target.selectionEnd);
|
||||
} else {
|
||||
root.target.select(root.target.selectionStart, Math.max(pos, root.target.selectionStart + 1));
|
||||
}
|
||||
} else {
|
||||
root.target.cursorPosition = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.15
|
||||
import org.kde.kirigami 2.5 as Kirigami
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property Item controlRoot
|
||||
parent: controlRoot ? controlRoot.Window.contentItem : undefined
|
||||
modal: false
|
||||
focus: false
|
||||
closePolicy: Popup.NoAutoClose
|
||||
property bool shouldBeVisible: false
|
||||
|
||||
x: {
|
||||
if (!controlRoot || !controlRoot.Window.contentItem) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, controlRoot.positionToRectangle(controlRoot.selectionStart).x, 0).x - root.width/2), controlRoot.Window.contentItem.width - root.width);
|
||||
}
|
||||
|
||||
y: {
|
||||
if (!controlRoot || !controlRoot.Window.contentItem) {
|
||||
return 0;
|
||||
}
|
||||
var desiredY = controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionStart).y).y - root.height;
|
||||
|
||||
if (desiredY >= 0) {
|
||||
return Math.min(desiredY, controlRoot.Window.contentItem.height - root.height);
|
||||
} else {
|
||||
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionEnd).y + Math.round(Kirigami.Units.gridUnit*1.5)).y), controlRoot.Window.contentItem.height - root.height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
visible: controlRoot ? shouldBeVisible && Qt.platform.os !== "android" && Kirigami.Settings.tabletMode && (controlRoot.selectedText.length > 0 || controlRoot.canPaste) : false
|
||||
|
||||
width: contentItem.implicitWidth + leftPadding + rightPadding
|
||||
|
||||
contentItem: RowLayout {
|
||||
ToolButton {
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.name: "edit-cut"
|
||||
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
|
||||
onClicked: {
|
||||
controlRoot.cut();
|
||||
}
|
||||
}
|
||||
ToolButton {
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.name: "edit-copy"
|
||||
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
|
||||
onClicked: {
|
||||
controlRoot.copy();
|
||||
}
|
||||
}
|
||||
ToolButton {
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.name: "edit-paste"
|
||||
visible: controlRoot && controlRoot.canPaste
|
||||
onClicked: {
|
||||
controlRoot.paste();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com>
|
||||
SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick 2.6
|
||||
import QtQml 2.2
|
||||
import QtQuick.Controls 2.15
|
||||
import org.kde.kirigami 2.5 as Kirigami
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
|
||||
property Item target
|
||||
property bool deselectWhenMenuClosed: true
|
||||
property int restoredCursorPosition: 0
|
||||
property int restoredSelectionStart
|
||||
property int restoredSelectionEnd
|
||||
property bool persistentSelectionSetting
|
||||
property var spellcheckhighlighter: null
|
||||
property var suggestions: ([])
|
||||
Component.onCompleted: persistentSelectionSetting = persistentSelectionSetting // break binding
|
||||
|
||||
property var runOnMenuClose
|
||||
|
||||
parent: Overlay.overlay
|
||||
|
||||
function storeCursorAndSelection() {
|
||||
contextMenu.restoredCursorPosition = target.cursorPosition;
|
||||
contextMenu.restoredSelectionStart = target.selectionStart;
|
||||
contextMenu.restoredSelectionEnd = target.selectionEnd;
|
||||
}
|
||||
|
||||
// target is pressed with mouse
|
||||
function targetClick(handlerPoint, newTarget, spellcheckhighlighter, mousePosition) {
|
||||
if (handlerPoint.pressedButtons === Qt.RightButton) { // only accept just right click
|
||||
if (contextMenu.visible) {
|
||||
deselectWhenMenuClosed = false; // don't deselect text if menu closed by right click on textfield
|
||||
dismiss();
|
||||
} else {
|
||||
contextMenu.target = newTarget;
|
||||
contextMenu.target.persistentSelection = true; // persist selection when menu is opened
|
||||
contextMenu.spellcheckhighlighter = spellcheckhighlighter
|
||||
contextMenu.suggestions = spellcheckhighlighter.suggestions(mousePosition);
|
||||
storeCursorAndSelection();
|
||||
popup(contextMenu.target);
|
||||
// slightly locate context menu away from mouse so no item is selected when menu is opened
|
||||
x += 1
|
||||
y += 1
|
||||
}
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
// context menu keyboard key
|
||||
function targetKeyPressed(event, newTarget) {
|
||||
if (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Menu) {
|
||||
contextMenu.target = newTarget;
|
||||
target.persistentSelection = true; // persist selection when menu is opened
|
||||
storeCursorAndSelection();
|
||||
popup(contextMenu.target);
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool targetIsPassword: target !== null && (target.echoMode === TextInput.PasswordEchoOnEdit || target.echoMode === TextInput.Password)
|
||||
|
||||
onAboutToShow: {
|
||||
if (Overlay.overlay) {
|
||||
let tempZ = 0
|
||||
for (let i in Overlay.overlay.visibleChildren) {
|
||||
tempZ = Math.max(tempZ, Overlay.overlay.visibleChildren[i].z)
|
||||
}
|
||||
z = tempZ + 1
|
||||
}
|
||||
}
|
||||
|
||||
// deal with whether or not text should be deselected
|
||||
onClosed: {
|
||||
// restore text field's original persistent selection setting
|
||||
target.persistentSelection = persistentSelectionSetting
|
||||
// deselect text field text if menu is closed not because of a right click on the text field
|
||||
if (deselectWhenMenuClosed) {
|
||||
target.deselect();
|
||||
}
|
||||
deselectWhenMenuClosed = true;
|
||||
|
||||
// restore cursor position
|
||||
target.forceActiveFocus();
|
||||
target.cursorPosition = restoredCursorPosition;
|
||||
target.select(restoredSelectionStart, restoredSelectionEnd);
|
||||
|
||||
// run action
|
||||
runOnMenuClose();
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
runOnMenuClose = function() {};
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
active: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
|
||||
model: suggestions
|
||||
delegate: MenuItem {
|
||||
text: modelData
|
||||
onClicked: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {
|
||||
spellcheckhighlighter.replaceWord(modelData);
|
||||
};
|
||||
}
|
||||
}
|
||||
onObjectAdded: contextMenu.insertItem(0, object)
|
||||
onObjectRemoved: contextMenu.removeItem(0)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled && suggestions.length === 0
|
||||
action: Action {
|
||||
text: spellcheckhighlighter ? i18nc("@action:inmenu", "No suggestions for %1", spellcheckhighlighter.wordUnderMouse) : ""
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
|
||||
action: Action {
|
||||
text: i18n("Add to dictionary")
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {
|
||||
spellcheckhighlighter.addWordToDictionary(spellcheckhighlighter.wordUnderMouse)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
|
||||
action: Action {
|
||||
text: i18n("Ignore")
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {
|
||||
spellcheckhighlighter.ignoreWord(spellcheckhighlighter.wordUnderMouse)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly
|
||||
action: Action {
|
||||
icon.name: "edit-undo-symbolic"
|
||||
text: i18nc("@action:inmenu", "Undo")
|
||||
shortcut: StandardKey.Undo
|
||||
}
|
||||
enabled: target !== null && target.canUndo
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.undo()};
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly
|
||||
action: Action {
|
||||
icon.name: "edit-redo-symbolic"
|
||||
text: i18nc("@action:inmenu", "Redo")
|
||||
shortcut: StandardKey.Redo
|
||||
}
|
||||
enabled: target !== null && target.canRedo
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.redo()};
|
||||
}
|
||||
}
|
||||
MenuSeparator {
|
||||
visible: target !== null && !target.readOnly
|
||||
}
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly && !targetIsPassword
|
||||
action: Action {
|
||||
icon.name: "edit-cut-symbolic"
|
||||
text: i18nc("@action:inmenu", "Cut")
|
||||
shortcut: StandardKey.Cut
|
||||
}
|
||||
enabled: target !== null && target.selectedText
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.cut()}
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
action: Action {
|
||||
icon.name: "edit-copy-symbolic"
|
||||
text: i18nc("@action:inmenu", "Copy")
|
||||
shortcut: StandardKey.Copy
|
||||
}
|
||||
enabled: target !== null && target.selectedText
|
||||
visible: !targetIsPassword
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.copy()}
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly
|
||||
action: Action {
|
||||
icon.name: "edit-paste-symbolic"
|
||||
text: i18nc("@action:inmenu", "Paste")
|
||||
shortcut: StandardKey.Paste
|
||||
}
|
||||
enabled: target !== null && target.canPaste
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.paste()};
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
visible: target !== null && !target.readOnly
|
||||
action: Action {
|
||||
icon.name: "edit-delete-symbolic"
|
||||
text: i18nc("@action:inmenu", "Delete")
|
||||
shortcut: StandardKey.Delete
|
||||
}
|
||||
enabled: target !== null && target.selectedText
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.remove(target.selectionStart, target.selectionEnd)};
|
||||
}
|
||||
}
|
||||
MenuSeparator {
|
||||
visible: !targetIsPassword
|
||||
}
|
||||
MenuItem {
|
||||
action: Action {
|
||||
icon.name: "edit-select-all-symbolic"
|
||||
text: i18nc("@action:inmenu", "Select All")
|
||||
shortcut: StandardKey.SelectAll
|
||||
}
|
||||
visible: !targetIsPassword
|
||||
onTriggered: {
|
||||
deselectWhenMenuClosed = false;
|
||||
runOnMenuClose = function() {target.selectAll()};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,3 @@ ReplyPane 1.0 ReplyPane.qml
|
||||
AttachmentPane 1.0 AttachmentPane.qml
|
||||
CompletionMenu 1.0 CompletionMenu.qml
|
||||
EmojiPickerPane 1.0 EmojiPickerPane.qml
|
||||
singleton TextFieldContextMenu 1.0 TextFieldContextMenu.qml
|
||||
CursorDelegate 1.0 CursorDelegate.qml
|
||||
CursorHandle 1.0 CursorHandle.qml
|
||||
singleton MobileTextActionsToolBar 1.0 MobileTextActionsToolBar.qml
|
||||
|
||||
@@ -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,9 +11,6 @@ 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
|
||||
@@ -32,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,8 +78,9 @@ MouseArea {
|
||||
id: replyText
|
||||
textMessage: reply.display
|
||||
textFormat: Text.RichText
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,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
|
||||
|
||||
@@ -14,7 +14,6 @@ RowLayout {
|
||||
id: row
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: icon
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignTop
|
||||
@@ -38,10 +37,9 @@ RowLayout {
|
||||
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,13 +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)
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||
|
||||
text: "<style>
|
||||
table {
|
||||
@@ -38,19 +38,13 @@ 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
|
||||
@@ -62,12 +56,9 @@ a{
|
||||
applicationWindow().hoverLinkIndicator.text = "";
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
||||
onTapped: spoilerRevealed = true
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -121,7 +121,7 @@ Kirigami.OverlaySheet {
|
||||
|
||||
RowLayout {
|
||||
id: altAlias
|
||||
Kirigami.FormData.label: i18n("Other Aliases:")
|
||||
Kirigami.FormData.label: i18n("Alt Aliases")
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.altAliases && room.altAliases.length
|
||||
@@ -139,6 +139,7 @@ Kirigami.OverlaySheet {
|
||||
|
||||
Label {
|
||||
text: modelData
|
||||
color: Kirigami.Theme.disabledColor
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
|
||||
@@ -74,7 +74,7 @@ Kirigami.OverlaySheet {
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: room.htmlSafeMemberName(user.id)
|
||||
text: displayName
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
@@ -129,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,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: pushReplaceLayer("qrc:/imports/NeoChat/Page/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/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
|
||||
/**
|
||||
* Context menu when clicking on a room in the room list
|
||||
*/
|
||||
Menu {
|
||||
id: root
|
||||
property var selectedText
|
||||
|
||||
Repeater {
|
||||
model: WebShortcutModel {
|
||||
selectedText: root.selectedText
|
||||
}
|
||||
delegate: MenuItem {
|
||||
text: model.display
|
||||
icon.name: model.decoration
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -66,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
|
||||
@@ -130,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,7 +5,6 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import org.kde.syntaxhighlighting 1.0
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
@@ -17,16 +16,9 @@ Kirigami.OverlaySheet {
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: sourceTextArea
|
||||
text: sourceText
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
SyntaxHighlighter {
|
||||
textEdit: sourceTextArea
|
||||
repository: Repository
|
||||
definition: "JSON"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: AccountListModel { }
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: model.user.displayName
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: model.user.id
|
||||
icon: model.user.avatarMediaId ? ("image://mxc/" + model.user.avatarMediaId) : "im-user"
|
||||
|
||||
onClicked: {
|
||||
Controller.activeConnection = model.connection
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection, true)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
]
|
||||
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
|
||||
|
||||
@@ -138,10 +97,11 @@ Kirigami.Page {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -178,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 {
|
||||
|
||||
@@ -9,71 +9,43 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.Page {
|
||||
Kirigami.ScrollablePage {
|
||||
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
|
||||
ListView {
|
||||
model: DevicesModel {
|
||||
id: devices
|
||||
}
|
||||
}
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Controls.ScrollView {
|
||||
id: scroll
|
||||
Component.onCompleted: background.visible = pageSettingStack.wideMode
|
||||
anchors.fill: parent
|
||||
ListView {
|
||||
clip: true
|
||||
model: DevicesModel {
|
||||
id: devices
|
||||
text: model.displayName
|
||||
subtitle: model.id
|
||||
icon: "network-connect"
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,39 +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)
|
||||
}
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
text: i18n("New room")
|
||||
icon.name: "list-add"
|
||||
}
|
||||
|
||||
function goToNextRoom() {
|
||||
do {
|
||||
@@ -65,55 +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.margins: 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
|
||||
@@ -130,7 +64,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemSelectionModel {
|
||||
id: itemSelection
|
||||
model: roomListModel
|
||||
@@ -154,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))
|
||||
}
|
||||
@@ -165,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,125 +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")
|
||||
flat: true
|
||||
|
||||
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
|
||||
|
||||
@@ -26,18 +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
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
if(!RoomManager.currentRoom) {
|
||||
if(pageStack.lastItem == page) {
|
||||
pageStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
title: currentRoom.displayName
|
||||
|
||||
signal switchRoomUp()
|
||||
signal switchRoomDown()
|
||||
@@ -55,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);
|
||||
}
|
||||
}
|
||||
@@ -64,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;
|
||||
@@ -84,6 +73,8 @@ Kirigami.ScrollablePage {
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: invitation
|
||||
|
||||
property var id
|
||||
|
||||
visible: currentRoom && currentRoom.isInvite
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Accept this invitation?")
|
||||
@@ -92,7 +83,7 @@ Kirigami.ScrollablePage {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Reject")
|
||||
|
||||
onClicked: RoomManager.leaveRoom(page.currentRoom);
|
||||
onClicked: RoomManager.leave(page.currentRoom);
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
@@ -101,6 +92,8 @@ Kirigami.ScrollablePage {
|
||||
|
||||
onClicked: {
|
||||
currentRoom.acceptInvitation();
|
||||
invitation.id = currentRoom.id
|
||||
currentRoom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,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) {
|
||||
@@ -172,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
|
||||
@@ -235,7 +228,7 @@ Kirigami.ScrollablePage {
|
||||
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
|
||||
@@ -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;
|
||||
@@ -372,17 +365,18 @@ Kirigami.ScrollablePage {
|
||||
|
||||
innerObject: TextDelegate {
|
||||
isEmote: true
|
||||
Layout.fillWidth: Config.compactLayout
|
||||
Layout.fillWidth: !Config.showAvatarInTimeline
|
||||
Layout.maximumWidth: emoteContainer.bubbleMaxWidth
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,17 +392,18 @@ Kirigami.ScrollablePage {
|
||||
hoverComponent: hoverActions
|
||||
|
||||
innerObject: TextDelegate {
|
||||
Layout.fillWidth: Config.compactLayout
|
||||
Layout.fillWidth: !Config.showAvatarInTimeline
|
||||
Layout.maximumWidth: messageContainer.bubbleMaxWidth
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
|
||||
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
|
||||
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,10 +418,11 @@ Kirigami.ScrollablePage {
|
||||
onReplyClicked: goToEvent(eventID)
|
||||
|
||||
innerObject: TextDelegate {
|
||||
Layout.fillWidth: !Config.compactLayout
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,6 +439,7 @@ 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 {
|
||||
@@ -453,7 +450,7 @@ Kirigami.ScrollablePage {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,22 +550,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -588,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 {
|
||||
@@ -900,9 +881,8 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
/// Open context menu for normal message
|
||||
function openMessageContext(author, message, eventId, source, eventType, formattedBody, selectedText) {
|
||||
function openMessageContext(author, message, eventId, source, eventType, formattedBody) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(page, {
|
||||
selectedText: selectedText,
|
||||
author: author,
|
||||
message: message,
|
||||
eventId: eventId,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
@@ -9,80 +8,81 @@ 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: i18n("Settings")
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
|
||||
onBackRequested: {
|
||||
if (pageSettingStack.depth > 1 && !pageSettingStack.wideMode && pageSettingStack.currentIndex !== 0) {
|
||||
event.accepted = true;
|
||||
pageSettingStack.pop();
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PageRow {
|
||||
id: pageSettingStack
|
||||
anchors.fill: parent
|
||||
columnView.columnWidth: Kirigami.Units.gridUnit * 12
|
||||
initialPage: Kirigami.ScrollablePage {
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
ListView {
|
||||
Component.onCompleted: if (pageSettingStack.wideMode) {
|
||||
actions[0].trigger();
|
||||
}
|
||||
property list<Kirigami.Action> actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("General")
|
||||
icon.name: "org.kde.neochat"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/GeneralSettingsPage.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Appearance")
|
||||
icon.name: "preferences-desktop-theme-global"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/AppearanceSettingsPage.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Accounts")
|
||||
icon.name: "preferences-system-users"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Page/AccountsPage.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Custom Emoji")
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/Emoticons.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Devices")
|
||||
iconName: "network-connect"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Page/DevicesPage.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("About NeoChat")
|
||||
icon.name: "help-about"
|
||||
onTriggered: pageSettingStack.push(aboutPage)
|
||||
}
|
||||
]
|
||||
model: actions
|
||||
delegate: Kirigami.BasicListItem {
|
||||
action: modelData
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: aboutPage
|
||||
Kirigami.AboutPage {
|
||||
aboutData: Controller.aboutData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onErrorOccured(message) {
|
||||
onErrorOccured: {
|
||||
headerMessage.text = message;
|
||||
headerMessage.visible = true;
|
||||
headerMessage.type = Kirigami.MessageType.Error;
|
||||
|
||||
@@ -198,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) : ""
|
||||
|
||||
@@ -1,254 +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 {
|
||||
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,155 +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 {
|
||||
|
||||
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,78 +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 {
|
||||
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,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,2 +0,0 @@
|
||||
module NeoChat.Settings
|
||||
ThemeRadioButton 1.0 ThemeRadioButton.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]=ਨਵਾਂ ਸੁਨੇਹਾ ਹੈ
|
||||
|
||||
@@ -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>
|
||||
@@ -117,21 +105,19 @@
|
||||
<p xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前,NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
|
||||
<p>NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
|
||||
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
|
||||
<p xml:lang="ca">El NeoChat funciona en el mòbils i a l'escriptori, proporcionant un experiència d'usuari coherent.</p>
|
||||
<p xml:lang="ca-valencia">El NeoChat funciona en el mòbils i a l'escriptori, proporcionant un experiència d'usuari coherent.</p>
|
||||
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
|
||||
<p xml:lang="ca-valencia">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
|
||||
<p xml:lang="de">NeoChat funktioniert sowohl auf dem Mobiltelefon als auch auf dem Arbeitsfläche und bietet ein einheitliches Benutzererlebnis. </p>
|
||||
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
|
||||
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
|
||||
<p xml:lang="eu">NeoChat mugikorretan eta mahaigainean dabil, erabiltzaile esperientzia koherentea eskainiz.</p>
|
||||
<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>
|
||||
|
||||
@@ -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]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
|
||||
|
||||
286
qml/main.qml
286
qml/main.qml
@@ -6,7 +6,7 @@ import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
@@ -27,14 +27,9 @@ Kirigami.ApplicationWindow {
|
||||
onClosing: Controller.saveWindowGeometry(root)
|
||||
|
||||
pageStack.initialPage: LoadingPage {}
|
||||
pageStack.globalToolBar.canContainHandles: true
|
||||
|
||||
property bool roomListLoaded: false
|
||||
|
||||
property RoomPage roomPage: RoomPage {
|
||||
KeyNavigation.left: pageStack.get(0)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.quitAction
|
||||
function onTriggered() {
|
||||
@@ -42,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
|
||||
@@ -62,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) {
|
||||
pageStack.push(root.roomPage);
|
||||
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);
|
||||
}
|
||||
@@ -146,7 +140,6 @@ Kirigami.ApplicationWindow {
|
||||
root.show()
|
||||
root.raise()
|
||||
root.requestActivate()
|
||||
Controller.raiseWindow(root)
|
||||
}
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
@@ -160,96 +153,110 @@ 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.columnView.columnWidth: Kirigami.Units.gridUnit * 17
|
||||
|
||||
globalDrawer: Kirigami.GlobalDrawer {
|
||||
property bool hasLayer
|
||||
contentItem.implicitWidth: columnWidth
|
||||
isMenu: true
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Explore rooms")
|
||||
icon.name: "compass"
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms") && Controller.accountCount > 0
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Start a Chat")
|
||||
icon.name: "irc-join-channel"
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "irc-join-channel"
|
||||
onTriggered: {
|
||||
let dialog = createRoomDialog.createObject(root.overlay);
|
||||
dialog.open();
|
||||
}
|
||||
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: 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"
|
||||
enabled: Controller.accountCount > 0
|
||||
onTriggered: Controller.logout(Controller.activeConnection, true)
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Quit")
|
||||
icon.name: "gtk-quit"
|
||||
shortcut: StandardKey.Quit
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Component {
|
||||
id: aboutPage
|
||||
Kirigami.AboutPage {
|
||||
aboutData: Controller.aboutData
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: roomListComponent
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
Connections {
|
||||
target: Config
|
||||
function onBlurChanged() {
|
||||
Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
target: LoginHelper
|
||||
function onInitialSyncFinished() {
|
||||
pageStack.replace(roomListComponent, {
|
||||
activeConnection: Controller.activeConnection
|
||||
});
|
||||
roomListLoaded = true;
|
||||
RoomManager.loadInitialRoom();
|
||||
}
|
||||
function onCompactLayoutChanged() {
|
||||
Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
Connections {
|
||||
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
|
||||
});
|
||||
@@ -258,6 +265,13 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function onBusyChanged() {
|
||||
if(!Controller.busy && roomListLoaded === false) {
|
||||
pageStack.replace(roomListComponent);
|
||||
roomListLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onConnectionDropped() {
|
||||
if (Controller.accountCount === 0) {
|
||||
RoomManager.reset();
|
||||
@@ -273,7 +287,6 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
function onShowWindow() {
|
||||
root.showWindow()
|
||||
root.raise()
|
||||
}
|
||||
|
||||
function onUserConsentRequired(url) {
|
||||
@@ -284,7 +297,7 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
function onDirectChatAvailable(directChat) {
|
||||
onDirectChatAvailable: {
|
||||
RoomManager.enterRoom(Controller.activeConnection.room(directChat.id));
|
||||
}
|
||||
}
|
||||
@@ -354,7 +367,7 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
|
||||
property Item hoverLinkIndicator: QQC2.Control {
|
||||
parent: overlay.parent
|
||||
parent: overlay.parent
|
||||
property alias text: linkText.text
|
||||
opacity: text.length > 0 ? 1 : 0
|
||||
|
||||
@@ -369,69 +382,4 @@ Kirigami.ApplicationWindow {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settingsPageComponent
|
||||
SettingsPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListComponent
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
|
||||
Connections {
|
||||
target: root.roomPage
|
||||
function onSwitchRoomUp() {
|
||||
roomList.goToNextRoom();
|
||||
}
|
||||
function onSwitchRoomDown() {
|
||||
roomList.goToPreviousRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: Kirigami.NavigationTabBar {
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "globe"
|
||||
text: "Spaces"
|
||||
checked: fooPage.visble
|
||||
onTriggered: {
|
||||
console.warn("foo")
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "dialog-messages"
|
||||
text: "Rooms"
|
||||
checked: fooPage.visble
|
||||
onTriggered: {
|
||||
while (pageStack.depth > 0) {
|
||||
pageStack.pop();
|
||||
}
|
||||
pageStack.push(roomListComponent);
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "document-open-recent-symbolic"
|
||||
text: "Recent"
|
||||
checked: fooPage.visble
|
||||
onTriggered: {
|
||||
console.warn("foo")
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "settings-configure"
|
||||
text: "Settings"
|
||||
checked: fooPage.visble
|
||||
onTriggered: {
|
||||
while (pageStack.depth > 0) {
|
||||
pageStack.pop();
|
||||
}
|
||||
pageStack.push(settingsPageComponent);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
res.qrc
15
res.qrc
@@ -18,18 +18,12 @@
|
||||
<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>
|
||||
<file>imports/NeoChat/Component/ChatBox/AttachmentPane.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/ReplyPane.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/CompletionMenu.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/CursorHandle.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/CursorDelegate.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/MobileTextActionsToolBar.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/TextFieldContextMenu.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/qmldir</file>
|
||||
<file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file>
|
||||
<file>imports/NeoChat/Component/Emoji/qmldir</file>
|
||||
@@ -44,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>
|
||||
@@ -63,8 +56,6 @@
|
||||
<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>
|
||||
@@ -73,11 +64,5 @@
|
||||
<file>qtquickcontrols2.conf</file>
|
||||
<file>imports/NeoChat/Component/glowdot.png</file>
|
||||
<file>imports/NeoChat/Component/confetti.png</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/qmldir</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -8,8 +8,6 @@ add_executable(neochat
|
||||
controller.cpp
|
||||
actionshandler.cpp
|
||||
emojimodel.cpp
|
||||
customemojimodel.cpp
|
||||
customemojimodel+network.cpp
|
||||
clipboard.cpp
|
||||
matriximageprovider.cpp
|
||||
messageeventmodel.cpp
|
||||
@@ -32,10 +30,6 @@ add_executable(neochat
|
||||
stickerevent.cpp
|
||||
chatboxhelper.cpp
|
||||
commandmodel.cpp
|
||||
webshortcutmodel.cpp
|
||||
spellcheckhighlighter.cpp
|
||||
blurhash.cpp
|
||||
blurhashimageprovider.cpp
|
||||
../res.qrc
|
||||
)
|
||||
|
||||
@@ -48,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)
|
||||
@@ -70,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"
|
||||
@@ -104,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)
|
||||
@@ -118,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})
|
||||
|
||||
@@ -8,13 +8,31 @@
|
||||
AccountListModel::AccountListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(&Controller::instance(), &Controller::connectionAdded, this, [=]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
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, [=]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,17 +42,19 @@ QVariant AccountListModel::data(const QModelIndex &index, int role) const
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= Controller::instance().connections().count()) {
|
||||
if (index.row() >= m_connections.count()) {
|
||||
qDebug() << "AccountListModel, something's wrong: index.row() >= "
|
||||
"m_users.count()";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto connection = Controller::instance().connections().at(index.row());
|
||||
auto m_connection = m_connections.at(index.row());
|
||||
|
||||
if (role == UserRole) {
|
||||
return QVariant::fromValue(connection->user());
|
||||
return QVariant::fromValue(m_connection->user());
|
||||
}
|
||||
if (role == ConnectionRole) {
|
||||
return QVariant::fromValue(connection);
|
||||
return QVariant::fromValue(m_connection);
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -46,7 +66,7 @@ int AccountListModel::rowCount(const QModelIndex &parent) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Controller::instance().connections().count();
|
||||
return m_connections.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountListModel::roleNames() const
|
||||
|
||||
@@ -23,4 +23,7 @@ public:
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
QVector<Connection *> m_connections;
|
||||
};
|
||||
|
||||
@@ -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,28 +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;
|
||||
QString cleanedText = text;
|
||||
|
||||
auto preprocess = [cem](const QString &it) -> QString {
|
||||
if (cem == nullptr) {
|
||||
return it;
|
||||
}
|
||||
return cem->preprocessText(it);
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -132,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
|
||||
@@ -179,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;
|
||||
}
|
||||
|
||||
@@ -215,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"));
|
||||
@@ -253,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
|
||||
@@ -306,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;
|
||||
}
|
||||
@@ -330,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>
|
||||
@@ -39,18 +39,20 @@
|
||||
#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;
|
||||
@@ -113,7 +115,8 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
connect(m_mgr, &QNetworkConfigurationManager::onlineStateChanged, this, &Controller::isOnlineChanged);
|
||||
connect(m_mgr, &QNetworkConfigurationManager::onlineStateChanged,
|
||||
this, &Controller::isOnlineChanged);
|
||||
}
|
||||
|
||||
Controller::~Controller()
|
||||
@@ -144,7 +147,7 @@ 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);
|
||||
}
|
||||
@@ -152,6 +155,7 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
|
||||
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,7 +169,7 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
|
||||
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)
|
||||
@@ -260,13 +264,13 @@ void Controller::invokeLogin()
|
||||
if (!account.homeserver().isEmpty()) {
|
||||
auto accessToken = loadAccessTokenFromKeyChain(account);
|
||||
|
||||
auto connection = new Connection(account.homeserver());
|
||||
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, [=](const QString &error, const QString &) {
|
||||
@@ -282,7 +286,7 @@ void Controller::invokeLogin()
|
||||
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()) {
|
||||
@@ -391,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
|
||||
@@ -463,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()) {
|
||||
@@ -478,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);
|
||||
@@ -561,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);
|
||||
@@ -585,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(":")) {
|
||||
@@ -594,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)
|
||||
@@ -610,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,7 +33,6 @@ 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:
|
||||
@@ -89,10 +87,6 @@ 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;
|
||||
@@ -108,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 : qAsConst(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 : qAsConst(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();
|
||||
Q_EMIT endResetModel();
|
||||
connect(job, &BaseJob::failure, this, [=]() {
|
||||
beginResetModel();
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -10,10 +10,9 @@
|
||||
#include <utility>
|
||||
|
||||
struct Emoji {
|
||||
Emoji(QString u, QString s, bool isCustom = false)
|
||||
Emoji(QString u, QString s)
|
||||
: unicode(std::move(std::move(u)))
|
||||
, shortname(std::move(std::move(s)))
|
||||
, isCustom(isCustom)
|
||||
{
|
||||
}
|
||||
Emoji() = default;
|
||||
@@ -29,18 +28,15 @@ struct Emoji {
|
||||
{
|
||||
arch >> object.unicode;
|
||||
arch >> object.shortname;
|
||||
object.isCustom = object.unicode.startsWith("image://");
|
||||
return arch;
|
||||
}
|
||||
|
||||
QString unicode;
|
||||
QString shortname;
|
||||
bool isCustom = false;
|
||||
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString unicode MEMBER unicode)
|
||||
Q_PROPERTY(QString shortname MEMBER shortname)
|
||||
Q_PROPERTY(bool isCustom MEMBER isCustom)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Emoji)
|
||||
|
||||
@@ -29,8 +29,8 @@ public:
|
||||
enum MatchMode { MatchDefault, MatchExtension, MatchContent };
|
||||
Q_ENUM(MatchMode)
|
||||
|
||||
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, FileTypeSingleton::MatchMode mode = MatchDefault) const;
|
||||
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, FileTypeSingleton::MatchMode mode = MatchDefault) const;
|
||||
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, MatchMode mode = MatchDefault) const;
|
||||
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode = MatchDefault) const;
|
||||
Q_INVOKABLE QList<QMimeType> mimeTypesForFileName(const QString &fileName) const;
|
||||
|
||||
Q_INVOKABLE QMimeType mimeTypeForData(const QByteArray &data) const;
|
||||
|
||||
125
src/login.cpp
125
src/login.cpp
@@ -18,11 +18,10 @@ Login::Login(QObject *parent)
|
||||
void Login::init()
|
||||
{
|
||||
m_homeserverReachable = false;
|
||||
m_connection = new Connection();
|
||||
m_connection = nullptr;
|
||||
m_matrixId = QString();
|
||||
m_password = QString();
|
||||
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
|
||||
.arg(QSysInfo::machineHostName(), QSysInfo::productType(), QSysInfo::productVersion(), QSysInfo::currentCpuArchitecture());
|
||||
m_deviceName = QString();
|
||||
m_supportsSso = false;
|
||||
m_supportsPassword = false;
|
||||
m_ssoUrl = QUrl();
|
||||
@@ -30,17 +29,20 @@ void Login::init()
|
||||
connect(this, &Login::matrixIdChanged, this, [=]() {
|
||||
setHomeserverReachable(false);
|
||||
|
||||
if (m_connection) {
|
||||
delete m_connection;
|
||||
m_connection = nullptr;
|
||||
}
|
||||
|
||||
if (m_matrixId == "@") {
|
||||
return;
|
||||
}
|
||||
|
||||
m_testing = true;
|
||||
Q_EMIT testingChanged();
|
||||
if (!m_connection) {
|
||||
m_connection = new Connection();
|
||||
}
|
||||
m_connection = new Connection(this);
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [=]() {
|
||||
connect(m_connection, &Connection::loginFlowsChanged, this, [=]() {
|
||||
setHomeserverReachable(true);
|
||||
m_testing = false;
|
||||
Q_EMIT testingChanged();
|
||||
@@ -49,42 +51,6 @@ void Login::init()
|
||||
Q_EMIT loginFlowsChanged();
|
||||
});
|
||||
});
|
||||
connect(m_connection, &Connection::connected, this, [=] {
|
||||
Q_EMIT connected();
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
Q_ASSERT(m_connection);
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
m_connection = nullptr;
|
||||
});
|
||||
connect(m_connection, &Connection::networkError, this, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
connect(m_connection, &Connection::loginError, this, [=](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connectSingleShot(m_connection, &Connection::syncDone, this, [=]() {
|
||||
Q_EMIT Controller::instance().initiated();
|
||||
});
|
||||
}
|
||||
|
||||
void Login::setHomeserverReachable(bool reachable)
|
||||
@@ -139,7 +105,51 @@ void Login::login()
|
||||
m_isLoggingIn = true;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
|
||||
m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString());
|
||||
setDeviceName("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " "
|
||||
+ QSysInfo::currentCpuArchitecture());
|
||||
|
||||
m_connection = new Connection(this);
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
|
||||
connect(m_connection, &Connection::loginFlowsChanged, this, [=]() {
|
||||
m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString());
|
||||
connect(m_connection, &Connection::connected, this, [=] {
|
||||
Q_EMIT connected();
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
});
|
||||
connect(m_connection, &Connection::networkError, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
connect(m_connection, &Connection::loginError, [=](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::syncDone, this, [=]() {
|
||||
Q_EMIT initialSyncFinished();
|
||||
disconnect(m_connection, &Connection::syncDone, this, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
bool Login::supportsPassword() const
|
||||
@@ -159,12 +169,29 @@ QUrl Login::ssoUrl() const
|
||||
|
||||
void Login::loginWithSso()
|
||||
{
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [=]() {
|
||||
SsoSession *session = m_connection->prepareForSso(m_deviceName);
|
||||
m_ssoUrl = session->ssoUrl();
|
||||
});
|
||||
SsoSession *session = m_connection->prepareForSso("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " "
|
||||
+ QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
|
||||
m_ssoUrl = session->ssoUrl();
|
||||
Q_EMIT ssoUrlChanged();
|
||||
connect(m_connection, &Connection::connected, [=]() {
|
||||
Q_EMIT connected();
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
});
|
||||
connect(m_connection, &Connection::syncDone, this, [=]() {
|
||||
Q_EMIT initialSyncFinished();
|
||||
disconnect(m_connection, &Connection::syncDone, this, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
bool Login::testing() const
|
||||
|
||||
@@ -58,6 +58,7 @@ Q_SIGNALS:
|
||||
void matrixIdChanged();
|
||||
void passwordChanged();
|
||||
void deviceNameChanged();
|
||||
void initialSyncFinished();
|
||||
void loginFlowsChanged();
|
||||
void ssoUrlChanged();
|
||||
void connected();
|
||||
@@ -76,6 +77,6 @@ private:
|
||||
bool m_supportsPassword = false;
|
||||
Connection *m_connection = nullptr;
|
||||
QUrl m_ssoUrl;
|
||||
bool m_testing = false;
|
||||
bool m_testing;
|
||||
bool m_isLoggingIn = false;
|
||||
};
|
||||
|
||||
88
src/main.cpp
88
src/main.cpp
@@ -2,16 +2,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QDebug>
|
||||
#include <QFontDatabase>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QNetworkProxy>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QQuickWindow>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QGuiApplication>
|
||||
@@ -22,7 +21,6 @@
|
||||
#include <KAboutData>
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
#include <KDBusService>
|
||||
#include <KWindowSystem>
|
||||
#endif
|
||||
#include <KLocalizedContext>
|
||||
#include <KLocalizedString>
|
||||
@@ -32,7 +30,6 @@
|
||||
|
||||
#include "accountlistmodel.h"
|
||||
#include "actionshandler.h"
|
||||
#include "blurhashimageprovider.h"
|
||||
#include "chatboxhelper.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "clipboard.h"
|
||||
@@ -40,7 +37,6 @@
|
||||
#include "controller.h"
|
||||
#include "csapi/joining.h"
|
||||
#include "csapi/leaving.h"
|
||||
#include "customemojimodel.h"
|
||||
#include "devicesmodel.h"
|
||||
#include "emojimodel.h"
|
||||
#include "filetypesingleton.h"
|
||||
@@ -53,32 +49,15 @@
|
||||
#include "neochatuser.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "publicroomlistmodel.h"
|
||||
#include <room.h>
|
||||
#include "roomlistmodel.h"
|
||||
#include "roommanager.h"
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
#include "spellcheckhighlighter.h"
|
||||
#include "userdirectorylistmodel.h"
|
||||
#include "userlistmodel.h"
|
||||
#include "webshortcutmodel.h"
|
||||
#include <room.h>
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
static void raiseWindow(QWindow *window)
|
||||
{
|
||||
if (KWindowSystem::isPlatformWayland()) {
|
||||
KWindowSystem::setCurrentXdgActivationToken(qEnvironmentVariable("XDG_ACTIVATION_TOKEN"));
|
||||
KWindowSystem::activateWindow(window->winId());
|
||||
} else {
|
||||
window->raise();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
Q_DECL_EXPORT
|
||||
#endif
|
||||
@@ -90,9 +69,8 @@ int main(int argc, char *argv[])
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
QGuiApplication app(argc, argv);
|
||||
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
|
||||
QQuickStyle::setStyle(QStringLiteral("Material"));
|
||||
#else
|
||||
QIcon::setFallbackThemeName("breeze");
|
||||
QApplication app(argc, argv);
|
||||
// Default to org.kde.desktop style unless the user forces another style
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
@@ -119,34 +97,28 @@ int main(int argc, char *argv[])
|
||||
QStringLiteral(NEOCHAT_VERSION_STRING),
|
||||
i18n("Matrix client"),
|
||||
KAboutLicense::GPL_V3,
|
||||
i18n("© 2018-2020 Black Hat, 2020-2021 KDE Community"));
|
||||
i18n("© 2018-2020 Black Hat, 2020 KDE Community"));
|
||||
about.addAuthor(i18n("Black Hat"), QString(), QStringLiteral("bhat@encom.eu.org"));
|
||||
about.addAuthor(i18n("Carl Schwan"), QString(), QStringLiteral("carl@carlschwan.eu"));
|
||||
about.addAuthor(i18n("Tobias Fella"), QString(), QStringLiteral("fella@posteo.de"));
|
||||
about.setOrganizationDomain("kde.org");
|
||||
|
||||
about.addComponent(QStringLiteral("libQuotient"),
|
||||
i18n("A Qt5 library to write cross-platform clients for Matrix"),
|
||||
QString(),
|
||||
QStringLiteral("https://github.com/quotient-im/libquotient"),
|
||||
KAboutLicense::LGPL_V2_1);
|
||||
|
||||
KAboutData::setApplicationData(about);
|
||||
QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
KDBusService service(KDBusService::Unique);
|
||||
service.connect(&service, &KDBusService::activateRequested, &RoomManager::instance(), [](const QStringList &arguments, const QString &workingDirectory) {
|
||||
Q_UNUSED(workingDirectory);
|
||||
if (arguments.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto args = arguments;
|
||||
args.removeFirst();
|
||||
for (const auto &arg : args) {
|
||||
RoomManager::instance().openResource(arg);
|
||||
}
|
||||
});
|
||||
service.connect(&service,
|
||||
&KDBusService::activateRequested,
|
||||
&RoomManager::instance(),
|
||||
[](const QStringList &arguments, const QString &workingDirectory) {
|
||||
Q_UNUSED(workingDirectory);
|
||||
auto args = arguments;
|
||||
args.removeFirst();
|
||||
for (const auto &arg : args) {
|
||||
RoomManager::instance().openResource(arg);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef NEOCHAT_FLATPAK
|
||||
@@ -162,39 +134,36 @@ int main(int argc, char *argv[])
|
||||
Login *login = new Login();
|
||||
ChatBoxHelper chatBoxHelper;
|
||||
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
ColorSchemer colorScheme;
|
||||
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
|
||||
if (!config->colorScheme().isEmpty()) {
|
||||
colorScheme.apply(config->colorScheme());
|
||||
}
|
||||
#endif
|
||||
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
|
||||
qmlRegisterSingletonInstance<RoomManager>("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ChatBoxHelper", &chatBoxHelper);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app));
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CommandModel", new CommandModel(&app));
|
||||
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||
qmlRegisterType<SpellcheckHighlighter>("org.kde.neochat", 1, 0, "SpellcheckHighlighter");
|
||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
||||
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
|
||||
qmlRegisterSingletonType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel", [](QQmlEngine *engine2, QJSEngine *scriptEngine) -> QObject * {
|
||||
Q_UNUSED(scriptEngine);
|
||||
Q_UNUSED(engine2);
|
||||
return new EmojiModel();
|
||||
});
|
||||
qmlRegisterSingletonType<CommandModel>("org.kde.neochat", 1, 0, "CommandModel", [](QQmlEngine *engine2, QJSEngine *scriptEngine) -> QObject * {
|
||||
Q_UNUSED(scriptEngine);
|
||||
Q_UNUSED(engine2);
|
||||
return new CommandModel();
|
||||
});
|
||||
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
|
||||
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
|
||||
qmlRegisterUncreatableType<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM");
|
||||
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
|
||||
|
||||
qRegisterMetaType<User *>("User*");
|
||||
@@ -227,7 +196,6 @@ int main(int argc, char *argv[])
|
||||
|
||||
engine.addImportPath("qrc:/imports");
|
||||
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
|
||||
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
|
||||
|
||||
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
@@ -245,7 +213,7 @@ int main(int argc, char *argv[])
|
||||
auto view = qobject_cast<QQuickWindow *>(obj);
|
||||
if (view) {
|
||||
view->show();
|
||||
raiseWindow(view);
|
||||
view->raise();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ void ThumbnailResponse::prepareResult()
|
||||
errorStr.clear();
|
||||
} else if (job->error() == BaseJob::Abandoned) {
|
||||
errorStr = i18n("Image request has been cancelled");
|
||||
// qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
|
||||
//qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
|
||||
} else {
|
||||
errorStr = job->errorString();
|
||||
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr;
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
#include <QQmlEngine> // for qmlRegisterType()
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
#include <KFormat>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
@@ -37,7 +37,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[HighlightRole] = "isHighlighted";
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
roles[LongOperationRole] = "progressInfo";
|
||||
roles[FileMimetypeIcon] = "fileMimetypeIcon";
|
||||
roles[AnnotationRole] = "annotation";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[ReplyRole] = "reply";
|
||||
@@ -57,6 +56,7 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
using namespace Quotient;
|
||||
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
|
||||
qRegisterMetaType<FileTransferInfo>();
|
||||
qmlRegisterUncreatableType<EventStatus>("org.kde.neochat", 1, 0, "EventStatus", "EventStatus is not an creatable type");
|
||||
|
||||
QTimer::singleShot(0, this, [=]() {
|
||||
if (!m_currentRoom) {
|
||||
@@ -68,7 +68,7 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
return;
|
||||
}
|
||||
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
|
||||
if (it == m_currentRoom->historyEdge()) {
|
||||
if (it == m_currentRoom->timelineEdge()) {
|
||||
m_currentRoom->getPreviousContent(50);
|
||||
}
|
||||
});
|
||||
@@ -144,7 +144,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
endInsertRows();
|
||||
if (!m_lastReadEventIndex.isValid()) {
|
||||
// no read marker, so see if we need to create one.
|
||||
moveReadMarker(m_currentRoom->readMarkerEventId());
|
||||
moveReadMarker(QString(), m_currentRoom->readMarkerEventId());
|
||||
}
|
||||
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
||||
@@ -166,7 +166,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
movingEvent = true;
|
||||
// Reverse i because row 0 is bottommost in the model
|
||||
const auto row = timelineBaseIndex() - i - 1;
|
||||
beginMoveRows({}, row, row, {}, timelineBaseIndex());
|
||||
Q_ASSERT(beginMoveRows({}, row, row, {}, timelineBaseIndex()));
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventMerged, this, [this] {
|
||||
if (movingEvent) {
|
||||
@@ -184,10 +184,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
beginRemoveRows({}, i, i);
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
|
||||
connect(m_currentRoom, &Room::readMarkerMoved, this, [=](const QString &fromEventId, const QString &toEventId) {
|
||||
Q_UNUSED(fromEventId);
|
||||
moveReadMarker(toEventId);
|
||||
});
|
||||
connect(m_currentRoom, &Room::readMarkerMoved, this, &MessageEventModel::moveReadMarker);
|
||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
||||
});
|
||||
@@ -200,9 +197,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
|
||||
#ifndef QUOTIENT_07
|
||||
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
|
||||
#endif
|
||||
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [=] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
@@ -235,10 +230,10 @@ void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles)
|
||||
Q_EMIT dataChanged(idx, idx, roles);
|
||||
}
|
||||
|
||||
void MessageEventModel::moveReadMarker(const QString &toEventId)
|
||||
void MessageEventModel::moveReadMarker(const QString &fromEventId, const QString &toEventId)
|
||||
{
|
||||
const auto timelineIt = m_currentRoom->findInTimeline(toEventId);
|
||||
if (timelineIt == m_currentRoom->historyEdge()) {
|
||||
if (timelineIt == m_currentRoom->timelineEdge()) {
|
||||
return;
|
||||
}
|
||||
int newRow = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
||||
@@ -283,7 +278,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
|
||||
row = int(pendingIt - m_currentRoom->pendingEvents().begin());
|
||||
} else {
|
||||
const auto timelineIt = m_currentRoom->findInTimeline(id);
|
||||
if (timelineIt == m_currentRoom->historyEdge()) {
|
||||
if (timelineIt == m_currentRoom->timelineEdge()) {
|
||||
qWarning() << "Trying to refresh inexistent event:" << id;
|
||||
return -1;
|
||||
}
|
||||
@@ -372,6 +367,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
|
||||
} else {
|
||||
return m_currentRoom->timelineSize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MessageEventModel::canFetchMore(const QModelIndex &parent) const
|
||||
@@ -389,6 +385,7 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
|
||||
{
|
||||
Q_UNUSED(evt)
|
||||
@@ -415,22 +412,22 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
bool isPending = row < timelineBaseIndex();
|
||||
|
||||
if (m_lastReadEventIndex.row() == row) {
|
||||
switch (role) {
|
||||
switch(role) {
|
||||
case EventTypeRole:
|
||||
return QStringLiteral("readMarker");
|
||||
case TimeRole: {
|
||||
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
|
||||
const KFormat format;
|
||||
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
||||
}
|
||||
case TimeRole:
|
||||
{
|
||||
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
|
||||
const KFormat format;
|
||||
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
||||
}
|
||||
case SpecialMarksRole:
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto timelineIt = m_currentRoom->messageEvents().crbegin()
|
||||
+ std::max(0, row - timelineBaseIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0));
|
||||
const auto timelineIt = m_currentRoom->messageEvents().crbegin() + std::max(0, row - timelineBaseIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0));
|
||||
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
||||
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
||||
|
||||
@@ -438,7 +435,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
if (evt.isRedacted()) {
|
||||
auto reason = evt.redactedBecause()->reason();
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
||||
: i18n("<i>[This message was deleted: %1]</i>").arg(evt.redactedBecause()->reason());
|
||||
}
|
||||
|
||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
||||
@@ -490,9 +487,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
if (evt.isStateEvent()) {
|
||||
return "state";
|
||||
}
|
||||
if (is<const EncryptedEvent>(evt)) {
|
||||
return "encrypted";
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
@@ -536,15 +530,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return m_currentRoom->isEventHighlighted(&evt);
|
||||
}
|
||||
|
||||
if (role == FileMimetypeIcon) {
|
||||
auto e = eventCast<const RoomMessageEvent>(&evt);
|
||||
if (!e || !e->hasFileContent()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return e->content()->fileInfo()->mimeType.iconName();
|
||||
}
|
||||
|
||||
if (role == SpecialMarksRole) {
|
||||
if (isPending) {
|
||||
return pendingIt->deliveryStatus();
|
||||
@@ -635,7 +620,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return {};
|
||||
};
|
||||
const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
|
||||
if (replyIt == m_currentRoom->historyEdge()) {
|
||||
if (replyIt == m_currentRoom->timelineEdge()) {
|
||||
return {};
|
||||
};
|
||||
const auto &replyEvt = **replyIt;
|
||||
@@ -684,11 +669,12 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
content = QVariant::fromValue(e->image().originalJson);
|
||||
}
|
||||
|
||||
return QVariantMap{{"eventId", replyEventId},
|
||||
{"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)},
|
||||
{"content", content},
|
||||
{"type", type},
|
||||
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
|
||||
return QVariantMap{
|
||||
{"eventId", replyEventId},
|
||||
{"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)},
|
||||
{"content", content},
|
||||
{"type", type},
|
||||
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
@@ -757,8 +743,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
int MessageEventModel::eventIDToIndex(const QString &eventID) const
|
||||
{
|
||||
const auto it = m_currentRoom->findInTimeline(eventID);
|
||||
if (it == m_currentRoom->historyEdge()) {
|
||||
// qWarning() << "Trying to find inexistent event:" << eventID;
|
||||
if (it == m_currentRoom->timelineEdge()) {
|
||||
//qWarning() << "Trying to find inexistent event:" << eventID;
|
||||
return -1;
|
||||
}
|
||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||
|
||||
@@ -30,8 +30,6 @@ public:
|
||||
UserMarkerRole,
|
||||
FormattedBodyRole,
|
||||
|
||||
FileMimetypeIcon,
|
||||
|
||||
ReplyRole,
|
||||
|
||||
ShowAuthorRole,
|
||||
@@ -84,7 +82,7 @@ private:
|
||||
void refreshLastUserEvents(int baseTimelineRow);
|
||||
void refreshEventRoles(int row, const QVector<int> &roles = {});
|
||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
void moveReadMarker(const QString &fromEventId, const QString &toEventId);
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
|
||||
@@ -14,17 +14,6 @@
|
||||
<entry name="ActiveConnection" type="String">
|
||||
<label>Latest active connection</label>
|
||||
</entry>
|
||||
<entry name="ColorScheme" type="String">
|
||||
<label>Color scheme</label>
|
||||
</entry>
|
||||
<entry name="Blur" type="bool">
|
||||
<label>Make NeoChat blurry</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="Transparency" type="Double">
|
||||
<label>Background transparency value</label>
|
||||
<default>0.3</default>
|
||||
</entry>
|
||||
<entry name="ShowNotifications" type="bool">
|
||||
<label>Show notifications</label>
|
||||
<default>true</default>
|
||||
@@ -41,23 +30,12 @@
|
||||
<label>Use s/text/replacement syntax to edit your last message.</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowLocalMessagesOnRight" type="bool">
|
||||
<label>"Show your messages on the right</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="RoomListPageWidth" type="int">
|
||||
<default>-1</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Timeline">
|
||||
<entry name="ShowAvatarInTimeline" type="bool">
|
||||
<label>Show avatar in the timeline</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="CompactLayout" type="bool">
|
||||
<label>Use a compact layout</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowRename" type="bool">
|
||||
<label>Show rename events in the timeline</label>
|
||||
<default>true</default>
|
||||
@@ -75,11 +53,5 @@
|
||||
<default>true</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="RoomDrawer">
|
||||
<entry name="ShowAvatarInRoomDrawer" type="bool">
|
||||
<label>Show avatar in the room drawer</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
|
||||
|
||||
@@ -12,14 +12,10 @@
|
||||
#include <QTextDocument>
|
||||
#include <functional>
|
||||
|
||||
#include <qcoro/qcorosignal.h>
|
||||
#include <qcoro/task.h>
|
||||
|
||||
#include "connection.h"
|
||||
#include "csapi/account-data.h"
|
||||
#include "csapi/content-repo.h"
|
||||
#include "csapi/leaving.h"
|
||||
#include "csapi/redaction.h"
|
||||
#include "csapi/room_state.h"
|
||||
#include "csapi/rooms.h"
|
||||
#include "csapi/typing.h"
|
||||
@@ -57,7 +53,6 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
Q_EMIT isInviteChanged();
|
||||
}
|
||||
});
|
||||
connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged);
|
||||
}
|
||||
|
||||
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||
@@ -152,23 +147,10 @@ const RoomMessageEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool NeoChatRoom::lastEventIsSpoiler() const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(event)) {
|
||||
if (e->hasTextContent() && e->content() && e->mimeType().name() == "text/html") {
|
||||
auto htmlBody = static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
|
||||
return htmlBody.contains("data-mx-spoiler");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::lastEventToString() const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
return htmlSafeMemberName(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
|
||||
return user(event->senderId())->displayname(this) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
|
||||
}
|
||||
return QLatin1String("");
|
||||
}
|
||||
@@ -239,12 +221,12 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
|
||||
int NeoChatRoom::savedTopVisibleIndex() const
|
||||
{
|
||||
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||
return firstDisplayedMarker() == timelineEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||
}
|
||||
|
||||
int NeoChatRoom::savedBottomVisibleIndex() const
|
||||
{
|
||||
return lastDisplayedMarker() == historyEdge() ? 0 : int(lastDisplayedMarker() - messageEvents().rbegin());
|
||||
return lastDisplayedMarker() == timelineEdge() ? 0 : int(lastDisplayedMarker() - messageEvents().rbegin());
|
||||
}
|
||||
|
||||
void NeoChatRoom::saveViewport(int topIndex, int bottomIndex)
|
||||
@@ -280,13 +262,15 @@ QVariantList NeoChatRoom::getUsers(const QString &keyword) const
|
||||
return matchedList;
|
||||
}
|
||||
|
||||
QVariantMap NeoChatRoom::getUser(const QString &userID) const
|
||||
QVariantMap NeoChatRoom::getUser(const QString& userID) const
|
||||
{
|
||||
NeoChatUser user(userID, connection());
|
||||
return QVariantMap{{QStringLiteral("id"), user.id()},
|
||||
{QStringLiteral("displayName"), user.displayname(this)},
|
||||
{QStringLiteral("avatarMediaId"), user.avatarMediaId(this)},
|
||||
{QStringLiteral("color"), user.color()}};
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("id"), user.id() },
|
||||
{ QStringLiteral("displayName"), user.displayname(this) },
|
||||
{ QStringLiteral("avatarMediaId"), user.avatarMediaId(this) },
|
||||
{ QStringLiteral("color"), user.color() }
|
||||
};
|
||||
}
|
||||
|
||||
QUrl NeoChatRoom::urlToMxcUrl(const QUrl &mxcUrl)
|
||||
@@ -318,7 +302,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
using namespace Quotient;
|
||||
return visit(
|
||||
evt,
|
||||
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
[prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
|
||||
// 1. prettyPrint/HTML
|
||||
@@ -330,10 +314,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
htmlBody.replace(utils::userPillRegExp, R"(<b class="user-pill">\1</b>)");
|
||||
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
|
||||
|
||||
auto url = connection()->homeserver();
|
||||
auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':' + QString::number(url.port()) : QString());
|
||||
htmlBody.replace(utils::mxcImageRegExp, QStringLiteral(R"(<img \1 src="%1/_matrix/media/r0/download/\2/\3" \4 > )").arg(base));
|
||||
|
||||
return htmlBody;
|
||||
}
|
||||
|
||||
@@ -371,7 +351,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
},
|
||||
[this](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
auto subjectName = this->user(e.userId())->displayname();
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case MembershipType::Invite:
|
||||
@@ -399,7 +379,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename() && NeoChatConfig::self()->showRename()) {
|
||||
if (e.displayName().isEmpty()) {
|
||||
if (!e.displayName().isEmpty()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.displayName().toHtmlEscaped());
|
||||
@@ -438,7 +418,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
return (e.senderId() != e.userId()) ? i18n("banned %1 from the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||
: i18n("self-banned from the room");
|
||||
case MembershipType::Knock:
|
||||
return i18n("requested an invite");
|
||||
return i18n("knocked");
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
@@ -463,10 +443,9 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1" : e.version().toHtmlEscaped());
|
||||
},
|
||||
[](const StateEventBase &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
// A small hack for state events from TWIM bot
|
||||
return e.stateKey() == "twim" ? i18n("updated the database")
|
||||
: e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
@@ -476,16 +455,12 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||
{
|
||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||
#ifdef QUOTIENT_07
|
||||
if (isJobPending(job)) {
|
||||
if(isJobPending(job)) {
|
||||
#else
|
||||
if (isJobRunning(job)) {
|
||||
#endif
|
||||
connect(job, &BaseJob::success, this, [this, job] {
|
||||
#ifdef QUOTIENT_07
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject{{"url", job->contentUri().toString()}});
|
||||
#else
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject{{"url", job->contentUri()}});
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -557,14 +532,12 @@ QString msgTypeToString(MessageEventType msgType)
|
||||
}
|
||||
}
|
||||
|
||||
QString NeoChatRoom::preprocessText(const QString &text)
|
||||
{
|
||||
return markdownToHTML(text);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
{
|
||||
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId);
|
||||
const auto html = markdownToHTML(text);
|
||||
QString cleanText(text);
|
||||
cleanText.replace(QRegularExpression("\\[(.+)\\]\\(.+\\)"), "\\1");
|
||||
postHtmlMessage(rawText, html, type, replyEventId, relateToEventId);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
@@ -573,7 +546,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
bool isReply = !replyEventId.isEmpty();
|
||||
bool isEdit = !relateToEventId.isEmpty();
|
||||
const auto replyIt = findInTimeline(replyEventId);
|
||||
if (replyIt == historyEdge()) {
|
||||
if (replyIt == timelineEdge()) {
|
||||
isReply = false;
|
||||
}
|
||||
|
||||
@@ -639,7 +612,7 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
|
||||
}
|
||||
|
||||
const auto eventIt = findInTimeline(eventId);
|
||||
if (eventIt == historyEdge()) {
|
||||
if (eventIt == timelineEdge()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -704,7 +677,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
|
||||
bool NeoChatRoom::readMarkerLoaded() const
|
||||
{
|
||||
const auto it = findInTimeline(readMarkerEventId());
|
||||
return it != historyEdge();
|
||||
return it != timelineEdge();
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isInvite() const
|
||||
@@ -716,36 +689,3 @@ bool NeoChatRoom::isUserBanned(const QString &user) const
|
||||
{
|
||||
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::htmlSafeName() const
|
||||
{
|
||||
return name().toHtmlEscaped();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::htmlSafeDisplayName() const
|
||||
{
|
||||
return displayName().toHtmlEscaped();
|
||||
}
|
||||
|
||||
void NeoChatRoom::deleteMessagesByUser(const QString &user)
|
||||
{
|
||||
doDeleteMessagesByUser(user);
|
||||
}
|
||||
|
||||
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user)
|
||||
{
|
||||
QStringList events;
|
||||
for (const auto &event : messageEvents()) {
|
||||
if (event->senderId() == user && !event->isRedacted() && !event.viewAs<RedactionEvent>() && !event->isStateEvent()) {
|
||||
events += event->id();
|
||||
}
|
||||
}
|
||||
for (const auto &e : events) {
|
||||
auto job = connection()->callApi<RedactEventJob>(id(), QUrl::toPercentEncoding(e), connection()->generateTxnId());
|
||||
co_await qCoro(job, &BaseJob::finished);
|
||||
if (job->error() != BaseJob::Success) {
|
||||
qWarning() << "Error: \"" << job->error() << "\" while deleting messages. Aborting";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
|
||||
#include <qcoro/task.h>
|
||||
|
||||
#include "neochatuser.h"
|
||||
#include "room.h"
|
||||
|
||||
@@ -33,7 +31,6 @@ class NeoChatRoom : public Room
|
||||
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
|
||||
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
|
||||
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
|
||||
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
|
||||
|
||||
public:
|
||||
explicit NeoChatRoom(Connection *connection, QString roomId, JoinState joinState = {});
|
||||
@@ -50,15 +47,8 @@ public:
|
||||
/// Convenient way to get the last event but in a string format.
|
||||
///
|
||||
/// \see lastEvent
|
||||
/// \see lastEventIsSpoiler
|
||||
[[nodiscard]] QString lastEventToString() const;
|
||||
|
||||
/// Convenient way to check if the last event looks like it has spoilers.
|
||||
///
|
||||
/// \see lastEvent
|
||||
/// \see lastEventToString
|
||||
[[nodiscard]] bool lastEventIsSpoiler() const;
|
||||
|
||||
/// Convenient way to get the QDateTime of the last event.
|
||||
///
|
||||
/// \see lastEvent
|
||||
@@ -115,16 +105,6 @@ public:
|
||||
|
||||
bool isInvite() const;
|
||||
|
||||
Q_INVOKABLE QString htmlSafeName() const;
|
||||
Q_INVOKABLE QString htmlSafeDisplayName() const;
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
|
||||
{
|
||||
return safeMemberName(userId).toHtmlEscaped();
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
QString m_cachedInput;
|
||||
QSet<const Quotient::RoomEvent *> highlights;
|
||||
@@ -139,7 +119,6 @@ private:
|
||||
void onRedaction(const RoomEvent &prevEvent, const RoomEvent &after) override;
|
||||
|
||||
static QString markdownToHTML(const QString &markdown);
|
||||
QCoro::Task<void> doDeleteMessagesByUser(const QString &user);
|
||||
|
||||
private Q_SLOTS:
|
||||
void countChanged();
|
||||
@@ -153,15 +132,12 @@ Q_SIGNALS:
|
||||
void readMarkerLoadedChanged();
|
||||
void lastActiveTimeChanged();
|
||||
void isInviteChanged();
|
||||
void displayNameChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void uploadFile(const QUrl &url, const QString &body = QString());
|
||||
void acceptInvitation();
|
||||
void forget();
|
||||
void sendTypingNotification(bool isTyping);
|
||||
QString preprocessText(const QString &text);
|
||||
|
||||
/// @param rawText The text as it was typed.
|
||||
/// @param cleanedText The text with link to the users.
|
||||
void postMessage(const QString &rawText,
|
||||
@@ -178,5 +154,4 @@ public Q_SLOTS:
|
||||
void addLocalAlias(const QString &alias);
|
||||
void removeLocalAlias(const QString &alias);
|
||||
void toggleReaction(const QString &eventId, const QString &reaction);
|
||||
void deleteMessagesByUser(const QString &user);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,24 @@
|
||||
|
||||
#include "neochatuser.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QPalette>
|
||||
#include <PlatformTheme> // Kirigami
|
||||
#include <utility>
|
||||
|
||||
#include "csapi/profile.h"
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
static Kirigami::PlatformTheme *s_theme = nullptr;
|
||||
|
||||
NeoChatUser::NeoChatUser(QString userId, Connection *connection)
|
||||
: User(std::move(userId), connection)
|
||||
{
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, &NeoChatUser::polishColor);
|
||||
if (!s_theme) {
|
||||
s_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(&Controller::instance(), true));
|
||||
Q_ASSERT(s_theme);
|
||||
}
|
||||
|
||||
connect(s_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
|
||||
polishColor();
|
||||
}
|
||||
|
||||
@@ -30,7 +41,9 @@ void NeoChatUser::setColor(const QColor &color)
|
||||
|
||||
void NeoChatUser::polishColor()
|
||||
{
|
||||
const auto lightness = static_cast<QGuiApplication *>(QGuiApplication::instance())->palette().color(QPalette::Active, QPalette::Window).lightnessF();
|
||||
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
|
||||
setColor(QColor::fromHslF(hueF(), 1, -0.7 * lightness + 0.9, 1));
|
||||
setColor(QColor::fromHslF(hueF(),
|
||||
1 - s_theme->alternateBackgroundColor().saturationF(),
|
||||
-0.7 * s_theme->alternateBackgroundColor().lightnessF() + 0.9,
|
||||
s_theme->textColor().alphaF()));
|
||||
}
|
||||
|
||||
@@ -5,8 +5,14 @@
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "room.h"
|
||||
#include "user.h"
|
||||
|
||||
namespace Kirigami
|
||||
{
|
||||
class PlatformTheme;
|
||||
}
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NeoChatUser : public User
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
#endif
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "roommanager.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
NotificationsManager &NotificationsManager::instance()
|
||||
{
|
||||
@@ -55,17 +55,19 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
notification->setPixmap(img);
|
||||
|
||||
notification->setDefaultAction(i18n("Open NeoChat in this room"));
|
||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||
RoomManager::instance().enterRoom(room);
|
||||
connect(notification, &KNotification::defaultActivated, this, [this, room]() {
|
||||
Q_EMIT RoomManager::instance().enterRoom(room);
|
||||
Q_EMIT Controller::instance().showWindow();
|
||||
});
|
||||
|
||||
#if KNOTIFICATIONS_VERSION >= QT_VERSION_CHECK(5, 81, 0)
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, room->preprocessText(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
QObject::connect(replyAction.get(), &KNotificationReplyAction::replied, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, text, RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
});
|
||||
notification->setReplyAction(std::move(replyAction));
|
||||
#endif
|
||||
|
||||
notification->sendEvent();
|
||||
|
||||
|
||||
@@ -173,11 +173,8 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (avatarUrl.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
return avatarUrl.url().remove(0, 6);
|
||||
#else
|
||||
|
||||
return avatarUrl.remove(0, 6);
|
||||
#endif
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room.topic;
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
#include "notificationsmanager.h"
|
||||
#include "roommanager.h"
|
||||
|
||||
Q_DECLARE_METATYPE(Quotient::JoinState)
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool useUnityCounter()
|
||||
{
|
||||
@@ -291,6 +289,7 @@ void RoomListModel::updateRoom(Room *room, Room *prev)
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_rooms.count(), m_rooms.count());
|
||||
doAddRoom(newRoom);
|
||||
RoomManager::instance().enterRoom(qobject_cast<NeoChatRoom *>(newRoom));
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
@@ -329,7 +328,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
NeoChatRoom *room = m_rooms.at(index.row());
|
||||
if (role == NameRole) {
|
||||
return !room->name().isEmpty() ? room->htmlSafeName() : room->htmlSafeDisplayName();
|
||||
return !room->name().isEmpty() ? room->name() : room->displayName();
|
||||
}
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
@@ -342,26 +341,18 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
if (room->joinState() == JoinState::Invite) {
|
||||
return NeoChatRoomType::Invited;
|
||||
return RoomType::Invited;
|
||||
}
|
||||
if (room->isFavourite()) {
|
||||
return NeoChatRoomType::Favorite;
|
||||
return RoomType::Favorite;
|
||||
}
|
||||
if (room->isLowPriority()) {
|
||||
return NeoChatRoomType::Deprioritized;
|
||||
return RoomType::Deprioritized;
|
||||
}
|
||||
if (room->isDirectChat()) {
|
||||
return NeoChatRoomType::Direct;
|
||||
return RoomType::Direct;
|
||||
}
|
||||
const RoomCreateEvent *creationEvent = room->creation();
|
||||
QJsonObject contentJson = creationEvent->contentJson();
|
||||
QJsonObject::const_iterator typeIter = contentJson.find("type");
|
||||
if (typeIter != contentJson.end()) {
|
||||
if (typeIter.value().toString() == "m.space") {
|
||||
return NeoChatRoomType::Space;
|
||||
}
|
||||
}
|
||||
return NeoChatRoomType::Normal;
|
||||
return RoomType::Normal;
|
||||
}
|
||||
if (role == UnreadCountRole) {
|
||||
return room->unreadCount();
|
||||
@@ -373,9 +364,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return room->highlightCount();
|
||||
}
|
||||
if (role == LastEventRole) {
|
||||
if (room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
}
|
||||
return room->lastEventToString();
|
||||
}
|
||||
if (role == LastActiveTimeRole) {
|
||||
@@ -385,7 +373,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (!room->successorId().isEmpty()) {
|
||||
return QStringLiteral("upgraded");
|
||||
}
|
||||
return QVariant::fromValue(room->joinState());
|
||||
return toCString(room->joinState());
|
||||
}
|
||||
if (role == CurrentRoomRole) {
|
||||
return QVariant::fromValue(room);
|
||||
@@ -439,33 +427,11 @@ QString RoomListModel::categoryName(int section)
|
||||
return i18n("Normal");
|
||||
case 5:
|
||||
return i18n("Low priority");
|
||||
case 6:
|
||||
return i18n("Spaces");
|
||||
default:
|
||||
return "Deadbeef";
|
||||
}
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryIconName(int section)
|
||||
{
|
||||
switch (section) {
|
||||
case 1:
|
||||
return QStringLiteral("user-invisible");
|
||||
case 2:
|
||||
return QStringLiteral("favorite");
|
||||
case 3:
|
||||
return QStringLiteral("dialog-messages");
|
||||
case 4:
|
||||
return QStringLiteral("group");
|
||||
case 5:
|
||||
return QStringLiteral("object-order-lower");
|
||||
case 6:
|
||||
return QStringLiteral("group");
|
||||
default:
|
||||
return QStringLiteral("tools-report-bug");
|
||||
}
|
||||
}
|
||||
|
||||
void RoomListModel::setCategoryVisible(int category, bool visible)
|
||||
{
|
||||
beginResetModel();
|
||||
@@ -489,15 +455,10 @@ bool RoomListModel::categoryVisible(int category) const
|
||||
|
||||
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
|
||||
{
|
||||
for (const auto &room : qAsConst(m_rooms)) {
|
||||
for (const auto &room : m_rooms) {
|
||||
if (room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int RoomListModel::indexForRoom(NeoChatRoom *room) const
|
||||
{
|
||||
return m_rooms.indexOf(room);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user