Compare commits

..

65 Commits
1.1 ... 1.0

Author SHA1 Message Date
Tobias Fella
143bd456de Actually save the settings
(cherry picked from commit 75d3b346ac)
2021-01-23 16:40:05 +00:00
Tobias Fella
513de82515 Fix showing user's displayName instead of mxid in roomlist delegate subtitles
(cherry picked from commit 6f7f0e025d)
2021-01-18 21:28:40 +00:00
l10n daemon script
581fd5f212 GIT_SILENT made messages (after extraction) 2021-01-18 09:21:05 +01:00
Tobias Fella
2452f630d0 Load serverAddress using QUrl::fromUserInput()
Fixes login when 'https://' is not added to the server url


(cherry picked from commit a653be8be8)
2021-01-17 00:32:39 +00:00
Carl Schwan
2cdc37c3d5 Don't load events if not needed
(cherry picked from commit 7762f5f5ae)
2021-01-14 21:11:32 +00:00
Carl Schwan
5af99a872c Make sure we load events when opening a room
(cherry picked from commit 1abc28ad7f)
2021-01-14 20:54:41 +00:00
l10n daemon script
5fe7ba478b 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-01-14 10:25:39 +01:00
Carl Schwan
f25bc6bac6 Fix appstream file 2021-01-12 22:49:21 +01:00
Carl Schwan
46ee015775 Update appdata 2021-01-12 22:44:54 +01:00
Carl Schwan
f93be04b2b Update version 2021-01-12 22:29:24 +01:00
Carl Schwan
f2d9ca13d8 fix path 2021-01-12 22:27:55 +01:00
Carl Schwan
eb04f4adf1 Update screenshots 2021-01-12 22:26:50 +01:00
Carl Schwan
6b983332af Fix edited message appearing two times in the timeline 2021-01-12 21:55:23 +01:00
Carl Schwan
4e197c3cc8 Fix autocompletion
Now it will save a map from display name to id and use that to generate
clean matrix.to links. This also make sure the colors used for the
preview are correct by using NeoChatUser and fix the bug with the regex
by simply removing the regex.
2021-01-12 21:47:57 +01:00
Carl Schwan
bfd6d2ffe2 Fix the white bar in the room page's header 2021-01-12 17:48:35 +01:00
Carl Schwan
421422edd0 Fix avatar loading in multiple places and prefers name instead of
display name for avatar fallback.

This also fixes a bug where users didn't get their avatar loaded in the
room list.

Fix #209
2021-01-12 17:46:15 +01:00
Carl Schwan
0eda9608ec Fix PgUp/PgDn keys in message view switch rooms
Now use Ctr+PgUp/PgDn keys instead

Fix #213
2021-01-12 17:42:48 +01:00
Carl Schwan
06fd8630d9 Fix NeoChat not syncing
This problem was caused because addConnection was starting the sync
proccess unfortunally because the user wasn't connected this aborted
almost immediately and then the sync proccess wouldn't run at all.

Now start the sync proccess after making sure we are connected.

Fix #228
2021-01-12 17:41:10 +01:00
Carl Schwan
e7d8c4b69c Handle non-consistent configuration 2021-01-12 17:40:58 +01:00
Carl Schwan
44d4269978 Fix initial loading of room 2021-01-12 17:38:55 +01:00
Adriaan de Groot
759244b5d2 CMake: systematically use the feature-summary
There's not much point in having a feature summary that will
trip over just-a-few of the required packages, while also
using REQUIRED in find_package() calls -- then you have to
re-run CMake for all the REQUIRED ones you're missing,
and then one more time for the packages that are required
in the feature summary.

Use the feature summary (e.g. TYPE REQUIRED) consistently.
Then you can run CMake once and learn about all the missing
dependencies in one go.
2021-01-12 17:38:27 +01:00
Tobias Fella
92a307747f Fix active connection not loading on startup 2021-01-12 17:37:58 +01:00
Carl Schwan
627929203f Disable menu item when login in
Fix #204
2021-01-12 17:32:17 +01:00
Tobias Fella
0c449ab4bd Ask for consent to terms and conditions if required 2021-01-12 17:30:26 +01:00
Tobias Fella
45c35b3cbe Fix accountCount not updating correctly 2021-01-12 17:29:02 +01:00
l10n daemon script
ecd6a63564 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-01-12 10:47:53 +01:00
l10n daemon script
b75bf3a75b GIT_SILENT made messages (after extraction) 2021-01-12 08:53:10 +01:00
l10n daemon script
43288db000 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-01-02 10:22:39 +01:00
l10n daemon script
0d6c793a5e GIT_SILENT made messages (after extraction) 2021-01-02 08:58:52 +01:00
l10n daemon script
6daf184a60 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-01-01 09:39:20 +01:00
l10n daemon script
93173e3f43 GIT_SILENT made messages (after extraction) 2021-01-01 08:24:14 +01:00
l10n daemon script
ec4aa320c1 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"
2020-12-29 09:36:04 +01:00
l10n daemon script
079a1c8a34 GIT_SILENT made messages (after extraction) 2020-12-29 08:20:25 +01:00
Carl Schwan
e9bb0972a9 Fix Platform is undefined bug 2020-12-29 01:42:19 +01:00
Nicolas Fella
1717790096 Don't call stopSync when destroying controller
Connection does that internally already


(cherry picked from commit 6a1fd3ff31)
2020-12-28 17:31:24 +00:00
Tobias Fella
893bc79f1e Don't load empty images from imageprovider
Previously, when there was no avatar set, the source property of Avatar was still set to 'image://mxc/',
which caused Avatar to load that from the imageprovider. The imageprovider can't provide an empty image and aborts with error


(cherry picked from commit 724f10a895)
2020-12-28 17:30:16 +00:00
Antonio Rojas
e87ae48f17 Add missing cmake check for kitemmodels
Otherwise packagers have no way to know that it is a runtime dependency


(cherry picked from commit 93e0a2b2f6)
2020-12-28 17:28:37 +00:00
Tobias Fella
97bbcf3062 Fix segfault/assert when logging out of account
(cherry picked from commit 0fe0f45944)
2020-12-28 00:07:54 +00:00
Eamonn Rea
9f9498541a Fix cursorShape not updating for messages
(cherry picked from commit 066ab1e6c6)
2020-12-28 00:58:07 +01:00
Tobias Fella
50a4d0a33f Fix login for homeservers without well-known
(cherry picked from commit 3858956e82)
2020-12-27 22:38:17 +00:00
Carl Schwan
85b4d7d049 Don't translate something we shouldn't
(cherry picked from commit dce3b796c2)
2020-12-26 15:59:24 +00:00
l10n daemon script
b574849df3 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"
2020-12-26 09:29:49 +01:00
l10n daemon script
e802d7f805 GIT_SILENT made messages (after extraction) 2020-12-26 08:15:49 +01:00
Tobias Fella
99438011ca Fix image saving
(cherry picked from commit 8aec6b67cb)
2020-12-24 12:35:54 +00:00
Nicolas Fella
99ccfaf93e Default to org.kde.desktop QQC2 style
plasma-integration does that for us, but that obviously doesn't work for non-Plasma desktops.
2020-12-24 00:20:10 +01:00
Carl Schwan
c56973763c Dismiss reply when clicking on Esc
Backport of 59f9c36854
2020-12-23 18:02:37 +01:00
Devin Lin
c96109e9b7 Fix room header text alignment and add support for two line room descriptions
(cherry picked from commit 93f35faf95)
2020-12-23 09:06:33 +00:00
Devin Lin
3f01b3badf Show feedback on avatar hover
(cherry picked from commit 87a7a34d80)
2020-12-23 08:51:29 +00:00
Tobias Fella
093412c788 Revert "Add symbolic icon"
This reverts commit 89bf5d3a31
2020-12-22 23:43:35 +00:00
Carl Schwan
c5ddb61981 Use correct version 2020-12-22 23:12:46 +01:00
Nicolas Fella
ad4e52b20d Fix icon in notifyrc
(cherry picked from commit ef8c21213a)
2020-12-22 22:02:23 +00:00
Carl Schwan
5991d59ddd Fix not eliding text in USerDetailDialog
Fix: #169
2020-12-22 16:23:49 +01:00
Carl Schwan
de49a26462 Switch back to plain text editing
See https://bugreports.qt.io/browse/QTBUG-89630


(cherry picked from commit 6482f08eba)
2020-12-21 09:25:07 +00:00
Carl Schwan
4924702c15 Use TextArea instead of simple field for room topic
(cherry picked from commit f61eff2937)
2020-12-20 19:27:26 +00:00
Tobias Fella
8060edd1c6 Allow opening links in the MessageDelegateContextMenu
Fixes #167


(cherry picked from commit 449adf993c)
2020-12-20 18:17:46 +00:00
Jan Blackquill
89bf5d3a31 Add symbolic icon
(cherry picked from commit 9189a8ca30)
2020-12-20 09:19:53 +00:00
Carl Schwan
60762b934c Fix current page not getting updated after switching a page
This was caused by myself not updating the index after updating the
content.
2020-12-19 23:03:01 +01:00
Carl Schwan
7729fec259 Add special font configuration for flatpak
(cherry picked from commit 6e659c853b)
2020-12-19 10:49:32 +00:00
Carl Schwan
026769b07f Make kquickimageeditor a required dependency 2020-12-17 13:19:03 +01:00
Carl Schwan
5be14a4b8f Last icon fix 2020-12-17 10:37:22 +01:00
Carl Schwan
0b70d2b33f fix icon 2020-12-17 10:37:22 +01:00
Carl Schwan
dab77b8d07 Rename icon and set icon name explicitely
Fix #140
2020-12-17 10:37:22 +01:00
Carl Schwan
2acdf61b16 Don't recreate RoomPage each time and add a small loading indicator
(cherry picked from commit bd41dcc986)
2020-12-17 08:59:36 +00:00
Carl Schwan
defa3d4b77 Improve autocompletion
(cherry picked from commit 2b84c5dd02)
2020-12-17 08:58:30 +00:00
Mathew Broady
be709a2732 Remove forgotten NeoChat.Effect imports
Fixes the "Start Chat" and "Explore Rooms" pages


(cherry picked from commit 79dab63993)
2020-12-17 06:24:29 +00:00
93 changed files with 2330 additions and 4156 deletions

2
.gitignore vendored
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.1)
project(Neochat) project(Neochat)
set(KF5_MIN_VERSION "5.77.0") set(KF5_MIN_VERSION "5.76.0")
set(QT_MIN_VERSION "5.15.0") set(QT_MIN_VERSION "5.15.0")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -19,16 +19,13 @@ include(ECMQMLModules)
include(KDEClangFormat) include(KDEClangFormat)
include(KDECMakeSettings) include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
if(NEOCHAT_FLATPAK) include(cmake/Flatpak.cmake)
include(cmake/Flatpak.cmake)
endif()
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in # 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) cmake_policy(SET CMP0063 OLD)
ecm_setup_version(1.1.1 ecm_setup_version(1.0.1
VARIABLE_PREFIX NEOCHAT VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
) )
@@ -60,10 +57,11 @@ else()
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Secure storage of account secrets" PURPOSE "Secure storage of account secrets"
) )
endif() find_package(KF5DBusAddons ${KF5_MIN_VERSION})
set_package_properties(KF5DBusAddons PROPERTIES
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE) TYPE REQUIRED
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED) PURPOSE "DBus convenience functions"
)
endif() endif()
find_package(Quotient 0.6) find_package(Quotient 0.6)
@@ -96,12 +94,9 @@ set_package_properties(KQuickImageEditor PROPERTIES
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR}) 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.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps) install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@2/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@3/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/symbolic/apps)
install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
# add_definitions(-DQT_NO_KEYWORDS) Need to fix libQuotient first
add_definitions(-DQT_NO_FOREACH) add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src) add_subdirectory(src)

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 Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the license or (at your option) 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 6 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

@@ -6,10 +6,7 @@ KConfig and KI18n.
## Get it ## Get it
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions. There is no stable release for now, but a Flatpak version is available for the nightly
Along with the stable release, a Flatpak version is available for the nightly
version: version:
``` ```
@@ -18,11 +15,9 @@ flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kde
flatpak install kdeapps org.kde.neochat 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) A nigthly 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_android/). and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/Neochat_android/).
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
![Timeline](https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png) ![Timeline](https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png)
## Features ## Features

View File

@@ -1,3 +1,8 @@
if(NOT NEOCHAT_FLATPAK)
# Only include this if we build a Flatpak
return()
endif()
include(GNUInstallDirs) include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the # Include FontConfig config which uses the Emoji One font from the

View File

@@ -4,19 +4,19 @@
<alias> <alias>
<family>serif</family> <family>serif</family>
<prefer> <prefer>
<family>Noto Color Emoji</family> <family>Emoji One</family>
</prefer> </prefer>
</alias> </alias>
<alias> <alias>
<family>sans-serif</family> <family>sans-serif</family>
<prefer> <prefer>
<family>Noto Color Emoji</family> <family>Emoji One</family>
</prefer> </prefer>
</alias> </alias>
<alias> <alias>
<family>monospace</family> <family>monospace</family>
<prefer> <prefer>
<family>Noto Color Emoji</family> <family>Emoji One</family>
</prefer> </prefer>
</alias> </alias>
</fontconfig> </fontconfig>

View File

