Compare commits

..

83 Commits

Author SHA1 Message Date
l10n daemon script
f5837a208a SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-03-05 02:49:38 +00:00
l10n daemon script
3b45c0b2f7 GIT_SILENT made messages (after extraction) 2021-12-29 02:19:23 +00:00
l10n daemon script
e1a72816d0 GIT_SILENT made messages (after extraction) 2021-10-25 01:44:33 +00:00
l10n daemon script
aea46c547f GIT_SILENT made messages (after extraction) 2021-10-23 01:40:12 +00:00
l10n daemon script
fe009f764e GIT_SILENT made messages (after extraction) 2021-08-31 01:57:32 +00:00
l10n daemon script
8c14f8d2eb SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-07-28 02:18:15 +00:00
l10n daemon script
559da66197 GIT_SILENT made messages (after extraction) 2021-07-28 01:43:57 +00:00
l10n daemon script
759496620f SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-07-26 02:17:04 +00:00
l10n daemon script
1f7b1ad29f SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-07-20 02:18:30 +00:00
l10n daemon script
9338567137 GIT_SILENT made messages (after extraction) 2021-07-20 01:42:10 +00:00
l10n daemon script
75d2488dbc GIT_SILENT made messages (after extraction) 2021-07-05 01:42:44 +00:00
l10n daemon script
4bed6a07cd GIT_SILENT made messages (after extraction) 2021-06-28 01:56:28 +00:00
l10n daemon script
cb1c9d1869 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-16 02:24:26 +00:00
l10n daemon script
76b2d4ac49 GIT_SILENT made messages (after extraction) 2021-06-16 01:50:18 +00:00
l10n daemon script
5fc931484e SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-14 02:13:57 +00:00
l10n daemon script
e60ed3b889 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-13 02:20:41 +00:00
l10n daemon script
1314567fde GIT_SILENT made messages (after extraction) 2021-06-13 01:46:53 +00:00
l10n daemon script
047aa793e7 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-07 02:15:39 +00:00
l10n daemon script
d60f8f4dfa GIT_SILENT made messages (after extraction) 2021-06-07 01:41:55 +00:00
l10n daemon script
20139c08d8 GIT_SILENT made messages (after extraction) 2021-06-06 01:44:24 +00:00
l10n daemon script
fb59bac165 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-04 02:26:16 +00:00
l10n daemon script
5a65dc4d9a GIT_SILENT made messages (after extraction) 2021-06-04 01:50:31 +00:00
l10n daemon script
03ec17366e SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-03 02:27:05 +00:00
l10n daemon script
15137cd704 GIT_SILENT made messages (after extraction) 2021-06-03 01:51:44 +00:00
l10n daemon script
2aea0015ba GIT_SILENT made messages (after extraction) 2021-06-02 01:41:00 +00:00
l10n daemon script
f6fd099808 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-06-01 02:13:55 +00:00
l10n daemon script
04082fc095 GIT_SILENT made messages (after extraction) 2021-06-01 01:40:17 +00:00
Carl Schwan
bc977c3fc6 Update apstream screenshots 2021-05-31 20:26:07 +02:00
Carl Schwan
c4c283c85a Update versioning to 1.2.0 2021-05-31 17:25:35 +02:00
Carl Schwan
56f49fabf7 Don't mark message as read when the current window is not visible
Fix #378

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

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


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

Otherwise this will breaks when replacing names later

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

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


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

Fix #377

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

Fix #324

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

Fix #372

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

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

fixes #367 by eliding instead of wrapping text


(cherry picked from commit bbcf4239a4)
2021-05-27 14:18:05 +00:00
l10n daemon script
8913aa8a66 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-05-26 02:28:13 +00:00
l10n daemon script
5db3e14ae6 GIT_SILENT made messages (after extraction) 2021-05-26 01:52:33 +00:00
l10n daemon script
c5a3fc0431 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-05-25 02:21:15 +00:00
l10n daemon script
fc791d41fa GIT_SILENT made messages (after extraction) 2021-05-25 01:47:45 +00:00
Carl Schwan
127ad19109 Fix minor bugs
(cherry picked from commit d14674c2cd)
2021-05-24 14:55:28 +00:00
Carl Schwan
066ea4f8bd Make sure message are loaded when scrolling to the top
(cherry picked from commit 49c1736f7c)
2021-05-24 14:53:11 +00:00
Carl Schwan
cf60337b27 Make effects more visible
(cherry picked from commit db62f06de4)
2021-05-24 14:52:55 +00:00
l10n daemon script
98672cf870 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-05-24 02:13:42 +00:00
l10n daemon script
abd03299ec GIT_SILENT made messages (after extraction) 2021-05-24 01:39:11 +00:00
Carl Schwan
41b64f977c Fix creating broken direct chat for user with a direct chat already open
Just enter the existing room instead of trying to create a new one but
broken.