@@ -22,10 +22,8 @@ ToolBar {
property alias isReply: replyItem.visible property alias isReply: replyItem.visible
property bool isReaction: false property bool isReaction: false
property var replyUser property var replyUser
property string replyEventID: "" property string replyEventID
property string replyContent: "" property string replyContent
property string editEventId
property alias isAutoCompleting: autoCompleteListView.visible property alias isAutoCompleting: autoCompleteListView.visible
property var autoCompleteModel property var autoCompleteModel
@@ -33,10 +31,7 @@ ToolBar {
property int autoCompleteEndPosition property int autoCompleteEndPosition
property bool hasAttachment: false property bool hasAttachment: false
property url attachmentPath: "" property url attachmentPath
property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
property bool hasImageAttachment: hasAttachment && attachmentMimetype.valid
&& FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
position: ToolBar.Footer position: ToolBar.Footer
@@ -197,12 +192,13 @@ ToolBar {
Image { Image {
Layout.preferredHeight: Kirigami.Units.gridUnit * 10 Layout.preferredHeight: Kirigami.Units.gridUnit * 10
source: attachmentPath source: attachmentPath
visible: hasImageAttachment visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
Layout.preferredWidth: paintedWidth Layout.preferredWidth: paintedWidth
RowLayout { RowLayout {
anchors.right: parent.right anchors.right: parent.right
Button { Button {
visible: isImage
icon.name: "document-edit" icon.name: "document-edit"
// HACK: Use a component because an url doesn't work // HACK: Use a component because an url doesn't work
@@ -237,7 +233,7 @@ ToolBar {
} }
} }
Rectangle { Rectangle {
color: Qt.rgba(255, 255, 255, 40) color: rgba(255, 255, 255, 40)
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -252,7 +248,7 @@ ToolBar {
} }
RowLayout { RowLayout {
visible: hasAttachment && !hasImageAttachment visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
ToolButton { ToolButton {
icon.name: "dialog-cancel" icon.name: "dialog-cancel"
onClicked: { onClicked: {
@@ -261,33 +257,12 @@ ToolBar {
} }
} }
Kirigami.Icon {
id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(horizontalFileLabel.implicitHeight)
implicitWidth: implicitHeight
source: attachmentMimetype.iconName
}
Label { Label {
id: horizontalFileLabel
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
} }
} }
RowLayout {
visible: editEventId.length > 0
ToolButton {
icon.name: "dialog-cancel"
onClicked: clearEditReply();
}
Label {
Layout.alignment: Qt.AlignVCenter
text: i18n("Edit Message")
}
}
Kirigami.Separator { Kirigami.Separator {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
@@ -306,191 +281,187 @@ ToolBar {
icon.name: "dialog-cancel" icon.name: "dialog-cancel"
onClicked: clearEditReply() onClicked: clearReply()
} }
ScrollView { TextArea {
id: inputField
property real progress: 0
property bool autoAppeared: false
ChatDocumentHandler {
id: documentHandler
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
room: currentRoom ?? null
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumHeight: inputField.lineHeight * 8
TextArea {
id: inputField
property real progress: 0
property bool autoAppeared: false
// store each user we autoComplete here, this will be helpful later to generate wrapMode: Text.Wrap
// the matrix.to links. placeholderText: i18n("Write your message...")
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics topPadding: 0
property var userAutocompleted: ({}) bottomPadding: 0
leftPadding: Kirigami.Units.smallSpacing
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
ChatDocumentHandler { text: currentRoom != null ? currentRoom.cachedInput : ""
id: documentHandler
document: inputField.textDocument background: Item {}
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart Rectangle {
selectionEnd: inputField.selectionEnd width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
room: currentRoom ?? null height: parent.height
opacity: 0.2
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
} }
}
property int lineHeight: contentHeight / lineCount Timer {
id: repeatTimer
wrapMode: Text.Wrap repeat: true
placeholderText: i18n("Write your message...") interval: 5000
topPadding: 0 triggeredOnStart: true
bottomPadding: 0 onTriggered: currentRoom.sendTypingNotification(true)
leftPadding: Kirigami.Units.smallSpacing }
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
text: currentRoom != null ? currentRoom.cachedInput : ""
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
Rectangle {
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
height: parent.height
opacity: 0.2
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
}
}
Timer {
id: repeatTimer
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
Keys.onReturnPressed: {
if (isAutoCompleting) {
inputField.autoComplete();
isAutoCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n")
} else {
postMessage()
text = ""
clearEditReply()
closeAll()
}
}
Keys.onEscapePressed: {
clearEditReply();
closeAll();
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
root.pasteImage();
}
}
Keys.onBacktabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomUp();
return;
}
if (isAutoCompleting) {
autoCompleteListView.decrementCurrentIndex();
}
}
Keys.onTabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomDown();
return;
}
if (!isAutoCompleting) {
return;
}
// TODO detect moved cursor
// ignore first time tab was clicked so that user can select
// first emoji/user
if (autoAppeared === false) {
autoCompleteListView.incrementCurrentIndex()
} else {
autoAppeared = false;
}
Keys.onReturnPressed: {
if (isAutoCompleting) {
inputField.autoComplete(); inputField.autoComplete();
isAutoCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n")
} else {
postMessage()
text = ""
clearReply()
closeAll()
}
}
Keys.onEscapePressed: {
clearReply();
closeAll();
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
root.pasteImage();
}
}
Keys.onBacktabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomUp();
return;
}
if (isAutoCompleting) {
autoCompleteListView.decrementCurrentIndex();
}
}
Keys.onTabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomDown();
return;
}
if (!isAutoCompleting) {
return;
} }
onTextChanged: { // TODO detect moved cursor
timeoutTimer.restart()
repeatTimer.start() // ignore first time tab was clicked so that user can select
currentRoom.cachedInput = text // first emoji/user
if (autoAppeared === false) {
autoCompleteListView.incrementCurrentIndex()
} else {
autoAppeared = false; autoAppeared = false;
const autocompletionInfo = documentHandler.getAutocompletionInfo();
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.None) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.User) {
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
} else {
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
}
if (autoCompleteModel.length === 0) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
isAutoCompleting = true
autoAppeared = true;
autoCompleteEndPosition = cursorPosition
} }
function postMessage() { inputField.autoComplete();
roomManager.actionsHandler.postMessage(inputField.text.trim(), attachmentPath, }
replyEventID, editEventId, inputField.userAutocompleted);
clearAttachment(); onTextChanged: {
currentRoom.markAllMessagesAsRead(); timeoutTimer.restart()
clear(); repeatTimer.start()
text = Qt.binding(function() { currentRoom.cachedInput = text
return currentRoom != null ? currentRoom.cachedInput : ""; autoAppeared = false;
});
const autocompletionInfo = documentHandler.getAutocompletionInfo();
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.None) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
} }
function autoComplete() { if (autocompletionInfo.type === ChatDocumentHandler.User) {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText) autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
if (!autoCompleteListView.currentItem.isEmoji) { } else {
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId; autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
} }
if (autoCompleteModel.length === 0) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
isAutoCompleting = true
autoAppeared = true;
autoCompleteEndPosition = cursorPosition
}
// store each user we autoComplete here, this will be helpful later to generate
// the matrix.to links.
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
property var userAutocompleted: ({})
function postMessage() {
// Qt wraps lines so we need to use a small hack
// to remove the wrapped lines but not break the empty
// lines.
documentHandler.postMessage(inputField.text.trim(), attachmentPath, replyEventID,
inputField.userAutocompleted);
clearAttachment();
currentRoom.markAllMessagesAsRead();
clear();
text = Qt.binding(function() {
return currentRoom != null ? currentRoom.cachedInput : "";
});
}
function autoComplete() {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
if (!autoCompleteListView.currentItem.isEmoji) {
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
} }
} }
} }
@@ -548,12 +519,10 @@ ToolBar {
icon.name: "document-send" icon.name: "document-send"
icon.color: "transparent" icon.color: "transparent"
enabled: inputField.length > 0 || hasAttachment
onClicked: { onClicked: {
inputField.postMessage() inputField.postMessage()
inputField.text = "" inputField.text = ""
root.clearEditReply() root.clearReply()
root.closeAll() root.closeAll()
} }
@@ -582,46 +551,21 @@ ToolBar {
} }
function clear() { function clear() {
inputField.clear(); inputField.clear()
inputField.userAutocompleted = {}; inputField.userAutocompleted = {};
} }
function clearEditReply() { function clearReply() {
isReply = false; isReply = false
replyUser = null; replyUser = null;
clear(); replyContent = "";
root.replyContent = ""; replyEventID = ""
root.replyEventID = "";
root.editEventId = "";
focus();
} }
function focus() { function focus() {
inputField.forceActiveFocus() inputField.forceActiveFocus()
} }
function edit(editContent, editFormatedContent, editEventId) {
console.log("Editing ", editContent, "html:", editFormatedContent)
// Set the input field in edit mode
inputField.text = editContent;
root.editEventId = editEventId
// clean autocompletion list
inputField.userAutocompleted = {};
// Fill autocompletion list with values extracted from message.
// We can't just iterate on every user in the list and try to
// find matching display name since some users have display name
// matching frequent words and this will marks too many words as
// mentions.
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
let match;
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
inputField.userAutocompleted[match[2]] = match[1];
}
}
function closeAll() { function closeAll() {
replyItem.visible = false replyItem.visible = false
autoCompleteListView.visible = false autoCompleteListView.visible = false

View File

@@ -1,64 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
LoginStep {
id: root
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
property bool loading: false
title: i18n("@title", "Select a Homeserver")
action: Kirigami.Action {
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
onTriggered: {
// TODO
console.log("register todo")
}
}
onHomeserverChanged: {
LoginHelper.testHomeserver("@user:" + homeserver)
}
Kirigami.FormLayout {
Component.onCompleted: Controller.testHomeserver(homeserver)
QQC2.ComboBox {
id: serverCombo
Kirigami.FormData.label: i18n("Homeserver:")
model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")]
}
QQC2.TextField {
id: customHomeserver
Kirigami.FormData.label: i18n("Url:")
visible: serverCombo.currentIndex === 3
onTextChanged: {
Controller.testHomeserver(text)
}
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
}
}
QQC2.Button {
id: continueButton
text: i18nc("@action:button", "Continue")
action: root.action
}
}
}

View File

@@ -1,23 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
QQC2.BusyIndicator {
property var showContinueButton: false
property var showBackButton: false
property string title: i18n("Loading")
anchors.centerIn: parent
}

View File

@@ -1,68 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: login
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if(acceptableInput) {
LoginHelper.matrixId = text
}
}
Component.onCompleted: {
matrixIdField.forceActiveFocus()
}
Keys.onReturnPressed: {
login.action.trigger()
}
validator: RegularExpressionValidator {
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
}
}
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
} else if (LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/Password.qml");
} else {
processed("qrc:/imports/NeoChat/Component/Login/Sso.qml");
}
}
enabled: LoginHelper.homeserverReachable
}
}

View File

@@ -1,35 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginMethod
title: i18n("Login Methods")
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml")
}
}

View File

@@ -1,35 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginRegister
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml")
}
}

View File

@@ -1,28 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
/// Step for the login/registration flow
ColumnLayout {
property string title: i18n("Welcome")
property string message: i18n("Welcome")
property bool showContinueButton: false
property bool showBackButton: false
property bool acceptable: false
property string previousUrl: ""
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
property Action action: null
/// Called when switching to the next step.
signal processed(url nextUrl)
signal showMessage(string message)
}

View File

@@ -1,56 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: password
title: i18nc("@title", "Password")
message: i18n("Enter your password")
showContinueButton: true
showBackButton: true
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml"
action: Kirigami.Action {
text: i18nc("@action:button", "Login")
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
onTriggered: {
LoginHelper.login();
}
}
Connections {
target: LoginHelper
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
}
Kirigami.FormLayout {
Kirigami.PasswordField {
id: passwordField
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
Component.onCompleted: {
passwordField.forceActiveFocus()
}
Keys.onReturnPressed: {
password.action.trigger()
}
}
}
}

View File

@@ -1,42 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
LoginStep {
id: root
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
Kirigami.FormLayout {
Connections {
target: LoginHelper
onSsoUrlChanged: {
Qt.openUrlExternally(LoginHelper.ssoUrl)
}
onConnected: processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
}
}

View File

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

View File

@@ -107,7 +107,6 @@ RowLayout {
function saveFileAs() { function saveFileAs() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay) var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open() dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
} }
function downloadAndOpen() function downloadAndOpen()

View File

@@ -17,8 +17,6 @@ import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0 import NeoChat.Menu.Timeline 1.0
Image { Image {
id: img
readonly property bool isAnimated: contentType === "image/gif" readonly property bool isAnimated: contentType === "image/gif"
property bool openOnFinished: false property bool openOnFinished: false
@@ -28,7 +26,8 @@ Image {
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info // readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
property bool readonly: false
id: img
source: "image://mxc/" + mediaId source: "image://mxc/" + mediaId
@@ -37,12 +36,34 @@ Image {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
ToolTip.text: display Control {
ToolTip.visible: hoverHandler.hovered anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
HoverHandler { horizontalPadding: 8
id: hoverHandler verticalPadding: 4
enabled: img.readonly
contentItem: RowLayout {
Label {
text: Qt.formatTime(time)
color: "white"
font.pixelSize: 12
}
Label {
text: author.displayName
color: "white"
font.pixelSize: 12
}
}
background: Rectangle {
radius: 2
color: "black"
opacity: 0.3
}
} }
Rectangle { Rectangle {
@@ -66,8 +87,6 @@ Image {
MouseArea { MouseArea {
id: messageMouseArea id: messageMouseArea
enabled: !img.readonly
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton

View File

@@ -51,7 +51,7 @@ RowLayout {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline visible: showAuthor && Config.showAvatarInTimeline
name: author.name ?? author.displayName name: author.name
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color color: author.color
@@ -95,7 +95,7 @@ RowLayout {
visible: showAuthor && !isEmote visible: showAuthor && !isEmote
text: author.displayName text: author.displayName
font.weight: Font.Bold font.bold: true
color: author.color color: author.color
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
@@ -112,9 +112,7 @@ RowLayout {
} }
Connections { Connections {
target: replyLoader.item target: replyLoader.item
function onClicked() { onClicked: replyClicked(reply.eventId)
replyClicked(reply.eventId)
}
} }
} }
RowLayout { RowLayout {
@@ -134,13 +132,6 @@ RowLayout {
onReact: currentRoom.toggleReaction(eventId, emoji) onReact: currentRoom.toggleReaction(eventId, emoji)
} }
} }
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message")
icon.name: "document-edit"
onClicked: chatTextInput.edit(message, model.formattedBody, eventId)
}
QQC2.Button { QQC2.Button {
QQC2.ToolTip.text: i18n("Reply") QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered

View File

@@ -22,7 +22,7 @@ Flow {
contentItem: Label { contentItem: Label {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: modelData.reaction + " " + modelData.count text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "")
} }
padding: Kirigami.Units.smallSpacing padding: Kirigami.Units.smallSpacing
@@ -31,8 +31,6 @@ Flow {
radius: height / 2 radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.Button Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1
} }
checkable: true checkable: true
@@ -47,7 +45,7 @@ Flow {
for (var i = 0; i < modelData.authors.length; i++) { for (var i = 0; i < modelData.authors.length; i++) {
if (i === modelData.authors.length - 1 && i !== 0) { if (i === modelData.authors.length - 1 && i !== 0) {
text += i18nc("Separate the usernames of users", " and ") text += i18nc("Seperate the usernames of users", " and ")
} else if (i !== 0) { } else if (i !== 0) {
text += ", " text += ", "
} }

View File

@@ -15,28 +15,8 @@ TextEdit {
property bool isEmote: false property bool isEmote: false
text: "<style> text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
.user-pill{}
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
@@ -46,7 +26,15 @@ a{
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: { onLinkActivated: {
applicationWindow().handleLink(link, currentRoom) if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (!result || result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
} }
MouseArea { MouseArea {

View File

@@ -93,7 +93,7 @@ Video {
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white" color: "white"
text: i18n("Video") text: "Video"
font.pixelSize: 16 font.pixelSize: 16
padding: 8 padding: 8

View File

@@ -25,7 +25,7 @@ Kirigami.OverlaySheet {
TextField { TextField {
id: roomNameField id: roomNameField
Kirigami.FormData.label: i18n("Room Name") Kirigami.FormData.label: i18n("Room Name")
onAccepted: roomTopicField.forceActiveFocus(); onAccepted: roomTopixField.forceActiveFocus();
} }
TextField { TextField {
@@ -36,11 +36,11 @@ Kirigami.OverlaySheet {
Button { Button {
id: okButton id: okButton
text: i18nc("@action:button", "Ok") text: i18nc("@action:button", "Ok")
onClicked: { onClicked: {
roomManager.actionsHandler.createRoom(roomNameField.text, roomTopicField.text); Controller.createRoom(Controller.activeConnection, roomNameField.text, roomTopicField.text)
root.close(); root.close();
// TODO investigate how to join the new room automatically
root.destroy(); root.destroy();
} }
} }

View File

@@ -24,7 +24,7 @@ QQC2.Popup {
implicitHeight: Kirigami.Units.gridUnit * 20 implicitHeight: Kirigami.Units.gridUnit * 20
contentItem: EmojiPicker { contentItem: EmojiPicker {
onChosen: react(emoji) onChosen: react(emoji);
emojiModel: EmojiModel {} emojiModel: EmojiModel {}
} }
} }

View File

@@ -9,8 +9,6 @@ import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import org.kde.kirigami 2.13 as Kirigami import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0 import NeoChat.Component 1.0
import NeoChat.Setting 1.0 import NeoChat.Setting 1.0
@@ -111,10 +109,7 @@ Kirigami.OverlaySheet {
action: Kirigami.Action { action: Kirigami.Action {
text: i18n("Kick this user") text: i18n("Kick this user")
icon.name: "im-kick-user" icon.name: "im-kick-user"
onTriggered: { onTriggered: room.kickMember(user.id)
room.kickMember(user.id)
root.close()
}
} }
} }
Kirigami.BasicListItem { Kirigami.BasicListItem {
@@ -124,20 +119,7 @@ Kirigami.OverlaySheet {
text: i18n("Ban this user") text: i18n("Ban this user")
icon.name: "im-ban-user" icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor icon.color: Kirigami.Theme.negativeTextColor
onTriggered: { onTriggered: room.banMember(user.id)
room.banMember(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
action: Kirigami.Action {
text: i18n("Open a private chat")
icon.name: "document-send"
onTriggered: {
Controller.activeConnection.requestDirectChat(user)
root.close()
}
} }
} }
Component { Component {

View File

@@ -6,7 +6,6 @@
*/ */
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import NeoChat.Page 1.0
/** /**
* Context menu when clicking on a room in the room list * Context menu when clicking on a room in the room list
@@ -16,24 +15,23 @@ Menu {
property var room property var room
MenuItem { MenuItem {
text: i18n("Open in new window") text: i18n("Favourite")
onTriggered: roomManager.openWindow(room); checkable: true
} checked: room.isFavourite
MenuSeparator {}
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0) onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
} }
MenuItem { MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize") text: i18n("Deprioritize")
checkable: true
checked: room.isLowPriority
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0) onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
} }
MenuSeparator {}
MenuItem { MenuItem {
text: i18n("Mark as Read") text: i18n("Mark as Read")

View File

@@ -57,7 +57,15 @@ Kirigami.OverlaySheet {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
onLinkActivated: { onLinkActivated: {
applicationWindow().handleLink(link, currentRoom) if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (!result || result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
} }
} }
} }

View File

@@ -77,7 +77,7 @@ Kirigami.ScrollablePage {
actions.main: Kirigami.Action { actions.main: Kirigami.Action {
text: i18n("Add an account") text: i18n("Add an account")
iconName: "list-add-user" iconName: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml") onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/LoginPage.qml")
} }
Kirigami.OverlaySheet { Kirigami.OverlaySheet {

View File

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

View File

@@ -8,6 +8,7 @@ import QtQuick 2.10
import QtQuick.Controls 2.1 as QQC2 import QtQuick.Controls 2.1 as QQC2
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Dialogs 1.2
import org.kde.kquickimageeditor 1.0 as KQuickImageEditor import org.kde.kquickimageeditor 1.0 as KQuickImageEditor
import QtGraphicalEffects 1.12 import QtGraphicalEffects 1.12
import Qt.labs.platform 1.0 as Platform import Qt.labs.platform 1.0 as Platform
@@ -80,6 +81,21 @@ Kirigami.Page {
onActivated: saveAsAction.trigger(); onActivated: saveAsAction.trigger();
} anchors.fill: parent } anchors.fill: parent
FileDialog {
id: fileDialog
title: i18n("Save As")
folder: shortcuts.home
selectMultiple: false
selectExisting: false
onAccepted: {
fileDialog.close()
}
onRejected: {
fileDialog.close()
}
Component.onCompleted: visible = false
}
KQuickImageEditor.ImageDocument { KQuickImageEditor.ImageDocument {
id: imageDoc id: imageDoc
path: rootEditorView.imagePath path: rootEditorView.imagePath

View File

@@ -0,0 +1,50 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12
import org.kde.kirigami 2.14 as Kirigami
import QtQuick.Layouts 1.12
Kirigami.Page {
id: root
property var room
title: i18n("Invitation Received - %1", room.displayName)
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout {
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Cancel")
onClicked: roomManager.getBack();
}
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: {
room.forget()
roomManager.getBack();
}
}
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
room.acceptInvitation();
roomManager.enterRoom(room);
}
}
}
}
}

View File

@@ -15,13 +15,15 @@ Kirigami.ScrollablePage {
property var room property var room
parent: applicationWindow().overlay
title: i18n("Invite a User") title: i18n("Invite a User")
actions { actions {
main: Kirigami.Action { main: Kirigami.Action {
icon.name: "dialog-close" icon.name: "dialog-close"
text: i18nc("@action", "Cancel") text: i18nc("@action", "Cancel")
onTriggered: applicationWindow().pageStack.layers.pop() onTriggered: applicationWindow().pageStack.pop()
} }
} }
header: RowLayout { header: RowLayout {
@@ -123,7 +125,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
room.inviteToRoom(userID); room.inviteToRoom(userID);
applicationWindow().pageStack.layers.pop(); applicationWindow().pageStack.pop();
} }
} }
} }

View File

@@ -22,6 +22,8 @@ Kirigami.ScrollablePage {
property alias keyword: identifierField.text property alias keyword: identifierField.text
property string server property string server
signal joinRoom(string room)
title: i18n("Explore Rooms") title: i18n("Explore Rooms")
header: Control { header: Control {
@@ -49,9 +51,9 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
if (!identifierField.isJoined) { if (!identifierField.isJoined) {
roomManager.actionsHandler.joinRoom(identifierField.text); Controller.joinRoom(connection, identifierField.text);
// When joining the room, the room will be opened
} }
roomManager.enterRoom(connection.room(identifierField.room));
applicationWindow().pageStack.layers.pop(); applicationWindow().pageStack.layers.pop();
} }
} }
@@ -102,17 +104,17 @@ Kirigami.ScrollablePage {
width: publicRoomsListView.width width: publicRoomsListView.width
onClicked: { onClicked: {
if (!isJoined) { if (!isJoined) {
roomManager.actionsHandler.joinRoom(connection, roomID) Controller.joinRoom(connection, roomID)
justJoined = true; justJoined = true;
} else { } else {
roomManager.enterRoom(connection.room(roomID)) roomManager.enterRoom(connection.room(roomID))
applicationWindow().pageStack.layers.pop();
} }
applicationWindow().pageStack.layers.pop();
} }
contentItem: RowLayout { contentItem: RowLayout {
Kirigami.Avatar { Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.normal Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.normal Layout.preferredHeight: Kirigami.Units.iconSizes.huge
source: model.avatar ? ("image://mxc/" + model.avatar) : "" source: model.avatar ? ("image://mxc/" + model.avatar) : ""
name: name name: name

View File

@@ -0,0 +1,97 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
Kirigami.ScrollablePage {
id: root
title: i18n("Login")
header: QQC2.Control {
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
id: inlineMessage
visible: false
showCloseButton: true
}
}
Kirigami.FormLayout {
id: formLayout
QQC2.TextField {
id: serverField
Kirigami.FormData.label: i18n("Server Address")
text: "https://matrix.org"
onAccepted: usernameField.forceActiveFocus()
}
QQC2.TextField {
id: usernameField
Kirigami.FormData.label: i18n("Username")
onAccepted: passwordField.forceActiveFocus()
}
Kirigami.PasswordField {
id: passwordField
Kirigami.FormData.label: i18n("Password")
onAccepted: accessTokenField.forceActiveFocus()
}
QQC2.TextField {
id: accessTokenField
Kirigami.FormData.label: i18n("Access Token (Optional)")
onAccepted: deviceNameField.forceActiveFocus()
}
QQC2.TextField {
id: deviceNameField
Kirigami.FormData.label: i18n("Device Name (Optional)")
onAccepted: doLogin()
}
RowLayout {
QQC2.Button {
visible: Controller.accountCount > 0
text: i18n("Cancel")
onClicked: {
pageStack.layers.clear();
}
}
QQC2.Button {
text: i18n("Login")
onClicked: doLogin()
}
}
Connections {
target: Controller
onErrorOccured: {
inlineMessage.type = Kirigami.MessageType.Error;
if (detail && detail.length !== 0) {
inlineMessage.text = i18n("%1: %2", error, detail);
} else {
inlineMessage.text = error;
}
inlineMessage.visible = true;
}
}
}
function doLogin() {
inlineMessage.text = i18n("Loading, this might take up to 10 seconds.");
inlineMessage.type = Kirigami.MessageType.Information
inlineMessage.visible = true;
if (accessTokenField.text.length > 0) {
Controller.loginWithAccessToken(serverField.text.trim(), usernameField.text.trim(), accessTokenField.text, deviceNameField.text.trim());
} else {
Controller.loginWithCredentials(serverField.text.trim(), usernameField.text.trim(), passwordField.text, deviceNameField.text.trim());
}
}
}

View File

@@ -18,9 +18,13 @@ import NeoChat.Menu 1.0
Kirigami.ScrollablePage { Kirigami.ScrollablePage {
id: page id: page
property var roomListModel
property var enteredRoom property var enteredRoom
required property var activeConnection required property var activeConnection
signal enterRoom(var room)
signal leaveRoom(var room)
function goToNextRoom() { function goToNextRoom() {
do { do {
listView.incrementCurrentIndex(); listView.incrementCurrentIndex();
@@ -61,10 +65,7 @@ Kirigami.ScrollablePage {
} }
model: SortFilterRoomListModel { model: SortFilterRoomListModel {
id: sortFilterRoomListModel id: sortFilterRoomListModel
sourceModel: RoomListModel { sourceModel: roomListModel
id: roomListModel
connection: page.activeConnection
}
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
} }
@@ -76,50 +77,100 @@ Kirigami.ScrollablePage {
} }
contentItem: RowLayout { contentItem: RowLayout {
implicitHeight: categoryName.implicitHeight implicitHeight: categoryName.implicitHeight
Kirigami.Icon {
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
}
Kirigami.Heading { Kirigami.Heading {
id: categoryName id: categoryName
level: 3 level: 3
text: roomListModel.categoryName(section) text: roomListModel.categoryName(section)
Layout.fillWidth: true Layout.fillWidth: true
} }
Kirigami.Icon {
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
}
} }
} }
delegate: Kirigami.BasicListItem { delegate: Kirigami.AbstractListItem {
id: roomListItem id: roomListItem
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList property bool itemVisible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
visible: itemVisible
highlighted: roomManager.currentRoom && roomManager.currentRoom.displayName === displayName
focus: true focus: true
icon: undefined
action: Kirigami.Action { action: Kirigami.Action {
id: enterRoomAction id: enterRoomAction
onTriggered: { onTriggered: {
var roomItem = roomManager.enterRoom(currentRoom) if (category === RoomType.Invited) {
roomListItem.KeyNavigation.right = roomItem roomManager.openInvitation(currentRoom);
roomItem.focus = true; } else {
var roomItem = roomManager.enterRoom(currentRoom)
roomListItem.KeyNavigation.right = roomItem
roomItem.focus = true;
}
} }
} }
label: name ?? ""
subtitle: { contentItem: RowLayout {
let txt = (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ") id: roomLayout
if (txt.length) { spacing: Kirigami.Units.largeSpacing
return txt width: listView.width
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
} }
return " "
}
leading: Kirigami.Avatar { TapHandler {
source: avatar ? "image://mxc/" + avatar : "" onTapped: enterRoomAction.trigger()
name: model.name || i18n("No Name") onLongPressed: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
implicitWidth: height }
}
trailing: RowLayout { Kirigami.Avatar {
id: roomAvatar
property int size: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing
Layout.minimumHeight: size
Layout.maximumHeight: size
Layout.minimumWidth: size
Layout.maximumWidth: size
source: avatar ? ("image://mxc/" + avatar) : ""
name: model.name || i18n("No Name")
}
ColumnLayout {
id: roomitemcolumn
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
Layout.maximumHeight: Kirigami.Units.gridUnit * 2
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignHCenter
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: displayName ?? ""
elide: Text.ElideRight
font.bold: unreadCount >= 0 || highlightCount > 0 || notificationCount > 0
wrapMode: Text.NoWrap
}
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.7)
}
}
QQC2.Label { QQC2.Label {
text: notificationCount text: notificationCount
visible: notificationCount > 0 visible: notificationCount > 0
@@ -133,26 +184,6 @@ Kirigami.ScrollablePage {
radius: height / 2 radius: height / 2
} }
} }
QQC2.Button {
id: configButton
visible: roomListItem.hovered || Kirigami.Settings.isMobile
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
id: optionAction
icon.name: "configure"
onTriggered: {
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
configButton.visible = true
configButton.down = true
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.popup()
}
}
}
} }
} }
Component { Component {

View File

@@ -23,22 +23,11 @@ import NeoChat.Menu.Timeline 1.0
Kirigami.ScrollablePage { Kirigami.ScrollablePage {
id: page id: page
required property var currentRoom property var currentRoom
title: currentRoom.displayName
signal switchRoomUp() signal switchRoomUp()
signal switchRoomDown() signal switchRoomDown()
Connections {
target: Controller.activeConnection
function onJoinedRoom(room) {
if(room.id === invitation.id) {
roomManager.enterRoom(room);
}
}
}
Connections { Connections {
target: roomManager.actionsHandler target: roomManager.actionsHandler
onShowMessage: { onShowMessage: {
@@ -60,46 +49,37 @@ Kirigami.ScrollablePage {
} }
} }
Kirigami.PlaceholderMessage { title: currentRoom.displayName
id: invitation
property var id titleDelegate: Component {
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout { RowLayout {
QQC2.Button { visible: !Kirigami.Settings.isMobile
Layout.alignment : Qt.AlignHCenter Layout.fillWidth: true
text: i18n("Reject") Layout.maximumWidth: implicitWidth + 1 // The +1 is to make sure we do not trigger eliding at max width
Layout.minimumWidth: 0
onClicked: { spacing: Kirigami.Units.gridUnit * 0.8
page.currentRoom.forget() Kirigami.Heading {
roomManager.getBack(); id: titleLabel
} level: 2
Layout.alignment: Qt.AlignVCenter
text: page.title
opacity: page.isCurrentPage ? 1 : 0.4
maximumLineCount: 1
elide: Text.ElideRight
} }
QQC2.Label {
QQC2.Button { Layout.fillWidth: true
Layout.alignment : Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter
text: i18n("Accept") anchors.baseline: lineCount < 2 ? titleLabel.baseline : undefined // necessary, since there is no way to do this with Layout.alignment
text: currentRoom.topic
onClicked: { maximumLineCount: 2
currentRoom.acceptInvitation(); wrapMode: Text.Wrap
invitation.id = currentRoom.id elide: Text.ElideRight
currentRoom = null color: Kirigami.Theme.disabledTextColor
} font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9
} }
} }
} }
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
QQC2.BusyIndicator {
running: true
}
}
focus: true focus: true
Keys.onTabPressed: { Keys.onTabPressed: {
@@ -130,8 +110,6 @@ Kirigami.ScrollablePage {
ListView { ListView {
id: messageListView id: messageListView
visible: !invitation.visible
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
readonly property bool isLoaded: page.width * page.height > 10 readonly property bool isLoaded: page.width * page.height > 10
@@ -168,6 +146,14 @@ Kirigami.ScrollablePage {
room: currentRoom room: currentRoom
} }
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: messageListView.count === 0 && !currentRoom.allHistoryLoaded
QQC2.BusyIndicator {
running: true
}
}
QQC2.Popup { QQC2.Popup {
anchors.centerIn: parent anchors.centerIn: parent
@@ -187,7 +173,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
attachDialog.close() attachDialog.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) { fileDialog.chosen.connect(function(path) {
if (!path) return if (!path) return
@@ -226,10 +212,25 @@ Kirigami.ScrollablePage {
} }
MessageFilterModel { KSortFilterProxyModel {
id: sortedMessageEventModel id: sortedMessageEventModel
sourceModel: messageEventModel sourceModel: messageEventModel
filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin
function dontFilterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other";
}
function filterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state";
}
} }
// populate: Transition { // populate: Transition {
@@ -322,23 +323,24 @@ Kirigami.ScrollablePage {
onReplyClicked: goToEvent(eventID) onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [ innerObject: [
MouseArea {
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton
anchors.fill: parent
onClicked: {
if (mouse.button == Qt.RightButton) {
openMessageContext(author, display, eventId, toolTip);
}
}
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
},
TextDelegate { TextDelegate {
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, display, eventId, toolTip)
}
TapHandler {
acceptedButtons: Qt.LeftButton
//enabled: Kirigami.Settings.isMobile
onLongPressed: openMessageContext(author, display, eventId, toolTip)
}
}, },
ReactionDelegate { ReactionDelegate {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.topMargin: 0
Layout.bottomMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
} }
] ]
} }
@@ -382,35 +384,7 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.topMargin: 0
Layout.maximumHeight: 320 Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: 8
}
]
}
}
}
DelegateChoice {
roleValue: "sticker"
delegate: TimelineContainer {
width: messageListView.width
innerObject: MessageDelegate {
Layout.fillWidth: true
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
ImageDelegate {
readonly: true
Layout.maximumWidth: parent.width / 2
Layout.minimumWidth: 320
Layout.preferredHeight: info.h / info.w * width
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing
} }
] ]
} }
@@ -491,7 +465,6 @@ Kirigami.ScrollablePage {
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "other" roleValue: "other"
delegate: Item {} delegate: Item {}
@@ -590,8 +563,6 @@ Kirigami.ScrollablePage {
footer: ChatTextInput { footer: ChatTextInput {
id: chatTextInput id: chatTextInput
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@@ -1,21 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.14 as Kirigami
Kirigami.ApplicationWindow {
id: window
required property var currentRoom
minimumWidth: Kirigami.Units.gridUnit * 10
minimumHeight: Kirigami.Units.gridUnit * 15
pageStack.initialPage: RoomPage {
visible: true
currentRoom: window.currentRoom
}
}

View File

@@ -16,15 +16,6 @@ Kirigami.ScrollablePage {
title: i18n("Settings") title: i18n("Settings")
Kirigami.FormLayout { Kirigami.FormLayout {
QQC2.CheckBox {
Kirigami.FormData.label: i18n("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox { QQC2.CheckBox {
// TODO: When there are enough notification and timeline event // TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels. // settings, make 2 separate groups with FormData labels.

View File

@@ -42,10 +42,7 @@ Kirigami.ScrollablePage {
text: i18n("Chat") text: i18n("Chat")
highlighted: true highlighted: true
onClicked: { onClicked: Controller.createDirectChat(connection, identifierField.text)
connection.requestDirectChat(identifierField.text);
applicationWindow().pageStack.layers.pop();
}
} }
} }
} }
@@ -106,27 +103,23 @@ Kirigami.ScrollablePage {
} }
Button { Button {
id: joinChatButton
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
visible: directChats && directChats.length > 0 visible: directChats != null
icon.name: "document-send" icon.name: "document-send"
onClicked: { onClicked: {
connection.requestDirectChat(userID); roomListForm.joinRoom(connection.room(directChats[0]))
applicationWindow().pageStack.layers.pop(); root.close()
} }
} }
Button { Button {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
icon.name: "irc-join-channel" icon.name: "irc-join-channel"
// We wants to make sure an user can't start more than one
// chat with someone.
visible: !joinChatButton.visible
onClicked: { onClicked: {
connection.requestDirectChat(userID); Controller.createDirectChat(connection, userID)
applicationWindow().pageStack.layers.pop(); root.close()
} }
} }
} }

View File

@@ -1,101 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
Kirigami.ScrollablePage {
id: welcomePage
property alias currentStep: module.item
title: module.item.title ?? i18n("Welcome")
header: Controls.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
Component.onCompleted: LoginHelper.init()
Connections {
target: LoginHelper
onErrorOccured: {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
Controls.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 25
text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix")
}
Loader {
id: module
Layout.alignment: Qt.AlignHCenter
source: "qrc:/imports/NeoChat/Component/Login/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Controls.Button {
text: i18nc("@action:button", "Back")
enabled: welcomePage.currentStep.previousUrl !== ""
visible: welcomePage.currentStep.showBackButton
Layout.alignment: Qt.AlignHCenter
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
}
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
Connections {
target: currentStep
function onProcessed(nextUrl) {
module.source = nextUrl;
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
}
}
}

View File

@@ -1,8 +1,8 @@
module NeoChat.Page module NeoChat.Page
LoadingPage 1.0 LoadingPage.qml LoadingPage 1.0 LoadingPage.qml
LoginPage 1.0 LoginPage.qml
RoomListPage 1.0 RoomListPage.qml RoomListPage 1.0 RoomListPage.qml
RoomPage 1.0 RoomPage.qml RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml SettingsPage 1.0 SettingsPage.qml

View File

@@ -48,7 +48,7 @@ Kirigami.OverlayDrawer {
icon.name: "list-add-user" icon.name: "list-add-user"
text: i18n("Invite") text: i18n("Invite")
onClicked: { onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room}) applicationWindow().pageStack.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
roomDrawer.close(); roomDrawer.close();
} }
} }
@@ -84,6 +84,12 @@ Kirigami.OverlayDrawer {
} }
} }
Component {
id: fullScreenImage
FullScreenImage {}
}
Control { Control {
Layout.fillWidth: true Layout.fillWidth: true
bottomPadding: Kirigami.Units.largeSpacing bottomPadding: Kirigami.Units.largeSpacing
@@ -114,7 +120,6 @@ Kirigami.OverlayDrawer {
spacing: 0 spacing: 0
Kirigami.Heading { Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true Layout.fillWidth: true
level: 1 level: 1
font.bold: true font.bold: true
@@ -139,7 +144,6 @@ Kirigami.OverlayDrawer {
selectByMouse: true selectByMouse: true
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
@@ -167,16 +171,6 @@ Kirigami.OverlayDrawer {
headerPositioning: ListView.OverlayHeader headerPositioning: ListView.OverlayHeader
boundsBehavior: Flickable.DragOverBounds boundsBehavior: Flickable.DragOverBounds
header: Pane {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
contentItem: Kirigami.SearchField {
id: userListSearchField
onTextChanged: sortedMessageEventModel.filterString = text;
}
}
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
id: sortedMessageEventModel id: sortedMessageEventModel
@@ -185,13 +179,11 @@ Kirigami.OverlayDrawer {
} }
sortRole: "perm" sortRole: "perm"
filterRole: "name"
} }
delegate: Kirigami.AbstractListItem { delegate: Kirigami.AbstractListItem {
width: userListView.width width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2 implicitHeight: Kirigami.Units.gridUnit * 2
z: 1
contentItem: RowLayout { contentItem: RowLayout {
Kirigami.Avatar { Kirigami.Avatar {
@@ -229,7 +221,7 @@ Kirigami.OverlayDrawer {
} }
return "" return ""
} }
color: Kirigami.Theme.disabledTextColor color: perm == UserType.Muted ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
font.pixelSize: 12 font.pixelSize: 12
textFormat: Text.PlainText textFormat: Text.PlainText
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
@@ -237,7 +229,7 @@ Kirigami.OverlayDrawer {
} }
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open() onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open()
} }
} }
} }

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -3,7 +3,6 @@ IconName=org.kde.neochat
Name=Neochat Name=Neochat
Name[ca]=Neochat Name[ca]=Neochat
Name[ca@valencia]=Neochat Name[ca@valencia]=Neochat
Name[cs]=Neochat
Name[da]=Neochat Name[da]=Neochat
Name[de]=Neochat Name[de]=Neochat
Name[en_GB]=Neochat Name[en_GB]=Neochat
@@ -12,14 +11,12 @@ Name[eu]=Neochat
Name[fi]=Neochat Name[fi]=Neochat
Name[fr]=Neochat Name[fr]=Neochat
Name[hu]=Neochat Name[hu]=Neochat
Name[ia]=Neochat
Name[it]=Neochat Name[it]=Neochat
Name[nl]=Neochat Name[nl]=Neochat
Name[nn]=Neochat Name[nn]=Neochat
Name[pl]=Neochat Name[pl]=Neochat
Name[pt]=Neochat Name[pt]=Neochat
Name[pt_BR]=Neochat Name[pt_BR]=Neochat
Name[ro]=Neochat
Name[sk]=Neochat Name[sk]=Neochat
Name[sl]=Neochat Name[sl]=Neochat
Name[sv]=Neochat Name[sv]=Neochat
@@ -27,34 +24,33 @@ Name[uk]=Neochat
Name[x-test]=xxNeochatxx Name[x-test]=xxNeochatxx
Name[zh_CN]=Neochat Name[zh_CN]=Neochat
DesktopEntry=org.kde.neochat DesktopEntry=org.kde.neochat
Comment=A client for matrix, the decentralized communication protocol Comment=IM client for the Matrix protocol
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat Comment[ca]=Client de MI per al protocol Matrix
Comment[ca@valencia]=Un client per al Matrix, el protocol de comunicacions descentralitzat Comment[ca@valencia]=Client de MI per al protocol Matrix
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll Comment[de]=IM-Programm für das Matrix-Protokoll
Comment[en_GB]=A client for matrix, the decentralised communication protocol Comment[en_GB]=IM client for the Matrix protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado Comment[es]=Cliente de MI para el protocolo Matrix
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat Comment[eu]=Matrix protokolorako bat-bateko mezularitza bezeroa
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma Comment[fi]=Pikaviestiasiakas Matrix-yhteyskäytännölle
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications. Comment[fr]=Client « IM » pour le protocole « Matrix »
Comment[ia]=Un cliente per matrix, le protocollo de communication decentralisate Comment[hu]=Azonnali üzenetküldő kliens a Matrix protokollhoz
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato Comment[it]=Client di messaggistica istantanea per il protocollo Matrix
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol Comment[nl]=IM-client voor het Matrix-protocol
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldings­protokollen. Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się Comment[pl]=Komunikator internetowy dla protokołu Matrix
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado Comment[pt]=Cliente de MI para o protocolo Matrix
Comment[ro]=Client pentru Matrix, protocolul de comunicare descentralizată Comment[pt_BR]=Cliente de mensageiro instantâneo para o protocolo Matrix
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix Comment[sk]=IM klient pre protokol Matrix
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet Comment[sl]=Odjemalec neposrednega sporočanja po protokolu Matrix
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними Comment[sv]=Direktmeddelandeklient för protokollet Matrix
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx Comment[uk]=Клієнт служби миттєвого обміну повідомленнями для протоколу Matrix
Comment[zh_CN]=分布式通讯协议 Matrix 的客户端 Comment[x-test]=xxIM client for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的 IM 客户端
[Event/message] [Event/message]
Name=New message Name=New message
Name[ca]=Missatge nou Name[ca]=Missatge nou
Name[ca@valencia]=Missatge nou Name[ca@valencia]=Missatge nou
Name[cs]=Nová zpráva
Name[de]=Neue Nachricht Name[de]=Neue Nachricht
Name[en_GB]=New message Name[en_GB]=New message
Name[es]=Nuevo mensaje Name[es]=Nuevo mensaje
@@ -62,14 +58,12 @@ Name[eu]=Mezu berria
Name[fi]=Uusi viesti Name[fi]=Uusi viesti
Name[fr]=Nouveau message Name[fr]=Nouveau message
Name[hu]=Új üzenet Name[hu]=Új üzenet
Name[ia]=Nove message
Name[it]=Nuovo messaggio Name[it]=Nuovo messaggio
Name[nl]=Nieuw bericht Name[nl]=Nieuw bericht
Name[nn]=Ny melding Name[nn]=Ny melding
Name[pl]=Nowa wiadomość Name[pl]=Nowa wiadomość
Name[pt]=Nova mensagem Name[pt]=Nova mensagem
Name[pt_BR]=Nova mensagem Name[pt_BR]=Nova mensagem
Name[ro]=Mesaj nou
Name[sk]=Nová správa Name[sk]=Nová správa
Name[sl]=Novo sporočilo Name[sl]=Novo sporočilo
Name[sv]=Nytt meddelande Name[sv]=Nytt meddelande
@@ -86,14 +80,12 @@ Comment[eu]=Mezu berri bat dago
Comment[fi]=Saapui uusi viesti Comment[fi]=Saapui uusi viesti
Comment[fr]=Il y a un nouveau message Comment[fr]=Il y a un nouveau message
Comment[hu]=Új üzenet érkezett Comment[hu]=Új üzenet érkezett
Comment[ia]=Isto es un nove message
Comment[it]=È presente un nuovo messaggio Comment[it]=È presente un nuovo messaggio
Comment[nl]=Er is een nieuw bericht Comment[nl]=Er is een nieuw bericht
Comment[nn]=Du har ei ny melding Comment[nn]=Du har ei ny melding
Comment[pl]=Dostępna jest nowa wiadomość Comment[pl]=Dostępna jest nowa wiadomość
Comment[pt]=Tem uma mensagem nova Comment[pt]=Tem uma mensagem nova
Comment[pt_BR]=Existe uma nova mensagem Comment[pt_BR]=Existe uma nova mensagem
Comment[ro]=Este un mesaj nou
Comment[sk]=Je nová správa Comment[sk]=Je nová správa
Comment[sl]=Prišlo je novo sporočilo Comment[sl]=Prišlo je novo sporočilo
Comment[sv]=Det finns ett nytt meddelande Comment[sv]=Det finns ett nytt meddelande

View File

@@ -1,13 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V12H5.68102L2 15.0675V12H1V1ZM2 11H3V12.9325L5.31897 11H14V2H2V11Z"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="4" width="9" height="1"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="6" width="7" height="1"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="8" width="5" height="1"/>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M12 12.2929L10.8536 11.1465L10.1465 11.8536L13 14.7071V11.5H12V12.2929Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

View File

@@ -7,7 +7,6 @@
<name>Neochat</name> <name>Neochat</name>
<name xml:lang="ca">Neochat</name> <name xml:lang="ca">Neochat</name>
<name xml:lang="ca-valencia">Neochat</name> <name xml:lang="ca-valencia">Neochat</name>
<name xml:lang="cs">Neochat</name>
<name xml:lang="da">Neochat</name> <name xml:lang="da">Neochat</name>
<name xml:lang="de">Neochat</name> <name xml:lang="de">Neochat</name>
<name xml:lang="en-GB">Neochat</name> <name xml:lang="en-GB">Neochat</name>
@@ -16,7 +15,6 @@
<name xml:lang="fi">Neochat</name> <name xml:lang="fi">Neochat</name>
<name xml:lang="fr">Neochat</name> <name xml:lang="fr">Neochat</name>
<name xml:lang="hu">Neochat</name> <name xml:lang="hu">Neochat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">Neochat</name> <name xml:lang="id">Neochat</name>
<name xml:lang="it">Neochat</name> <name xml:lang="it">Neochat</name>
<name xml:lang="nl">Neochat</name> <name xml:lang="nl">Neochat</name>
@@ -32,7 +30,6 @@
<summary>A client for matrix, the decentralized communication protocol</summary> <summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary> <summary xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary> <summary xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary> <summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary> <summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary> <summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
@@ -40,7 +37,6 @@
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary> <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="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="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary> <summary xml:lang="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="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary> <summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
@@ -57,7 +53,6 @@
<p>A client for matrix, the decentralized communication protocol.</p> <p>A client for matrix, the decentralized communication protocol.</p>
<p xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p> <p xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p>
<p xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p> <p xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p>
<p xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix.</p>
<p xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll.</p> <p xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll.</p>
<p xml:lang="en-GB">A client for matrix, the decentralised communication protocol.</p> <p xml:lang="en-GB">A client for matrix, the decentralised communication protocol.</p>
<p xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado.</p> <p xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado.</p>
@@ -65,7 +60,6 @@
<p xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle.</p> <p xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle.</p>
<p xml:lang="fr">Un client « Matrix », le protocole décentralisé de communications.</p> <p xml:lang="fr">Un client « Matrix », le protocole décentralisé de communications.</p>
<p xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz.</p> <p xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz.</p>
<p xml:lang="ia">Un cliente per matrix, le protocollo de communication decentralisate.</p>
<p xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi.</p> <p xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi.</p>
<p xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato.</p> <p xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato.</p>
<p xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol.</p> <p xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol.</p>
@@ -87,7 +81,6 @@
<developer_name>The KDE Community</developer_name> <developer_name>The KDE Community</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name> <developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name> <developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name> <developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name> <developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name> <developer_name xml:lang="es">La comunidad KDE</developer_name>
@@ -95,7 +88,6 @@
<developer_name xml:lang="fi">KDE-yhteisö</developer_name> <developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name> <developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name> <developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name> <developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name> <developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name> <developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
@@ -110,30 +102,18 @@
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name> <developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license> <project_license>GPL-3.0</project_license>
<value key="KDE::matrix">#neochat:kde.org</value>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image> <image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://cdn.kde.org/screenshots/neochat/application-mobile.png</image> <image>https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<content_rating type="oars-1.1"> <content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release urgency="critical" version="1.1.1" date="2021-02-23"/>
<release version="1.1.0" date="2021-02-22">
<description>
<p>Probably the highlight of this release is the completely new login page. It detects the server configuration based on your Matrix Id. This allows you to login to servers requiring Single Sign On (SSO) (like the Mozilla or the incoming Fedora Matrix instance).</p>
<p>Servers that require agreeing to the TOS before usage are correctly detected now and redirect to their TOS webpage, allowing the user to agree to them instead of silently failing to load the account.</p>
<p>It is now possible to open a room into a new window. This allows you to view and interact with multiple rooms at the same time.</p>
<p>We added a few commands to NeoChat (/shrug, /lenny, /join, /ignore, ...).</p>
<p>We improved the Plasma integration a bit. Now the number of unread messages is displayed in the Plasma Taskbar.</p>
</description>
<url>https://carlschwan.eu/2020/02/22/neochat-1.1/</url>
</release>
<release version="1.0.1" date="2021-01-13"> <release version="1.0.1" date="2021-01-13">
<description> <description>
<p>This version fixes several bugs.</p> <p>This version fixes several bugs.</p>

View File

@@ -2,7 +2,6 @@
Name=Neochat Name=Neochat
Name[ca]=Neochat Name[ca]=Neochat
Name[ca@valencia]=Neochat Name[ca@valencia]=Neochat
Name[cs]=Neochat
Name[da]=Neochat Name[da]=Neochat
Name[de]=Neochat Name[de]=Neochat
Name[en_GB]=Neochat Name[en_GB]=Neochat
@@ -11,14 +10,12 @@ Name[eu]=Neochat
Name[fi]=Neochat Name[fi]=Neochat
Name[fr]=Neochat Name[fr]=Neochat
Name[hu]=Neochat Name[hu]=Neochat
Name[ia]=Neochat
Name[it]=Neochat Name[it]=Neochat
Name[nl]=Neochat Name[nl]=Neochat
Name[nn]=Neochat Name[nn]=Neochat
Name[pl]=Neochat Name[pl]=Neochat
Name[pt]=Neochat Name[pt]=Neochat
Name[pt_BR]=Neochat Name[pt_BR]=Neochat
Name[ro]=Neochat
Name[sk]=Neochat Name[sk]=Neochat
Name[sl]=Neochat Name[sl]=Neochat
Name[sv]=Neochat Name[sv]=Neochat
@@ -28,7 +25,6 @@ Name[zh_CN]=Neochat
GenericName=Matrix Client GenericName=Matrix Client
GenericName[ca]=Client del Matrix GenericName[ca]=Client del Matrix
GenericName[ca@valencia]=Client del Matrix GenericName[ca@valencia]=Client del Matrix
GenericName[cs]=Klient protokolu Matrix
GenericName[de]=Matrix-Programm GenericName[de]=Matrix-Programm
GenericName[en_GB]=Matrix Client GenericName[en_GB]=Matrix Client
GenericName[es]=Cliente para Matrix GenericName[es]=Cliente para Matrix
@@ -36,14 +32,12 @@ GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix » GenericName[fr]=Client « Matrix »
GenericName[hu]=Matrix kliens GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrix
GenericName[it]=Client Matrix GenericName[it]=Client Matrix
GenericName[nl]=Matrix-client GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient GenericName[nn]=Matrix-klient
GenericName[pl]=Program Matriksa GenericName[pl]=Program Matriksa
GenericName[pt]=Cliente de Matrix GenericName[pt]=Cliente de Matrix
GenericName[pt_BR]=Cliente Matrix GenericName[pt_BR]=Cliente Matrix
GenericName[ro]=Client Matrix
GenericName[sk]=Matrix Client GenericName[sk]=Matrix Client
GenericName[sl]=Odjemalec Matrix GenericName[sl]=Odjemalec Matrix
GenericName[sv]=Matrix-klient GenericName[sv]=Matrix-klient
@@ -60,14 +54,12 @@ Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix » Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Kliens a Matrix protokollhoz Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[it]=Client per il protocollo Matrix Comment[it]=Client per il protocollo Matrix
Comment[nl]=Client voor het Matrix-protocol Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pl]=Program obsługi protokołu Matriksa Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[sk]=Klient protokolu Matrix Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix Comment[sv]=Klient för protokollet Matrix

View File

@@ -6,7 +6,6 @@
*/ */
import QtQuick 2.14 import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2 import QtQuick.Controls 2.14 as QQC2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami import org.kde.kirigami 2.12 as Kirigami
@@ -20,17 +19,11 @@ import NeoChat.Page 1.0
Kirigami.ApplicationWindow { Kirigami.ApplicationWindow {
id: root id: root
property var currentRoom: null
property int columnWidth: Kirigami.Units.gridUnit * 13 property int columnWidth: Kirigami.Units.gridUnit * 13
minimumWidth: Kirigami.Units.gridUnit * 15
minimumHeight: Kirigami.Units.gridUnit * 20
wideScreen: width > columnWidth * 5 wideScreen: width > columnWidth * 5
onClosing: Controller.saveWindowGeometry(root)
pageStack.initialPage: LoadingPage {}
Connections { Connections {
target: root.quitAction target: root.quitAction
function onTriggered() { function onTriggered() {
@@ -38,38 +31,15 @@ Kirigami.ApplicationWindow {
} }
} }
// 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
// the previous session.
Timer {
id: saveWindowGeometryTimer
interval: 1000
onTriggered: Controller.saveWindowGeometry(root)
}
onWidthChanged: saveWindowGeometryTimer.restart()
onHeightChanged: saveWindowGeometryTimer.restart()
onXChanged: saveWindowGeometryTimer.restart()
onYChanged: saveWindowGeometryTimer.restart()
/** /**
* Manage opening and close rooms * Manage opening and close rooms
* TODO this should probably be moved to C++
*/ */
QtObject { QtObject {
id: roomManager id: roomManager
property var actionsHandler: ActionsHandler {
room: roomManager.currentRoom
connection: Controller.activeConnection
onRoomJoined: {
roomManager.enterRoom(Controller.activeConnection.room(roomName))
}
}
property var currentRoom: null property var currentRoom: null
property alias pageStack: root.pageStack property alias pageStack: root.pageStack
property bool invitationOpen: false
property var roomList: null property var roomList: null
property Item roomItem: null property Item roomItem: null
@@ -78,20 +48,11 @@ Kirigami.ApplicationWindow {
signal leaveRoom(string room); signal leaveRoom(string room);
signal openRoom(string room); signal openRoom(string room);
function roomByAliasOrId(aliasOrId) {
return Controller.activeConnection.room(aliasOrId)
}
function openRoomAndEvent(room, event) {
enterRoom(room)
roomItem.goToEvent(event)
}
function loadInitialRoom() { function loadInitialRoom() {
if (Config.openRoom) { if (Config.openRoom) {
const room = Controller.activeConnection.room(Config.openRoom); const room = Controller.activeConnection.room(Config.openRoom);
currentRoom = room; currentRoom = room;
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, }); roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
connectRoomToSignal(roomItem); connectRoomToSignal(roomItem);
} else { } else {
// TODO create welcome page // TODO create welcome page
@@ -99,11 +60,12 @@ Kirigami.ApplicationWindow {
} }
function enterRoom(room) { function enterRoom(room) {
if (currentRoom != null) { let item = null;
if (currentRoom != null || invitationOpen) {
roomItem.currentRoom = room; roomItem.currentRoom = room;
pageStack.currentIndex = pageStack.depth - 1; pageStack.currentIndex = pageStack.depth - 1;
} else { } else {
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, }); roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
} }
currentRoom = room; currentRoom = room;
Config.openRoom = room.id; Config.openRoom = room.id;
@@ -112,14 +74,17 @@ Kirigami.ApplicationWindow {
return roomItem; return roomItem;
} }
function getBack() { function openInvitation(room) {
pageStack.replace("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': currentRoom, }); if (currentRoom != null) {
currentRoom = null;
pageStack.removePage(pageStack.lastItem);
}
invitationOpen = true;
pageStack.push("qrc:/imports/NeoChat/Page/InvitationPage.qml", {"room": room});
} }
function openWindow(room) { function getBack() {
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room}); pageStack.replace(roomPage, { 'currentRoom': currentRoom, });
secondayWindow.width = root.width - roomList.width;
secondayWindow.show();
} }
function connectRoomToSignal(item) { function connectRoomToSignal(item) {
@@ -154,7 +119,7 @@ Kirigami.ApplicationWindow {
id: contextDrawer id: contextDrawer
contentItem.implicitWidth: columnWidth contentItem.implicitWidth: columnWidth
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
modal: !root.wideScreen || !enabled modal: !root.wideScreen
onEnabledChanged: drawerOpen = enabled && !modal onEnabledChanged: drawerOpen = enabled && !modal
onModalChanged: drawerOpen = !modal onModalChanged: drawerOpen = !modal
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
@@ -207,7 +172,7 @@ Kirigami.ApplicationWindow {
icon.name: "settings-configure" icon.name: "settings-configure"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml") onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings") enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: StandardKey.Preferences shortcut: Controller.preferencesShortcuts[0]
}, },
Kirigami.Action { Kirigami.Action {
text: i18n("About Neochat") text: i18n("About Neochat")
@@ -237,60 +202,45 @@ Kirigami.ApplicationWindow {
} }
} }
pageStack.initialPage: LoadingPage {}
Component { Component {
id: roomListComponent id: roomListComponent
RoomListPage { RoomListPage {
id: roomList id: roomList
roomListModel: spectralRoomListModel
activeConnection: Controller.activeConnection activeConnection: Controller.activeConnection
} }
} }
Connections {
target: LoginHelper
function onInitialSyncFinished() {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
Connections { Connections {
target: Controller target: Controller
function onInitiated() { onInitiated: {
if (roomManager.hasOpenRoom) {
return;
}
if (Controller.accountCount === 0) { if (Controller.accountCount === 0) {
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {}); pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {});
} else { } else {
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection}); roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
roomManager.loadInitialRoom(); roomManager.loadInitialRoom();
} }
} }
function onBusyChanged() { onConnectionAdded: {
if(!Controller.busy && roomManager.roomList === null) { if (Controller.accountCount === 1) {
roomManager.roomList = pageStack.replace(roomListComponent); roomManager.roomList = pageStack.replace(roomListComponent);
} }
} }
function onRoomJoined(roomId) { onConnectionDropped: {
const room = Controller.activeConnection.room(roomId);
return roomManager.enterRoom(room);
}
function onConnectionDropped() {
if (Controller.accountCount === 0) { if (Controller.accountCount === 0) {
pageStack.clear(); pageStack.clear();
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml"); pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml");
} }
} }
function onGlobalErrorOccured(error, detail) { onGlobalErrorOccured: showPassiveNotification(error + ": " + detail)
showPassiveNotification(error + ": " + detail)
}
function onShowWindow() { onShowWindow: root.showWindow()
root.showWindow()
}
function onOpenRoom(room) { function onOpenRoom(room) {
roomManager.enterRoom(room) roomManager.enterRoom(room)
@@ -302,13 +252,6 @@ Kirigami.ApplicationWindow {
} }
} }
Connections {
target: Controller.activeConnection
onDirectChatAvailable: {
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
}
}
Kirigami.OverlaySheet { Kirigami.OverlaySheet {
id: consentSheet id: consentSheet
@@ -331,47 +274,21 @@ Kirigami.ApplicationWindow {
} }
} }
RoomListModel {
id: spectralRoomListModel
connection: Controller.activeConnection
}
Component {
id: roomPage
RoomPage {}
}
Component { Component {
id: createRoomDialog id: createRoomDialog
CreateRoomDialog {} CreateRoomDialog {}
} }
Component {
id: roomWindow
RoomWindow {}
}
function handleLink(link, currentRoom) {
if (link.startsWith("https://matrix.to/")) {
var content = link.replace("https://matrix.to/#/", "").replace(/\?.*/, "")
if(content.match("^[#!]")) {
if(content.includes("/")) {
var result = content.match("([!#].*:.*)/(\\$.*)")
if(!result) {
return
}
if(result[1] == currentRoom.id) {
roomManager.roomItem.goToEvent(result[2])
} else {
roomManager.openRoomAndEvent(roomManager.roomByAliasOrId(result[1]), result[2])
}
} else {
roomManager.enterRoom(roomManager.roomByAliasOrId(content))
}
} else if(content.match("^@")) {
let dialog = userDialog.createObject(root.overlay, {room: currentRoom, user: currentRoom.user(content)})
dialog.open()
console.log(dialog.user)
}
} else {
Qt.openUrlExternally(link)
}
}
Component {
id: userDialog
UserDetailDialog {
}
}
} }

View File

@@ -1,4 +0,0 @@
[Material]
Primary=Blue
Accent=Blue
Theme=System

15
res.qrc
View File

@@ -2,18 +2,17 @@
<qresource prefix="/"> <qresource prefix="/">
<file>qml/main.qml</file> <file>qml/main.qml</file>
<file>imports/NeoChat/Page/qmldir</file> <file>imports/NeoChat/Page/qmldir</file>
<file>imports/NeoChat/Page/LoginPage.qml</file>
<file>imports/NeoChat/Page/LoadingPage.qml</file> <file>imports/NeoChat/Page/LoadingPage.qml</file>
<file>imports/NeoChat/Page/RoomListPage.qml</file> <file>imports/NeoChat/Page/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file> <file>imports/NeoChat/Page/RoomPage.qml</file>
<file>imports/NeoChat/Page/RoomWindow.qml</file>
<file>imports/NeoChat/Page/AccountsPage.qml</file> <file>imports/NeoChat/Page/AccountsPage.qml</file>
<file>imports/NeoChat/Page/JoinRoomPage.qml</file> <file>imports/NeoChat/Page/JoinRoomPage.qml</file>
<file>imports/NeoChat/Page/InviteUserPage.qml</file> <file>imports/NeoChat/Page/InviteUserPage.qml</file>
<file>imports/NeoChat/Page/SettingsPage.qml</file> <file>imports/NeoChat/Page/SettingsPage.qml</file>
<file>imports/NeoChat/Page/InvitationPage.qml</file>
<file>imports/NeoChat/Page/StartChatPage.qml</file> <file>imports/NeoChat/Page/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.qml</file> <file>imports/NeoChat/Page/ImageEditorPage.qml</file>
<file>imports/NeoChat/Page/DevicesPage.qml</file>
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/Component/qmldir</file> <file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/ChatTextInput.qml</file> <file>imports/NeoChat/Component/ChatTextInput.qml</file>
<file>imports/NeoChat/Component/AutoMouseArea.qml</file> <file>imports/NeoChat/Component/AutoMouseArea.qml</file>
@@ -32,15 +31,6 @@
<file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file> <file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file> <file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file> <file>imports/NeoChat/Component/Timeline/ImageDelegate.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>
<file>imports/NeoChat/Component/Login/Password.qml</file>
<file>imports/NeoChat/Component/Login/LoginRegister.qml</file>
<file>imports/NeoChat/Component/Login/Loading.qml</file>
<file>imports/NeoChat/Component/Login/Homeserver.qml</file>
<file>imports/NeoChat/Component/Login/LoginMethod.qml</file>
<file>imports/NeoChat/Component/Login/Sso.qml</file>
<file>imports/NeoChat/Setting/Setting.qml</file> <file>imports/NeoChat/Setting/Setting.qml</file>
<file>imports/NeoChat/Setting/qmldir</file> <file>imports/NeoChat/Setting/qmldir</file>
<file>imports/NeoChat/Setting/Palette.qml</file> <file>imports/NeoChat/Setting/Palette.qml</file>
@@ -59,6 +49,5 @@
<file>imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml</file> <file>imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml</file>
<file>imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml</file> <file>imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml</file>
<file>imports/NeoChat/Menu/RoomListContextMenu.qml</file> <file>imports/NeoChat/Menu/RoomListContextMenu.qml</file>
<file>qtquickcontrols2.conf</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -1,12 +1,10 @@
add_executable(neochat add_executable(neochat
accountlistmodel.cpp accountlistmodel.cpp
controller.cpp controller.cpp
actionshandler.cpp
emojimodel.cpp emojimodel.cpp
clipboard.cpp clipboard.cpp
matriximageprovider.cpp matriximageprovider.cpp
messageeventmodel.cpp messageeventmodel.cpp
messagefiltermodel.cpp
roomlistmodel.cpp roomlistmodel.cpp
neochatroom.cpp neochatroom.cpp
neochatuser.cpp neochatuser.cpp
@@ -18,29 +16,25 @@ add_executable(neochat
notificationsmanager.cpp notificationsmanager.cpp
sortfilterroomlistmodel.cpp sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp chatdocumenthandler.cpp
devicesmodel.cpp
filetypesingleton.cpp
login.cpp
stickerevent.cpp
../res.qrc ../res.qrc
) )
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID) if(NOT ANDROID)
target_sources(neochat PRIVATE trayicon.cpp) target_sources(neochat PRIVATE trayicon.cpp)
endif() endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR}) 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) target_link_libraries(neochat PRIVATE Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK) if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PRIVATE NEOCHAT_FLATPAK) target_compile_definitions(neochat PRIVATE NEOCHAT_FLATPAK)
endif() endif()
if (KQuickImageEditor_FOUND)
target_compile_definitions(neochat PRIVATE HAS_KQUICKIMAGEEDITOR)
endif()
if(ANDROID) if(ANDROID)
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL) target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS kirigami_package_breeze_icons(ICONS
@@ -74,17 +68,9 @@ if(ANDROID)
"search" "search"
"mail-replied-symbolic" "mail-replied-symbolic"
"edit-copy" "edit-copy"
"gtk-quit"
"compass"
"network-connect"
) )
else() else()
target_link_libraries(neochat PRIVATE Qt5::Widgets ${QTKEYCHAIN_LIBRARIES}) target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::DBusAddons ${QTKEYCHAIN_LIBRARIES})
endif()
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PRIVATE KF5::DBusAddons)
target_compile_definitions(neochat PRIVATE -DHAVE_KDBUSADDONS)
endif() endif()
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef ACCOUNTLISTMODEL_H
#pragma once #define ACCOUNTLISTMODEL_H
#include "controller.h" #include "controller.h"
@@ -30,3 +30,5 @@ public:
private: private:
QVector<Connection *> m_connections; QVector<Connection *> m_connections;
}; };
#endif // ACCOUNTLISTMODEL_H

View File

@@ -1,332 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPl-3.0-or-later
#include "actionshandler.h"
#include "csapi/joining.h"
#include <KLocalizedString>
#include <QStringBuilder>
#include <QDebug>
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
{
}
ActionsHandler::~ActionsHandler()
{};
NeoChatRoom *ActionsHandler::room() const
{
return m_room;
}
void ActionsHandler::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
Connection *ActionsHandler::connection() const
{
return m_connection;
}
void ActionsHandler::setConnection(Connection *connection)
{
if (m_connection == connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &Connection::directChatAvailable, nullptr, nullptr);
}
m_connection = connection;
if (m_connection != nullptr) {
connect(m_connection, &Connection::directChatAvailable,
this, [this](Quotient::Room *room) {
room->setDisplayed(true);
Q_EMIT roomJoined(room->id());
});
}
Q_EMIT connectionChanged();
}
QVariantList ActionsHandler::commands() const
{
QVariantList commands;
// Messages commands
commands.append({
QStringLiteral("prefix"), QStringLiteral("/shrug "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/lenny "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/plain "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends a message as plain text, without interpreting it as markdown")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/html "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends a message as html, without interpreting it as markdown")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/rainbow "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends the given message coloured as a rainbow")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/rainbowme "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends the given emote coloured as a rainbow")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/me "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Displays action")
});
// Actions commands
commands.append({
QStringLiteral("prefix"), QStringLiteral("/join "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<room-address>"),
QStringLiteral("help"), i18n("Joins room with given address")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/part "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "[<room-address>]"),
QStringLiteral("help"), i18n("Leave room")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/invite "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<user-id>"),
QStringLiteral("help"), i18n("Invites user with given id to current room")
});
// TODO more see elements /help action
return commands;
}
void ActionsHandler::joinRoom(const QString &alias)
{
if (!alias.contains(":")) {
Q_EMIT showMessage(MessageType::Error, i18n("The room id you are trying to join is not valid"));
return;
}
const auto knownServer = alias.mid(alias.indexOf(":") + 1);
auto joinRoomJob = m_connection->joinRoom(alias, QStringList{knownServer});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::failure, [=] {
Q_EMIT showMessage(MessageType::Error, i18n("Server error when joining the room \"%1\": %2",
joinRoomJob->errorString()));
});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::success, [this, joinRoomJob] {
Q_EMIT roomJoined(joinRoomJob->roomId());
});
}
void ActionsHandler::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT showMessage(MessageType::Error, i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
});
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::success, [=] {
Q_EMIT roomJoined(createRoomJob->roomId());
});
}
void ActionsHandler::postMessage(const QString &text,
const QString &attachementPath, const QString &replyEventId, const QString &editEventId,
const QVariantMap &usernames)
{
QString rawText = text;
QString cleanedText = text;
for (auto it = usernames.constBegin(); it != usernames.constEnd(); it++) {
cleanedText = cleanedText.replace(it.key(),
"[" + it.key() + "](https://matrix.to/#/" + it.value().toString() + ")");
}
if (attachementPath.length() > 0) {
m_room->uploadFile(attachementPath, cleanedText);
}
if (cleanedText.length() == 0) {
return;
}
auto messageEventType = RoomMessageEvent::MsgType::Text;
// Message commands
static const QString shrugPrefix = QStringLiteral("/shrug ");
static const QString lennyPrefix = QStringLiteral("/lenny ");
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 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 invitePrefix = QStringLiteral("/invite ");
static const QString joinPrefix = QStringLiteral("/join ");
static const QString partPrefix = QStringLiteral("/part");
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
// Admin commands
static QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500",
"#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b",
"#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff",
"#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff",
"#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
if (cleanedText.indexOf(shrugPrefix) == 0) {
cleanedText = QStringLiteral("¯\\\\_(ツ)\\_/¯") % cleanedText.remove(0, shrugPrefix.length());
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
return;
}
if (cleanedText.indexOf(lennyPrefix) == 0) {
cleanedText = QStringLiteral("( ͡° ͜ʖ ͡°)") % cleanedText.remove(0, lennyPrefix.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, rainbowText, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
return;
}
if (cleanedText.indexOf(rainbowmePrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowmePrefix.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, rainbowText, messageEventType, replyEventId, editEventId);
return;
}
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"));
return;
}
if (splittedText.count() > 1) {
joinRoom(splittedText[0] + ":" + splittedText[1]);
return;
}
else {
joinRoom(splittedText[0] + ":matrix.org");
}
return;
}
if (rawText.indexOf(invitePrefix) == 0) {
rawText = rawText.remove(0, invitePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
m_room->inviteToRoom(splittedText[0]);
return;
}
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
m_connection->leaveRoom(m_room);
return;
}
m_connection->leaveRoom(m_connection->room(splittedText[0]));
return;
}
if (rawText.indexOf(ignorePrefix) == 0) {
rawText = rawText.remove(0, ignorePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
if (m_connection->users().contains(splittedText[0])) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
const auto *user = m_connection->users()[splittedText[0]];
m_connection->addToIgnoredUsers(user);
return;
}
if (rawText.indexOf(unignorePrefix) == 0) {
rawText = rawText.remove(0, unignorePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
if (m_connection->users().contains(splittedText[0])) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
const auto *user = m_connection->users()[splittedText[0]];
m_connection->removeFromIgnoredUsers(user);
return;
}
if (cleanedText.indexOf(mePrefix) == 0) {
cleanedText = cleanedText.remove(0, mePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Emote;
} else if (cleanedText.indexOf(noticePrefix) == 0) {
cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice;
}
m_room->postMessage(rawText, cleanedText, messageEventType, replyEventId, editEventId);
}

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPl-3.0-or-later
#pragma once
#include <QObject>
#include "connection.h"
#include "neochatroom.h"
using namespace Quotient;
/// \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 List of command definition. Useful for building an autocompletion
/// engine or an help dialog.
Q_PROPERTY(QVariantList commands READ commands CONSTANT)
/// \brief The connection that will handle sending the message.
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/// \brief The connection that will handle sending the message.
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
enum MessageType {
Info,
Error,
};
Q_ENUM(MessageType);
explicit ActionsHandler(QObject *parent = nullptr);
~ActionsHandler();
QVariantList commands() const;
[[nodiscard]] Connection *connection() const;
void setConnection(Connection *connection);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
/// \brief Show error or information message.
///
/// These messages will be displayed in the room view header.
void showMessage(MessageType messageType, QString message);
/// \brief Emitted when an action made the user join a room.
///
/// Either when a new room was created, a direct chat was started
/// or a group chat was joined. The UI will react to this signal
/// and switch to the newly joined room.
void roomJoined(QString roomName);
void roomChanged();
void connectionChanged();
public Q_SLOTS:
/// \brief Create new room for a group chat.
void createRoom(const QString &name, const QString &topic);
/// \brief Join a room.
void joinRoom(const QString &alias);
/// \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);
private:
Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
};

View File

@@ -126,7 +126,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
if (cursor.block().text() == m_lastState) { if (cursor.block().text() == m_lastState) {
// ignore change, it was caused by autocompletion // ignore change, it was caused by autocompletion
return QVariantMap{ return QVariantMap {
{"type", AutoCompletionType::Ignore}, {"type", AutoCompletionType::Ignore},
}; };
} }
@@ -141,7 +141,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
QString autoCompletePrefix = textBeforeCursor.section(" ", -1); QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
if (autoCompletePrefix.isEmpty()) { if (autoCompletePrefix.isEmpty()) {
return QVariantMap{ return QVariantMap {
{"type", AutoCompletionType::None}, {"type", AutoCompletionType::None},
}; };
} }
@@ -151,22 +151,77 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
if (autoCompletePrefix.startsWith("@")) { if (autoCompletePrefix.startsWith("@")) {
autoCompletePrefix.remove(0, 1); autoCompletePrefix.remove(0, 1);
return QVariantMap{ return QVariantMap {
{"keyword", autoCompletePrefix}, {"keyword", autoCompletePrefix},
{"type", AutoCompletionType::User}, {"type", AutoCompletionType::User},
}; };
} }
return QVariantMap{ return QVariantMap {
{"keyword", autoCompletePrefix}, {"keyword", autoCompletePrefix},
{"type", AutoCompletionType::Emoji}, {"type", AutoCompletionType::Emoji},
}; };
} }
return QVariantMap{ return QVariantMap {
{"type", AutoCompletionType::None}, {"type", AutoCompletionType::None},
}; };
} }
void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath,
const QString &replyEventId, const QVariantMap usernames) const
{
if (!m_room || !m_document) {
return;
}
QString cleanedText = text;
cleanedText = cleanedText.trimmed();
for (const auto username : usernames.keys()) {
const auto replacement = usernames.value(username);
cleanedText = cleanedText.replace(username,
"[" + username + "](https://matrix.to/#/" + replacement.toString() + ")");
}
if (attachementPath.length() > 0) {
m_room->uploadFile(attachementPath, cleanedText);
}
if (cleanedText.length() == 0) {
return;
}
auto messageEventType = RoomMessageEvent::MsgType::Text;
const QString rainbowPrefix = QStringLiteral("/rainbow ");
const QString mePrefix = QStringLiteral("/me ");
const QString noticePrefix = QStringLiteral("/notice ");
if (cleanedText.indexOf(rainbowPrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
QString rainbowText;
QStringList rainbowColors {"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff",
"#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
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(text, rainbowText, messageEventType, replyEventId);
return;
}
if (cleanedText.indexOf(mePrefix) == 0) {
cleanedText = cleanedText.remove(0, mePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Emote;
} else if (cleanedText.indexOf(noticePrefix) == 0) {
cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice;
}
m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId);
}
void ChatDocumentHandler::replaceAutoComplete(const QString &word) void ChatDocumentHandler::replaceAutoComplete(const QString &word)
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = textCursor();

View File

@@ -14,7 +14,6 @@
class QTextDocument; class QTextDocument;
class QQuickTextDocument; class QQuickTextDocument;
class NeoChatRoom; class NeoChatRoom;
class Controller;
class ChatDocumentHandler : public QObject class ChatDocumentHandler : public QObject
{ {
@@ -52,8 +51,10 @@ public:
[[nodiscard]] NeoChatRoom *room() const; [[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QVariantMap usernames) const;
/// This function will look at the current QTextCursor and determine if there /// This function will look at the current QTextCursor and determine if there
/// is the possibility to autocomplete it. /// is the posibility to autocomplete it.
Q_INVOKABLE QVariantMap getAutocompletionInfo(); Q_INVOKABLE QVariantMap getAutocompletionInfo();
Q_INVOKABLE void replaceAutoComplete(const QString &word); Q_INVOKABLE void replaceAutoComplete(const QString &word);
@@ -63,7 +64,6 @@ Q_SIGNALS:
void selectionStartChanged(); void selectionStartChanged();
void selectionEndChanged(); void selectionEndChanged();
void roomChanged(); void roomChanged();
void joinRoom(QString roomName);
private: private:
[[nodiscard]] QTextCursor textCursor() const; [[nodiscard]] QTextCursor textCursor() const;

View File

@@ -57,7 +57,7 @@ bool Clipboard::saveImage(const QUrl &localPath) const
void Clipboard::saveText(QString message) void Clipboard::saveText(QString message)
{ {
QRegularExpression re("<[^>]*>"); QRegularExpression re("<[^>]*>");
auto *mineData = new QMimeData; // ownership is transferred to clipboard auto *mineData = new QMimeData; // ownership is transfered to clipboard
mineData->setHtml(message); mineData->setHtml(message);
mineData->setText(message.replace(re, "")); mineData->setText(message.replace(re, ""));
m_clipboard->setMimeData(mineData); m_clipboard->setMimeData(mineData);

View File

@@ -3,7 +3,6 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#pragma once #pragma once
#include <QObject> #include <QObject>

View File

@@ -8,42 +8,43 @@
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
#include <qt5keychain/keychain.h> #include <qt5keychain/keychain.h>
#endif #else
#include <KConfig> #include <KConfig>
#include <KConfigGroup> #include <KConfigGroup>
#include <KWindowConfig> #endif
#include <KLocalizedString> #include <KLocalizedString>
#include <QClipboard> #include <QClipboard>
#include <QDebug> #include <QDebug>
#include <QQuickWindow>
#include <QDir> #include <QDir>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QStandardPaths> #include <QStandardPaths>
#include <QStringBuilder>
#include <QSysInfo> #include <QSysInfo>
#include <QTimer> #include <QTimer>
#include <QCloseEvent> #include <QtGui/QCloseEvent>
#include <QDesktopServices> #include <QtGui/QDesktopServices>
#include <QMovie> #include <QtGui/QMovie>
#include <QPixmap> #include <QtGui/QPixmap>
#include <QAuthenticator> #include <QtNetwork/QAuthenticator>
#include <QNetworkReply> #include <QtNetwork/QNetworkReply>
#include <QStringBuilder>
#include <utility> #include <utility>
#include "csapi/account-data.h" #include "csapi/account-data.h"
#include "csapi/content-repo.h" #include "csapi/content-repo.h"
#include "csapi/joining.h"
#include "csapi/logout.h" #include "csapi/logout.h"
#include "csapi/profile.h" #include "csapi/profile.h"
#include "csapi/registration.h" #include "csapi/registration.h"
#include "csapi/wellknown.h" #include "csapi/wellknown.h"
#include "events/eventcontent.h" #include "events/eventcontent.h"
#include "events/roommessageevent.h" #include "events/roommessageevent.h"
#include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "neochatconfig.h"
#include "settings.h" #include "settings.h"
#include "utils.h" #include "utils.h"
#include <KStandardShortcut> #include <KStandardShortcut>
@@ -55,26 +56,16 @@
Controller::Controller(QObject *parent) Controller::Controller(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
QApplication::setQuitOnLastWindowClosed(false);
Connection::setRoomType<NeoChatRoom>(); Connection::setRoomType<NeoChatRoom>();
Connection::setUserType<NeoChatUser>(); Connection::setUserType<NeoChatUser>();
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
TrayIcon *trayIcon = new TrayIcon(this); TrayIcon *trayIcon = new TrayIcon(this);
if(NeoChatConfig::self()->systemTray()) { connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
trayIcon->show(); trayIcon->setIconSource("org.kde.neochat");
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow); trayIcon->setIsOnline(true);
QApplication::setQuitOnLastWindowClosed(false);
}
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [=](){
if(NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
trayIcon->hide();
disconnect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
}
QApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
});
#endif #endif
QTimer::singleShot(0, this, [=] { QTimer::singleShot(0, this, [=] {
@@ -102,6 +93,51 @@ inline QString accessTokenFileName(const AccountSettings &account)
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName; return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName;
} }
void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName)
{
if (user.isEmpty() || pass.isEmpty()) {
return;
}
if (deviceName.isEmpty()) {
deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture();
}
auto conn = new Connection(this);
const QUrl serverUrl = QUrl::fromUserInput(serverAddr);
// we are using a fake mixd since resolveServer just set the homeserver url :sigh:
conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443)));
connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() {
conn->loginWithPassword(user, pass, deviceName, "");
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
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);
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
connect(conn, &Connection::loginError, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed"), std::move(error));
});
});
connect(conn, &Connection::resolveError, this, [=](QString error) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
}
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName) void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
{ {
if (user.isEmpty() || token.isEmpty()) { if (user.isEmpty() || token.isEmpty()) {
@@ -224,7 +260,7 @@ void Controller::invokeLogin()
const auto accounts = SettingsGroup("Accounts").childGroups(); const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection(); QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) { for (const auto &accountId : accounts) {
AccountSettings account{accountId}; AccountSettings account {accountId};
if (id.isEmpty()) { if (id.isEmpty()) {
// handle case where the account config is empty // handle case where the account config is empty
id = accountId; id = accountId;
@@ -249,7 +285,6 @@ void Controller::invokeLogin()
Q_EMIT errorOccured(i18n("Login Failed"), error); Q_EMIT errorOccured(i18n("Login Failed"), error);
logout(connection, true); logout(connection, true);
} }
Q_EMIT initiated();
}); });
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) { connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured("Network Error", error); Q_EMIT errorOccured("Network Error", error);
@@ -257,14 +292,14 @@ void Controller::invokeLogin()
connection->connectWithToken(account.userId(), accessToken, account.deviceId()); connection->connectWithToken(account.userId(), accessToken, account.deviceId());
} }
} }
if (accounts.isEmpty()) { if (m_connections.isEmpty()) {
Q_EMIT initiated(); Q_EMIT initiated();
} }
} }
QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account) QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
{ {
QFile accountTokenFile{accessTokenFileName(account)}; QFile accountTokenFile {accessTokenFileName(account)};
if (accountTokenFile.open(QFile::ReadOnly)) { if (accountTokenFile.open(QFile::ReadOnly)) {
if (accountTokenFile.size() < 1024) { if (accountTokenFile.size() < 1024) {
return accountTokenFile.readAll(); return accountTokenFile.readAll();
@@ -302,7 +337,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool removed = false; bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken); bool saved = saveAccessTokenToKeyChain(account, accessToken);
if (saved) { if (saved) {
QFile accountTokenFile{accessTokenFileName(account)}; QFile accountTokenFile {accessTokenFileName(account)};
removed = accountTokenFile.remove(); removed = accountTokenFile.remove();
} }
if (!(saved && removed)) { if (!(saved && removed)) {
@@ -325,7 +360,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken) bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken)
{ {
// (Re-)Make a dedicated file for access_token. // (Re-)Make a dedicated file for access_token.
QFile accountTokenFile{accessTokenFileName(account)}; QFile accountTokenFile {accessTokenFileName(account)};
accountTokenFile.remove(); // Just in case accountTokenFile.remove(); // Just in case
auto fileDir = QFileInfo(accountTokenFile).dir(); auto fileDir = QFileInfo(accountTokenFile).dir();
@@ -364,6 +399,34 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true; return true;
} }
void Controller::joinRoom(Connection *c, const QString &alias)
{
if (!alias.contains(":")) {
return;
}
auto knownServer = alias.mid(alias.indexOf(":") + 1);
auto joinRoomJob = c->joinRoom(alias, QStringList {knownServer});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::failure, [=] {
Q_EMIT errorOccured("Join Room Failed", joinRoomJob->errorString());
});
}
void Controller::createRoom(Connection *c, const QString &name, const QString &topic)
{
auto createRoomJob = c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT errorOccured("Create Room Failed", createRoomJob->errorString());
});
}
void Controller::createDirectChat(Connection *c, const QString &userID)
{
auto createRoomJob = c->createDirectChat(userID);
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT errorOccured("Create Direct Chat Failed", createRoomJob->errorString());
});
}
void Controller::playAudio(const QUrl &localFile) void Controller::playAudio(const QUrl &localFile)
{ {
@@ -502,21 +565,7 @@ void Controller::setActiveConnection(Connection *connection)
Q_EMIT activeConnectionChanged(); Q_EMIT activeConnectionChanged();
} }
void Controller::saveWindowGeometry(QQuickWindow *window) QList<QKeySequence> Controller::preferencesShortcuts() const
{ {
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation); return KStandardShortcut::preferences();
KConfigGroup windowGroup(&dataResource, "Window");
KWindowConfig::saveWindowPosition(window, windowGroup);
KWindowConfig::saveWindowSize(window, windowGroup);
dataResource.sync();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
} }

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef CONTROLLER_H
#pragma once #define CONTROLLER_H
#include <QApplication> #include <QApplication>
#include <QMediaPlayer> #include <QMediaPlayer>
@@ -21,7 +21,6 @@ class QKeySequences;
#include "user.h" #include "user.h"
class NeoChatRoom; class NeoChatRoom;
class QQuickWindow;
using namespace Quotient; using namespace Quotient;
@@ -34,6 +33,9 @@ class Controller : public QObject
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged) Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
Q_PROPERTY(KAboutData aboutData READ aboutData WRITE setAboutData NOTIFY aboutDataChanged) Q_PROPERTY(KAboutData aboutData READ aboutData WRITE setAboutData NOTIFY aboutDataChanged)
/// Get the list of shortcuts activating the preferences page
Q_PROPERTY(QList<QKeySequence> preferencesShortcuts READ preferencesShortcuts CONSTANT)
public: public:
static Controller &instance(); static Controller &instance();
@@ -45,12 +47,15 @@ public:
void addConnection(Connection *c); void addConnection(Connection *c);
void dropConnection(Connection *c); void dropConnection(Connection *c);
Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString);
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &); Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword); Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
[[nodiscard]] int accountCount() const; [[nodiscard]] int accountCount() const;
[[nodiscard]] QList<QKeySequence> preferencesShortcuts() const;
[[nodiscard]] static bool quitOnLastWindowClosed(); [[nodiscard]] static bool quitOnLastWindowClosed();
void setQuitOnLastWindowClosed(bool value); void setQuitOnLastWindowClosed(bool value);
@@ -60,9 +65,6 @@ public:
void setAboutData(const KAboutData &aboutData); void setAboutData(const KAboutData &aboutData);
[[nodiscard]] KAboutData aboutData() const; [[nodiscard]] KAboutData aboutData() const;
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
enum PasswordStatus { enum PasswordStatus {
Success, Success,
Wrong, Wrong,
@@ -81,6 +83,8 @@ private:
static QByteArray loadAccessTokenFromFile(const AccountSettings &account); static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account); QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
void loadSettings(); void loadSettings();
void saveSettings() const; void saveSettings() const;
@@ -91,10 +95,10 @@ private Q_SLOTS:
Q_SIGNALS: Q_SIGNALS:
void busyChanged(); void busyChanged();
/// Error occurred because of user inputs /// Error occured because of user inputs
void errorOccured(QString error, QString detail); void errorOccured(QString error, QString detail);
/// Error occurred because of server or bug in NeoChat /// Error occured because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail); void globalErrorOccured(QString error, QString detail);
void syncDone(); void syncDone();
void connectionAdded(Quotient::Connection *_t1); void connectionAdded(Quotient::Connection *_t1);
@@ -110,14 +114,15 @@ Q_SIGNALS:
void showWindow(); void showWindow();
void openRoom(NeoChatRoom *room); void openRoom(NeoChatRoom *room);
void userConsentRequired(QUrl url); void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
public Q_SLOTS: public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout); void logout(Quotient::Connection *conn, bool serverSideLogout);
void joinRoom(Quotient::Connection *c, const QString &alias);
void createRoom(Quotient::Connection *c, const QString &name, const QString &topic);
void createDirectChat(Quotient::Connection *c, const QString &userID);
static void playAudio(const QUrl &localFile); static void playAudio(const QUrl &localFile);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile); void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn); static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry(QQuickWindow *);
}; };
// TODO libQuotient 0.7: Drop // TODO libQuotient 0.7: Drop
@@ -127,8 +132,4 @@ public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth = none); explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth = none);
}; };
class NeochatDeleteDeviceJob : public BaseJob #endif // CONTROLLER_H
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth = none);
};