Fix !237


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


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

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


(cherry picked from commit dded804f00)
2021-05-23 16:30:55 +00:00
l10n daemon script
530b4c24a0 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-05-23 02:12:00 +00:00
l10n daemon script
ada7bcef65 GIT_SILENT made messages (after extraction) 2021-05-23 01:38:51 +00:00
Carl Schwan
ad4ca3ad9e Fix i18n
(cherry picked from commit dbb43addc8)
2021-05-22 16:24:24 +00:00
Tobias Fella
4103c44eb5 Don't hide redacted events
(cherry picked from commit 135b2e49fa)
2021-05-22 12:18:07 +00:00
Tobias Fella
dd4ed7539e Revert "Fix showing multiple deleted messages from the same author"
This reverts commit b48c9bdadc.


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


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


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

Fixes a slight inconsistency with the implementation in Element

Fixes #357


(cherry picked from commit 3ea783b370)
2021-05-21 19:12:00 +00:00
114 changed files with 1037 additions and 5144 deletions

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@ build
neochat.kdev4
compile_commands.json
.cache/
.vscode/

View File

@@ -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

View File

@@ -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})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,3 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
SPDX-License-Identifier: CC0-1.0
-->
# NeoChat
NeoChat is a client for Matrix, the decentralized communication protocol for instant
@@ -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).

View File

@@ -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();

View File

@@ -61,10 +61,6 @@ Item {
QQC2.Pane {
id: connectionPane
padding: fontMetrics.lineSpacing * 0.25
FontMetrics {
id: fontMetrics
font: networkLabel.font
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
@@ -73,7 +69,6 @@ Item {
visible: !Controller.isOnline
width: parent.width
QQC2.Label {
id: networkLabel
text: i18n("NeoChat is offline. Please check your network connection.")
}
anchors.bottom: emojiPickerLoaderSeparator.top

View File

@@ -10,14 +10,12 @@ import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Popup {
id: control
// Expose internal ListView properties.
property alias model: completionListView.model
property alias listView: completionListView
property alias currentIndex: completionListView.currentIndex
property alias currentItem: completionListView.currentItem
property alias count: completionListView.count
@@ -44,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();
}
}

View File

@@ -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()
}
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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 {

View File

@@ -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()};
}
}
}

View File

@@ -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

View File

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

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

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

View File

@@ -1,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
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import 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;

View File

@@ -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

View File

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

View File

@@ -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
}
}
}

View File

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

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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 {

View File

@@ -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"

View File

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

View File

@@ -1,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/")
}
}
}

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -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"
}
}
}

View File

@@ -1,4 +1,2 @@
module NeoChat.Menu
RoomListContextMenu 1.0 RoomListContextMenu.qml
GlobalMenu 1.0 GlobalMenu.qml
EditMenu 1.0 EditMenu.qml

View File

@@ -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 {

View File

@@ -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()
}
}
]
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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
}
}
}

View File

@@ -30,7 +30,7 @@ Kirigami.ScrollablePage {
Connections {
target: LoginHelper
function onErrorOccured(message) {
onErrorOccured: {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;

View File

@@ -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) : ""

View File

@@ -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()
}
}
}
}
}

View File

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

View File

@@ -1,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
}
}
}

View File

@@ -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()
}
}
}
}
}

View File

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

View File

@@ -1,2 +0,0 @@
module NeoChat.Settings
ThemeRadioButton 1.0 ThemeRadioButton.qml

View File

@@ -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 lynmeldings­protokollen.
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]=ਨਵਾਂ ਸੁਨੇਹਾ ਹੈ

View File

@@ -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>

View File

@@ -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]=Lynmeldings­klient for Matrix-protokollen
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ

View File

@@ -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
View File

@@ -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>

View File

@@ -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})

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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);
}

View File

@@ -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/)
///

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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{

View File

@@ -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:

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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();
});
}

View File

@@ -32,6 +32,5 @@ public:
Q_INVOKABLE void setName(int index, const QString &name);
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
};

View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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);
};

View File

@@ -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()));
}

View File

@@ -5,8 +5,14 @@
#include <QObject>
#include "room.h"
#include "user.h"
namespace Kirigami
{
class PlatformTheme;
}
using namespace Quotient;
class NeoChatUser : public User

View File

@@ -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();

View File

@@ -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;

View File

@@ -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