View File

@@ -1,88 +0,0 @@
/**
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
*
* SPDX-LicenseIdentifier: GPL-2.0-or-later
*/
#include "devicesmodel.h"
#include <csapi/device_management.h>
#include "controller.h"
DevicesModel::DevicesModel(QObject *parent)
: QAbstractListModel(parent)
{
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 QVariant();
switch (role) {
case Id:
return m_devices[index.row()].deviceId;
case DisplayName:
return m_devices[index.row()].displayName;
case LastIp:
return m_devices[index.row()].lastSeenIp;
case LastTimestamp:
if (m_devices[index.row()].lastSeenTs)
return *m_devices[index.row()].lastSeenTs;
}
return QVariant();
}
int DevicesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_devices.size();
}
QHash<int, QByteArray> DevicesModel::roleNames() const
{
return {{Id, "id"}, {DisplayName, "displayName"}, {LastIp, "lastIp"}, {LastTimestamp, "lastTimestamp"}};
}
void DevicesModel::logout(int index, const QString &password)
{
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
if (job->error() != 0) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
authData["password"] = password;
authData["type"] = "m.login.password";
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, [this, index]() {
Q_EMIT beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
Q_EMIT endRemoveRows();
});
}
});
}
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;
Q_EMIT beginResetModel();
m_devices[index].displayName = name;
Q_EMIT endResetModel();
connect(job, &BaseJob::failure, this, [=]() {
Q_EMIT beginResetModel();
m_devices[index].displayName = oldName;
Q_EMIT endResetModel();
});
}

View File

@@ -1,39 +0,0 @@
/**
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
*
* SPDX-LicenseIdentifier: GPL-2.0-or-later
*/
#pragma once
#include <QAbstractListModel>
#include <csapi/definitions/client_device.h>
using namespace Quotient;
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE void logout(int index, const QString &password);
Q_INVOKABLE void setName(int index, const QString &name);
private:
QVector<Quotient::Device> m_devices;
};

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef EMOJIMODEL_H
#pragma once #define EMOJIMODEL_H
#include <QObject> #include <QObject>
#include <QSettings> #include <QSettings>
@@ -86,3 +86,5 @@ private:
QSettings m_settings; QSettings m_settings;
}; };
#endif // EMOJIMODEL_H

View File

@@ -1,114 +0,0 @@
/* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
*/
#include "filetypesingleton.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for(const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypeSingletonPrivate
{
Q_DECLARE_PUBLIC(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingletonPrivate)
public:
FileTypeSingletonPrivate(FileTypeSingleton *qq);
FileTypeSingleton * const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypeSingletonPrivate::FileTypeSingletonPrivate(FileTypeSingleton* qq) : q_ptr(qq)
{
}
FileTypeSingleton::FileTypeSingleton(QObject* parent)
: QObject(parent)
, d_ptr(new FileTypeSingletonPrivate(this))
{
}
FileTypeSingleton::~FileTypeSingleton() noexcept
{
}
QMimeType FileTypeSingleton::mimeTypeForName(const QString& nameOrAlias) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QString& fileName, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QFileInfo& fileInfo, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileTypeSingleton::mimeTypesForFileName(const QString& fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileTypeSingleton::mimeTypeForData(const QByteArray& data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileTypeSingleton::mimeTypeForData(QIODevice* device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileTypeSingleton::mimeTypeForUrl(const QUrl& url) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString& fileName, QIODevice* device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString& fileName, const QByteArray& data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileTypeSingleton::suffixForFileName(const QString& fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileTypeSingleton::supportedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedImageFormats;
}
QStringList FileTypeSingleton::supportedAnimatedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedAnimatedImageFormats;
}

View File

@@ -1,60 +0,0 @@
/* SPDX-FileCopyrightText: 2015 Klaralvdalens Datakonsult AB
* SPDX-FileCopyrightText: 2016 The Qt Company Ltd.
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <qqml.h>
#include <QMimeDatabase>
class FileTypeSingletonPrivate;
class FileTypeSingleton : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList supportedImageFormats READ supportedImageFormats CONSTANT FINAL)
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
QML_NAMED_ELEMENT(FileType)
QML_SINGLETON
public:
explicit FileTypeSingleton(QObject *parent = nullptr);
~FileTypeSingleton();
// Most of the code in this public section was copy/pasted from qmimedatabase.h
Q_INVOKABLE QMimeType mimeTypeForName(const QString &nameOrAlias) const;
enum MatchMode {
MatchDefault,
MatchExtension,
MatchContent
};
Q_ENUM(MatchMode)
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;
Q_INVOKABLE QMimeType mimeTypeForData(QIODevice *device) const;
Q_INVOKABLE QMimeType mimeTypeForUrl(const QUrl &url) const;
Q_INVOKABLE QMimeType mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const;
Q_INVOKABLE QMimeType mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const;
Q_INVOKABLE QString suffixForFileName(const QString &fileName) const;
// These return a list of file extensions, not mimetypes
QStringList supportedImageFormats() const;
QStringList supportedAnimatedImageFormats() const;
private:
const QScopedPointer<FileTypeSingletonPrivate> d_ptr;
Q_DECLARE_PRIVATE(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingleton)
};
QML_DECLARE_TYPE(FileTypeSingleton)

View File

@@ -1,206 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "login.h"
#include "connection.h"
#include "controller.h"
#include <QUrl>
#include <KLocalizedString>
Login::Login(QObject *parent)
: QObject(parent)
{
init();
}
void Login::init()
{
m_homeserverReachable = false;
m_connection = nullptr;
m_matrixId = QString();
m_password = QString();
m_deviceName = QString();
m_supportsSso = false;
m_supportsPassword = false;
m_ssoUrl = QUrl();
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();
m_connection = new Connection(this);
m_connection->resolveServer(m_matrixId);
connect(m_connection, &Connection::loginFlowsChanged, this, [=](){
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
});
});
}
void Login::setHomeserverReachable(bool reachable)
{
m_homeserverReachable = reachable;
Q_EMIT homeserverReachableChanged();
}
bool Login::homeserverReachable() const
{
return m_homeserverReachable;
}
QString Login::matrixId() const
{
return m_matrixId;
}
void Login::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if(!m_matrixId.startsWith('@')) {
m_matrixId.prepend('@');
}
Q_EMIT matrixIdChanged();
}
QString Login::password() const
{
return m_password;
}
void Login::setPassword(const QString &password)
{
m_password = password;
Q_EMIT passwordChanged();
}
QString Login::deviceName() const
{
return m_deviceName;
}
void Login::setDeviceName(const QString &deviceName)
{
m_deviceName = deviceName;
Q_EMIT deviceNameChanged();
}
void Login::login()
{
m_isLoggingIn = true;
Q_EMIT isLoggingInChanged();
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
{
return m_supportsPassword;
}
bool Login::supportsSso() const
{
return m_supportsSso;
}
QUrl Login::ssoUrl() const
{
return m_ssoUrl;
}
void Login::loginWithSso()
{
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
{
return m_testing;
}
bool Login::isLoggingIn() const
{
return m_isLoggingIn;
}

View File

@@ -1,85 +0,0 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QObject>
#include "csapi/wellknown.h"
#include "connection.h"
using namespace Quotient;
class Login : public QObject
{
Q_OBJECT
Q_PROPERTY(bool homeserverReachable READ homeserverReachable NOTIFY homeserverReachableChanged)
Q_PROPERTY(bool testing READ testing NOTIFY testingChanged)
Q_PROPERTY(QString matrixId READ matrixId WRITE setMatrixId NOTIFY matrixIdChanged)
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(bool supportsPassword READ supportsPassword NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(QUrl ssoUrl READ ssoUrl NOTIFY ssoUrlChanged)
Q_PROPERTY(bool isLoggingIn READ isLoggingIn NOTIFY isLoggingInChanged)
public:
explicit Login(QObject *parent = nullptr);
Q_INVOKABLE void init();
bool homeserverReachable() const;
QString matrixId() const;
void setMatrixId(const QString &matrixId);
QString password() const;
void setPassword(const QString &password);
QString deviceName() const;
void setDeviceName(const QString &deviceName);
bool supportsPassword() const;
bool supportsSso() const;
bool testing() const;
QUrl ssoUrl() const;
bool isLoggingIn() const;
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
Q_SIGNALS:
void homeserverReachableChanged();
void testHomeserverFinished();
void matrixIdChanged();
void passwordChanged();
void deviceNameChanged();
void initialSyncFinished();
void loginFlowsChanged();
void ssoUrlChanged();
void connected();
void errorOccured(QString message);
void testingChanged();
void isLoggingInChanged();
private:
void setHomeserverReachable(bool reachable);
bool m_homeserverReachable;
QString m_matrixId;
QString m_password;
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
Connection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing;
bool m_isLoggingIn = false;
};

View File

@@ -14,12 +14,11 @@
#include <QQuickWindow> #include <QQuickWindow>
#include <KAboutData> #include <KAboutData>
#ifdef HAVE_KDBUSADDONS #ifndef Q_OS_ANDROID
#include <KDBusService> #include <KDBusService>
#endif #endif
#include <KLocalizedContext> #include <KLocalizedContext>
#include <KLocalizedString> #include <KLocalizedString>
#include <KWindowConfig>
#include "neochat-version.h" #include "neochat-version.h"
@@ -29,13 +28,9 @@
#include "controller.h" #include "controller.h"
#include "csapi/joining.h" #include "csapi/joining.h"
#include "csapi/leaving.h" #include "csapi/leaving.h"
#include "devicesmodel.h"
#include "emojimodel.h" #include "emojimodel.h"
#include "filetypesingleton.h"
#include "login.h"
#include "matriximageprovider.h" #include "matriximageprovider.h"
#include "messageeventmodel.h" #include "messageeventmodel.h"
#include "messagefiltermodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "neochatuser.h" #include "neochatuser.h"
@@ -46,7 +41,6 @@
#include "sortfilterroomlistmodel.h" #include "sortfilterroomlistmodel.h"
#include "userdirectorylistmodel.h" #include "userdirectorylistmodel.h"
#include "userlistmodel.h" #include "userlistmodel.h"
#include "actionshandler.h"
using namespace Quotient; using namespace Quotient;
@@ -70,10 +64,6 @@ int main(int argc, char *argv[])
} }
#endif #endif
#ifdef Q_OS_WINDOWS
QApplication::setStyle(QStringLiteral("breeze"));
#endif
QApplication::setOrganizationName("KDE"); QApplication::setOrganizationName("KDE");
KAboutData about(QStringLiteral("neochat"), i18n("Neochat"), QStringLiteral(NEOCHAT_VERSION_STRING), i18n("Matrix client"), KAboutLicense::GPL_V3, i18n("© 2018-2020 Black Hat, 2020 KDE Community")); KAboutData about(QStringLiteral("neochat"), i18n("Neochat"), QStringLiteral(NEOCHAT_VERSION_STRING), i18n("Matrix client"), KAboutLicense::GPL_V3, i18n("© 2018-2020 Black Hat, 2020 KDE Community"));
@@ -85,39 +75,32 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about); KAboutData::setApplicationData(about);
QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat"))); QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
#ifdef HAVE_KDBUSADDONS #ifndef Q_OS_ANDROID
KDBusService service(KDBusService::Unique); KDBusService service(KDBusService::Unique);
#endif #endif
#ifdef NEOCHAT_FLATPAK #ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the // Copy over the included FontConfig configuration to the
// app's config dir: // app's config dir:
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"); QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf",
"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
#endif #endif
Clipboard clipboard; Clipboard clipboard;
auto config = NeoChatConfig::self(); auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
Login *login = new Login();
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel"); 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<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel"); qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel"); qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel"); 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<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel"); qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel"); qmlRegisterType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel");
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel"); 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<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM"); qmlRegisterUncreatableType<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM");
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM"); qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
@@ -131,7 +114,6 @@ int main(int argc, char *argv[])
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*"); qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*"); qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*"); qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
qRegisterMetaTypeStreamOperators<Emoji>(); qRegisterMetaTypeStreamOperators<Emoji>();
@@ -157,7 +139,7 @@ int main(int argc, char *argv[])
return -1; return -1;
} }
#ifdef HAVE_KDBUSADDONS #ifndef Q_OS_ANDROID
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) { QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
const auto rootObjects = engine.rootObjects(); const auto rootObjects = engine.rootObjects();
for (auto obj : rootObjects) { for (auto obj : rootObjects) {
@@ -169,17 +151,6 @@ int main(int argc, char *argv[])
} }
} }
}); });
const auto rootObjects = engine.rootObjects();
for (auto obj : rootObjects) {
auto view = qobject_cast<QQuickWindow*>(obj);
if (view) {
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup windowGroup(&dataResource, "Window");
KWindowConfig::restoreWindowSize(view, windowGroup);
KWindowConfig::restoreWindowPosition(view, windowGroup);
break;
}
}
#endif #endif
return QApplication::exec(); return QApplication::exec();
} }

View File

@@ -4,7 +4,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef MatrixImageProvider_H
#define MatrixImageProvider_H
#pragma once #pragma once
#include <QQuickAsyncImageProvider> #include <QQuickAsyncImageProvider>
@@ -52,3 +53,5 @@ class MatrixImageProvider : public QQuickAsyncImageProvider
public: public:
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
}; };
#endif // MatrixImageProvider_H

View File

@@ -14,8 +14,6 @@
#include <events/simplestateevents.h> #include <events/simplestateevents.h>
#include <user.h> #include <user.h>
#include "stickerevent.h"
#include <QDebug> #include <QDebug>
#include <QQmlEngine> // for qmlRegisterType() #include <QQmlEngine> // for qmlRegisterType()
#include <QTimeZone> #include <QTimeZone>
@@ -47,7 +45,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowSectionRole] = "showSection"; roles[ShowSectionRole] = "showSection";
roles[ReactionRole] = "reaction"; roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited"; roles[IsEditedRole] = "isEdited";
roles[FormattedBodyRole] = "formattedBody";
return roles; return roles;
} }
@@ -65,8 +62,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
return; return;
} }
m_currentRoom->getPreviousContent(50); m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [=]() { connect(this, &QAbstractListModel::rowsInserted, this, [=](){
if (m_currentRoom->readMarkerEventId().isEmpty()) { if(m_currentRoom->readMarkerEventId().isEmpty()) {
return; return;
} }
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId()); const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
@@ -302,7 +299,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt) inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
{ {
Q_UNUSED(evt) Q_UNUSED(evt)
return QVariantMap{ return QVariantMap {
{"isLocalUser", user->id() == room->localUser()->id()}, {"isLocalUser", user->id() == room->localUser()->id()},
{"id", user->id()}, {"id", user->id()},
{"avatarMediaId", user->avatarMediaId(room)}, {"avatarMediaId", user->avatarMediaId(room)},
@@ -332,20 +329,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
auto reason = evt.redactedBecause()->reason(); auto reason = evt.redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>").arg(evt.redactedBecause()->reason()); return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>").arg(evt.redactedBecause()->reason());
} }
return m_currentRoom->eventToString(evt, Qt::RichText); return m_currentRoom->eventToString(evt, Qt::RichText);
} }
if (role == FormattedBodyRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
return static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
}
}
return {};
}
if (role == MessageRole) { if (role == MessageRole) {
return m_currentRoom->eventToString(evt); return m_currentRoom->eventToString(evt);
} }
@@ -376,9 +362,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return "message"; return "message";
} }
if (is<const StickerEvent>(evt)) {
return "sticker";
}
if (evt.isStateEvent()) { if (evt.isStateEvent()) {
return "state"; return "state";
} }
@@ -415,10 +398,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// content JSON stored in EventContent::Base // content JSON stored in EventContent::Base
return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
}; };
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(e->image().originalJson);
}
} }
if (role == HighlightRole) { if (role == HighlightRole) {
@@ -487,9 +466,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id())); return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
} }
} }
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
}
} }
if (role == AnnotationRole) { if (role == AnnotationRole) {
@@ -526,7 +502,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}; };
const auto &replyEvt = **replyIt; const auto &replyEvt = **replyIt;
return QVariantMap{{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}}; return QVariantMap {{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
} }
if (role == ShowAuthorRole) { if (role == ShowAuthorRole) {
@@ -580,7 +556,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
authors.append(userAtEvent(author, m_currentRoom, evt)); authors.append(userAtEvent(author, m_currentRoom, evt));
} }
bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser())); bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser()));
res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}}); res.append(QVariantMap {{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
++i; ++i;
} }

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef MESSAGEEVENTMODEL_H
#pragma once #define MESSAGEEVENTMODEL_H
#include <QAbstractListModel> #include <QAbstractListModel>
@@ -32,7 +32,6 @@ public:
LongOperationRole, LongOperationRole,
AnnotationRole, AnnotationRole,
UserMarkerRole, UserMarkerRole,
FormattedBodyRole,
ReplyRole, ReplyRole,
@@ -40,7 +39,6 @@ public:
ShowSectionRole, ShowSectionRole,
ReactionRole, ReactionRole,
IsEditedRole, IsEditedRole,
// For debugging // For debugging
@@ -91,3 +89,5 @@ private:
Q_SIGNALS: Q_SIGNALS:
void roomChanged(); void roomChanged();
}; };
#endif // MESSAGEEVENTMODEL_H

View File

@@ -1,32 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "messagefiltermodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
const QString eventType = index.data(MessageEventModel::EventTypeRole).toString();
if (eventType == QLatin1String("other")) {
return false;
}
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == QLatin1String("state")) {
return false;
}
return true;
}

View File

@@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QSortFilterProxyModel>
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};

View File

@@ -32,18 +32,6 @@
<label>Show avatar in the timeline</label> <label>Show avatar in the timeline</label>
<default>true</default> <default>true</default>
</entry> </entry>
<entry name="ShowRename" type="bool">
<label>Show rename events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowAvatarUpdate" type="bool">
<label>Show avatar update events in the timeline</label>
<default>true</default>
</entry>
<entry name="SystemTray" type="bool">
<label>Close NeoChat to system tray</label>
<default>true</default>
</entry>
</group> </group>
</kcfg> </kcfg>

View File

@@ -27,13 +27,12 @@
#include "events/roomcanonicalaliasevent.h" #include "events/roomcanonicalaliasevent.h"
#include "events/roommessageevent.h" #include "events/roommessageevent.h"
#include "events/roompowerlevelsevent.h" #include "events/roompowerlevelsevent.h"
#include "stickerevent.h"
#include "events/typingevent.h" #include "events/typingevent.h"
#include "jobs/downloadfilejob.h" #include "jobs/downloadfilejob.h"
#include "neochatconfig.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "user.h" #include "user.h"
#include "utils.h" #include "utils.h"
#include "neochatconfig.h"
#include <KLocalizedString> #include <KLocalizedString>
@@ -51,7 +50,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return; return;
} }
const RoomEvent *lastEvent = messageEvents().rbegin()->get(); const RoomEvent *lastEvent = messageEvents().rbegin()->get();
if (lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) { if(lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
return; return;
} }
if (lastEvent->isStateEvent()) { if (lastEvent->isStateEvent()) {
@@ -62,25 +61,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return; return;
} }
QImage avatar_image; NotificationsManager::instance().postNotification(this, lastEvent->id(), displayName(), sender->displayname(this), eventToString(*lastEvent), avatar(128));
if (!sender->avatarUrl(this).isEmpty()) {
avatar_image = sender->avatar(128, this);
} else {
avatar_image = this->avatar(128);
}
NotificationsManager::instance().postNotification(this, displayName(), sender->displayname(this), eventToString(*lastEvent), avatar_image);
}); });
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged); connect(this, &Room::aboutToAddHistoricalMessages,
this, &NeoChatRoom::readMarkerLoadedChanged);
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged); connect(this, &Quotient::Room::eventsHistoryJobChanged,
this, &NeoChatRoom::lastActiveTimeChanged);
connect(this, &Room::joinStateChanged, this, [=](JoinState oldState, JoinState newState) {
if(oldState == JoinState::Invite && newState != JoinState::Invite) {
Q_EMIT isInviteChanged();
}
});
} }
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body) void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
@@ -105,6 +93,7 @@ void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
}); });
connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) { connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) {
if (id == txnId) { if (id == txnId) {
qDebug() << "Progress:" << progress << total;
setFileUploadingProgress(int(float(progress) / float(total) * 100)); setFileUploadingProgress(int(float(progress) / float(total) * 100));
} }
}); });
@@ -126,12 +115,7 @@ QVariantList NeoChatRoom::getUsersTyping() const
users.removeAll(localUser()); users.removeAll(localUser());
QVariantList userVariants; QVariantList userVariants;
for (User *user : users) { for (User *user : users) {
userVariants.append(QVariantMap { userVariants.append(QVariant::fromValue(user));
{"id", user->id()},
{"avatarMediaId", user->avatarMediaId(this)},
{"displayName", user->displayname(this)},
{"display", user->name()},
});
} }
return userVariants; return userVariants;
} }
@@ -182,6 +166,7 @@ QString NeoChatRoom::lastEventToString() const
return QLatin1String(""); return QLatin1String("");
} }
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
{ {
return highlights.contains(e); return highlights.contains(e);
@@ -276,15 +261,7 @@ QVariantList NeoChatRoom::getUsers(const QString &keyword) const
QVariantList matchedList; QVariantList matchedList;
for (const auto u : userList) { for (const auto u : userList) {
if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) { if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) {
NeoChatUser user(u->id(), u->connection()); matchedList.append(QVariant::fromValue(u));
QVariantMap userVariant {
{ QStringLiteral("id"), user.id() },
{ QStringLiteral("displayName"), user.displayname(this) },
{ QStringLiteral("avatarMediaId"), user.avatarMediaId(this) },
{ QStringLiteral("color"), user.color() }
};
matchedList.append(QVariant::fromValue(userVariant));
} }
} }
@@ -364,9 +341,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
} }
return plainBody; return plainBody;
}, },
[](const StickerEvent &e) {
return e.body();
},
[this](const RoomMemberEvent &e) { [this](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event // FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->user(e.userId())->displayname(); auto subjectName = this->user(e.userId())->displayname();
@@ -374,38 +348,25 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
switch (e.membership()) { switch (e.membership()) {
case MembershipType::Invite: case MembershipType::Invite:
if (e.repeatsState()) { if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName); return i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
} }
Q_FALLTHROUGH(); break;
case MembershipType::Join: { case MembershipType::Join: {
QString text {};
// Part 1: invites and joins
if (e.repeatsState()) { if (e.repeatsState()) {
text = i18n("joined the room (repeated)"); return i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite
? i18n("invited %1 to the room", subjectName)
: i18n("joined the room");
} }
if (!text.isEmpty()) { if (!e.prevContent() || e.membership() != e.prevContent()->membership) {
if (!e.reason().isEmpty()) { return e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
} }
// Part 2: profile changes of joined members QString text {};
if (e.isRename() && NeoChatConfig::self()->showRename()) { if (e.isRename()) {
if (!e.displayName().isEmpty()) { if (e.displayName().isEmpty()) {
text = i18n("cleared their display name"); text = i18n("cleared their display name");
} else { } else {
text = i18n("changed their display name to %1", e.displayName().toHtmlEscaped()); text = i18n("changed their display name to %1", e.displayName().toHtmlEscaped());
} }
} }
if (e.isAvatarUpdate() && NeoChatConfig::self()->showAvatarUpdate()) { if (e.isAvatarUpdate()) {
if (!text.isEmpty()) { if (!text.isEmpty()) {
text += i18n(" and "); text += i18n(" and ");
} }
@@ -466,7 +427,7 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
const auto job = connection()->uploadFile(localFile.toLocalFile()); const auto job = connection()->uploadFile(localFile.toLocalFile());
if (isJobRunning(job)) { if (isJobRunning(job)) {
connect(job, &BaseJob::success, this, [this, job] { connect(job, &BaseJob::success, this, [this, job] {
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject{{"url", job->contentUri()}}); connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject {{"url", job->contentUri()}});
}); });
} }
} }
@@ -514,6 +475,18 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
return result; return result;
} }
void NeoChatRoom::postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId)
{
const auto parsedHTML = markdownToHTML(text);
const bool isRichText = Qt::mightBeRichText(parsedHTML);
if (isRichText) { // Markdown
postHtmlMessage(text, parsedHTML, type, replyEventId);
} else { // Plain text
postPlainMessage(text, type, replyEventId);
}
}
QString msgTypeToString(MessageEventType msgType) QString msgTypeToString(MessageEventType msgType)
{ {
switch (msgType) { switch (msgType) {
@@ -538,52 +511,14 @@ QString msgTypeToString(MessageEventType msgType)
} }
} }
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId) void NeoChatRoom::postPlainMessage(const QString &text, MessageEventType type, const QString &replyEventId)
{ {
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)
{
bool isRichText = Qt::mightBeRichText(html);
bool isReply = !replyEventId.isEmpty(); bool isReply = !replyEventId.isEmpty();
bool isEdit = !relateToEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId); const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) { if (replyIt == timelineEdge()) {
isReply = false; isReply = false;
} }
if (isEdit) {
QJsonObject json {
{"type", "m.room.message"},
{"msgtype", msgTypeToString(type)},
{"body", "* " + text},
{"format", "org.matrix.custom.html"},
{"formatted_body", html},
{"m.new_content",
QJsonObject {
{"body", text},
{"msgtype", msgTypeToString(type)},
{"format", "org.matrix.custom.html"},
{"formatted_body", html}
}
},
{"m.relates_to",
QJsonObject {
{"rel_type", "m.replace"},
{"event_id", relateToEventId}
}
}
};
postJson("m.room.message", json);
return;
}
if (isReply) { if (isReply) {
const auto &replyEvt = **replyIt; const auto &replyEvt = **replyIt;
@@ -608,7 +543,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
"\">In reply to</a> <a href=\"https://matrix.to/#/" + "\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() + replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) + "</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + (isRichText ? html : text) "</blockquote></mx-reply>" + text
} }
}; };
// clang-format on // clang-format on
@@ -618,11 +553,52 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
return; return;
} }
if (isRichText) { Room::postMessage(text, type);
Room::postHtmlMessage(text, html, type); }
} else {
Room::postMessage(text, type); void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId)
{
bool isReply = !replyEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) {
isReply = false;
} }
if (isReply) {
const auto &replyEvt = **replyIt;
// clang-format off
QJsonObject json{
{"msgtype", msgTypeToString(type)},
{"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text},
{"format", "org.matrix.custom.html"},
{"m.relates_to",
QJsonObject {
{"m.in_reply_to",
QJsonObject {
{"event_id", replyEventId}
}
}
}
},
{"formatted_body",
"<mx-reply><blockquote><a href=\"https://matrix.to/#/" +
id() + "/" +
replyEventId +
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + html
}
};
// clang-format on
postJson("m.room.message", json);
return;
}
Room::postHtmlMessage(text, html, type);
} }
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction) void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
@@ -699,8 +675,3 @@ bool NeoChatRoom::readMarkerLoaded() const
const auto it = findInTimeline(readMarkerEventId()); const auto it = findInTimeline(readMarkerEventId());
return it != timelineEdge(); return it != timelineEdge();
} }
bool NeoChatRoom::isInvite() const
{
return joinState() == JoinState::Invite;
}

View File

@@ -3,7 +3,6 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#pragma once #pragma once
#include <events/encryptionevent.h> #include <events/encryptionevent.h>
@@ -33,7 +32,6 @@ class NeoChatRoom : public Room
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged) Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged) Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
public: public:
explicit NeoChatRoom(Connection *connection, QString roomId, JoinState joinState = {}); explicit NeoChatRoom(Connection *connection, QString roomId, JoinState joinState = {});
@@ -104,8 +102,6 @@ public:
Q_INVOKABLE [[nodiscard]] bool canSendEvent(const QString &eventType) const; Q_INVOKABLE [[nodiscard]] bool canSendEvent(const QString &eventType) const;
Q_INVOKABLE [[nodiscard]] bool canSendState(const QString &eventType) const; Q_INVOKABLE [[nodiscard]] bool canSendState(const QString &eventType) const;
bool isInvite() const;
private: private:
QString m_cachedInput; QString m_cachedInput;
QSet<const Quotient::RoomEvent *> highlights; QSet<const Quotient::RoomEvent *> highlights;
@@ -132,17 +128,15 @@ Q_SIGNALS:
void backgroundChanged(); void backgroundChanged();
void readMarkerLoadedChanged(); void readMarkerLoadedChanged();
void lastActiveTimeChanged(); void lastActiveTimeChanged();
void isInviteChanged();
public Q_SLOTS: public Q_SLOTS:
void uploadFile(const QUrl &url, const QString &body = QString()); void uploadFile(const QUrl &url, const QString &body = "");
void acceptInvitation(); void acceptInvitation();
void forget(); void forget();
void sendTypingNotification(bool isTyping); void sendTypingNotification(bool isTyping);
/// @param rawText The text as it was typed. void postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId);
/// @param cleanedText The text with link to the users. void postPlainMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type = Quotient::MessageEventType::Text, const QString &replyEventId = "");
void postMessage(const QString &rawText, const QString &cleanedText, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString()); void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = "");
void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString());
void changeAvatar(const QUrl &localFile); void changeAvatar(const QUrl &localFile);
void addLocalAlias(const QString &alias); void addLocalAlias(const QString &alias);
void removeLocalAlias(const QString &alias); void removeLocalAlias(const QString &alias);

View File

@@ -10,20 +10,13 @@
#include "csapi/profile.h" #include "csapi/profile.h"
#include "controller.h"
static Kirigami::PlatformTheme * s_theme = nullptr;
NeoChatUser::NeoChatUser(QString userId, Connection *connection) NeoChatUser::NeoChatUser(QString userId, Connection *connection)
: User(std::move(userId), connection) : User(std::move(userId), connection)
{ {
if (!s_theme) { m_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true));
s_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(&Controller::instance(), true)); Q_ASSERT(m_theme);
Q_ASSERT(s_theme);
}
connect(s_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor); connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
polishColor();
} }
QColor NeoChatUser::color() QColor NeoChatUser::color()
@@ -38,11 +31,11 @@ void NeoChatUser::setColor(const QColor &color)
} }
m_color = color; m_color = color;
Q_EMIT colorChanged(m_color); emit colorChanged(m_color);
} }
void NeoChatUser::polishColor() void NeoChatUser::polishColor()
{ {
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal // https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
setColor(QColor::fromHslF(hueF(), 1 - s_theme->alternateBackgroundColor().saturationF(), -0.7 * s_theme->alternateBackgroundColor().lightnessF() + 0.9, s_theme->textColor().alphaF())); setColor(QColor::fromHslF(hueF(), 1 - m_theme->alternateBackgroundColor().saturationF(), -0.7 * m_theme->alternateBackgroundColor().lightnessF() + 0.9, m_theme->textColor().alphaF()));
} }

View File

@@ -3,7 +3,6 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#pragma once #pragma once
#include <QObject> #include <QObject>
@@ -33,6 +32,7 @@ Q_SIGNALS:
void colorChanged(QColor _t1); void colorChanged(QColor _t1);
private: private:
Kirigami::PlatformTheme *m_theme = nullptr;
QColor m_color; QColor m_color;
void polishColor(); void polishColor();

View File

@@ -11,8 +11,8 @@
#include <KLocalizedString> #include <KLocalizedString>
#include <KNotification> #include <KNotification>
#include "controller.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "controller.h"
NotificationsManager &NotificationsManager::instance() NotificationsManager &NotificationsManager::instance()
{ {
@@ -25,7 +25,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{ {
} }
void NotificationsManager::postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon) void NotificationsManager::postNotification(NeoChatRoom *room, const QString &eventid, const QString &roomname, const QString &sender, const QString &text, const QImage &icon)
{ {
if (!NeoChatConfig::self()->showNotifications()) { if (!NeoChatConfig::self()->showNotifications()) {
return; return;
@@ -35,10 +35,10 @@ void NotificationsManager::postNotification(NeoChatRoom *room, const QString &ro
img.convertFromImage(icon); img.convertFromImage(icon);
KNotification *notification = new KNotification("message"); KNotification *notification = new KNotification("message");
if (sender == roomName) { if (sender == roomname) {
notification->setTitle(sender); notification->setTitle(sender);
} else { } else {
notification->setTitle(i18n("%1 (%2)", sender, roomName)); notification->setTitle(i18n("%1 (%2)", sender, roomname));
} }
notification->setText(text.toHtmlEscaped()); notification->setText(text.toHtmlEscaped());

View File

@@ -3,7 +3,6 @@
* *
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
#pragma once #pragma once
#include <QImage> #include <QImage>
@@ -22,7 +21,7 @@ class NotificationsManager : public QObject
public: public:
static NotificationsManager &instance(); static NotificationsManager &instance();
Q_INVOKABLE void postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon); Q_INVOKABLE void postNotification(NeoChatRoom *roomId, const QString &eventId, const QString &roomName, const QString &senderName, const QString &text, const QImage &icon);
private: private:
NotificationsManager(QObject *parent = nullptr); NotificationsManager(QObject *parent = nullptr);

View File

@@ -115,7 +115,7 @@ void PublicRoomListModel::next(int count)
return; return;
} }
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword}); job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter {m_keyword});
connect(job, &BaseJob::finished, this, [=] { connect(job, &BaseJob::finished, this, [=] {
attempted = true; attempted = true;

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef PUBLICROOMLISTMODEL_H
#pragma once #define PUBLICROOMLISTMODEL_H
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QObject> #include <QObject>
@@ -83,3 +83,5 @@ Q_SIGNALS:
void keywordChanged(); void keywordChanged();
void hasMoreChanged(); void hasMoreChanged();
}; };
#endif // PUBLICROOMLISTMODEL_H

View File

@@ -14,26 +14,11 @@
#include <QBrush> #include <QBrush>
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusInterface>
#endif
#include <QStandardPaths> #include <QStandardPaths>
#include <KLocalizedString> #include <KLocalizedString>
#include <utility> #include <utility>
#ifndef Q_OS_ANDROID
bool useUnityCounter() {
static const auto Result = QDBusInterface(
"com.canonical.Unity",
"/").isValid();
return Result;
}
#endif
RoomListModel::RoomListModel(QObject *parent) RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
@@ -41,37 +26,6 @@ RoomListModel::RoomListModel(QObject *parent)
for (auto collapsedSection : collapsedSections) { for (auto collapsedSection : collapsedSections) {
m_categoryVisibility[collapsedSection] = false; m_categoryVisibility[collapsedSection] = false;
} }
#ifndef Q_OS_ANDROID
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
if (useUnityCounter()) {
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
} else {
dbusUnityProperties["count-visible"] = false;
}
auto signal = QDBusMessage::createSignal(
"/com/canonical/unity/launcherentry/neochat",
"com.canonical.Unity.LauncherEntry",
"Update");
signal.setArguments({
launcherUrl,
dbusUnityProperties
});
QDBusConnection::sessionBus().send(signal);
}
});
#endif
} }
RoomListModel::~RoomListModel() = default; RoomListModel::~RoomListModel() = default;
@@ -228,9 +182,6 @@ void RoomListModel::refreshNotificationCount()
for (auto room : qAsConst(m_rooms)) { for (auto room : qAsConst(m_rooms)) {
count += room->notificationCount(); count += room->notificationCount();
} }
if (m_notificationCount == count) {
return;
}
m_notificationCount = count; m_notificationCount = count;
Q_EMIT notificationCountChanged(); Q_EMIT notificationCountChanged();
} }
@@ -429,13 +380,3 @@ bool RoomListModel::categoryVisible(int category) const
{ {
return m_categoryVisibility.value(category, true); return m_categoryVisibility.value(category, true);
} }
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for(const auto &room : m_rooms) {
if(room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
return room;
}
}
return nullptr;
}

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef ROOMLISTMODEL_H
#pragma once #define ROOMLISTMODEL_H
#include "connection.h" #include "connection.h"
#include "events/roomevent.h" #include "events/roomevent.h"
@@ -80,8 +80,6 @@ public:
return m_notificationCount; return m_notificationCount;
} }
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
private Q_SLOTS: private Q_SLOTS:
void doAddRoom(Quotient::Room *room); void doAddRoom(Quotient::Room *room);
void updateRoom(Quotient::Room *room, Quotient::Room *prev); void updateRoom(Quotient::Room *room, Quotient::Room *prev);
@@ -107,3 +105,5 @@ Q_SIGNALS:
void newMessage(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6); void newMessage(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6); void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
}; };
#endif // ROOMLISTMODEL_H

View File

@@ -1,26 +0,0 @@
// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "stickerevent.h"
using namespace Quotient;
StickerEvent::StickerEvent(const QJsonObject &obj)
: RoomEvent(typeId(), obj)
, m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject()))
{}
QString StickerEvent::body() const
{
return content<QString>("body"_ls);
}
const EventContent::ImageContent &StickerEvent::image() const
{
return m_imageContent;
}
QUrl StickerEvent::url() const
{
return m_imageContent.url;
}

View File

@@ -1,38 +0,0 @@
// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "events/roomevent.h"
#include "events/eventcontent.h"
namespace Quotient {
/// Sticker messages are specialised image messages that are displayed without
/// controls (e.g. no "download" link, or light-box view on click, as would be
/// displayed for for m.image events).
class StickerEvent : public RoomEvent
{
public:
DEFINE_EVENT_TYPEID("m.sticker", StickerEvent)
explicit StickerEvent(const QJsonObject &obj);
/// \brief A textual representation or associated description of the
/// sticker image.
///
/// This could be the alt text of the original image, or a message to
/// accompany and further describe the sticker.
QString body() const;
/// \brief Metadata about the image referred to in url including a
/// thumbnail representation.
const EventContent::ImageContent &image() const;
/// \brief The URL to the sticker image. This must be a valid mxc:// URI.
QUrl url() const;
private:
EventContent::ImageContent m_imageContent;
};
REGISTER_EVENT_TYPE(StickerEvent)
} // namespace Quotient

View File

@@ -1,6 +1,5 @@
/** /**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org> * SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
@@ -12,28 +11,34 @@
#include <KLocalizedString> #include <KLocalizedString>
TrayIcon::TrayIcon(QObject *parent) TrayIcon::TrayIcon(QObject *parent)
: QSystemTrayIcon(parent) : KStatusNotifierItem(parent)
{ {
setIcon(QIcon::fromTheme("org.kde.neochat"));
QMenu *menu = new QMenu(); QMenu *menu = new QMenu();
auto viewAction_ = new QAction(i18n("Show"), parent); auto viewAction_ = new QAction(i18n("Show"), parent);
connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow); connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow);
connect(this, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { connect(this, &KStatusNotifierItem::activateRequested, this, [this](bool active) {
if (reason == QSystemTrayIcon::Trigger) { if (active) {
Q_EMIT showWindow(); Q_EMIT showWindow();
} }
}); });
menu->addAction(viewAction_); menu->addAction(viewAction_);
menu->addSeparator(); setCategory(Communications);
auto quitAction = new QAction(i18n("Quit"), parent);
quitAction->setIcon(QIcon::fromTheme("application-exit"));
connect(quitAction, &QAction::triggered, QCoreApplication::instance(), QCoreApplication::quit);
menu->addAction(quitAction);
setContextMenu(menu); setContextMenu(menu);
} }
void TrayIcon::setIsOnline(bool online)
{
m_isOnline = online;
setStatus(Active);
Q_EMIT isOnlineChanged();
}
void TrayIcon::setIconSource(const QString &source)
{
m_iconSource = source;
setIconByName(source);
Q_EMIT iconSourceChanged();
}

View File

@@ -3,24 +3,48 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef TRAYICON_H
#pragma once #define TRAYICON_H
// Modified from mujx/nheko's TrayIcon. // Modified from mujx/nheko's TrayIcon.
#include <QSystemTrayIcon> #include <KStatusNotifierItem>
#include <QAction> #include <QAction>
#include <QIcon> #include <QIcon>
#include <QIconEngine> #include <QIconEngine>
#include <QPainter> #include <QPainter>
#include <QRect> #include <QRect>
class TrayIcon : public QSystemTrayIcon class TrayIcon : public KStatusNotifierItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString iconSource READ iconSource WRITE setIconSource NOTIFY iconSourceChanged)
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
public: public:
TrayIcon(QObject *parent = nullptr); TrayIcon(QObject *parent = nullptr);
QString iconSource()
{
return m_iconSource;
}
void setIconSource(const QString &source);
bool isOnline() const
{
return m_isOnline;
}
void setIsOnline(bool online);
Q_SIGNALS: Q_SIGNALS:
void notificationCountChanged();
void iconSourceChanged();
void isOnlineChanged();
void showWindow(); void showWindow();
private:
QString m_iconSource;
bool m_isOnline = true;
}; };
#endif // TRAYICON_H

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef USERDIRECTORYLISTMODEL_H
#pragma once #define USERDIRECTORYLISTMODEL_H
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QObject> #include <QObject>
@@ -71,3 +71,5 @@ Q_SIGNALS:
void keywordChanged(); void keywordChanged();
void limitedChanged(); void limitedChanged();
}; };
#endif // USERDIRECTORYLISTMODEL_H

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef USERLISTMODEL_H
#pragma once #define USERLISTMODEL_H
#include "room.h" #include "room.h"
@@ -76,3 +76,5 @@ private:
int findUserPos(Quotient::User *user) const; int findUserPos(Quotient::User *user) const;
[[nodiscard]] int findUserPos(const QString &username) const; [[nodiscard]] int findUserPos(const QString &username) const;
}; };
#endif // USERLISTMODEL_H

View File

@@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
#ifndef Utils_H
#pragma once #define Utils_H
#include "room.h" #include "room.h"
#include "user.h" #include "user.h"
@@ -20,9 +20,11 @@
namespace utils namespace utils
{ {
static const QRegularExpression removeReplyRegex{"> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression removeReplyRegex {"> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReplyRegex{"<mx-reply>.*?</mx-reply>", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression removeRichReplyRegex {"<mx-reply>.*?</mx-reply>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePillRegExp{"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression codePillRegExp {"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression userPillRegExp{"<a href=\"https://matrix.to/#/@.*?:.*?\">(.*?)</a>", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression userPillRegExp {"<a href=\"https://matrix.to/#/@.*?:.*?\">(.*?)</a>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression strikethroughRegExp{"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression strikethroughRegExp {"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption};
} // namespace utils } // namespace utils
#endif