Compare commits

..

65 Commits
v1.1.0 ... 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
92 changed files with 2327 additions and 4120 deletions

2
.gitignore vendored
View File

@@ -3,5 +3,3 @@ build
.DS_Store
.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)
set(KF5_MIN_VERSION "5.77.0")
set(KF5_MIN_VERSION "5.76.0")
set(QT_MIN_VERSION "5.15.0")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -19,16 +19,13 @@ include(ECMQMLModules)
include(KDEClangFormat)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
include(cmake/Flatpak.cmake)
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
cmake_policy(SET CMP0063 OLD)
ecm_setup_version(1.1.0
ecm_setup_version(1.0.1
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
@@ -60,10 +57,11 @@ else()
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
find_package(KF5DBusAddons ${KF5_MIN_VERSION})
set_package_properties(KF5DBusAddons PROPERTIES
TYPE REQUIRED
PURPOSE "DBus convenience functions"
)
endif()
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.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-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})
# add_definitions(-DQT_NO_KEYWORDS) Need to fix libQuotient first
add_definitions(-DQT_NO_FOREACH)
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
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions.
Along with the stable release, a Flatpak version is available for the nightly
There is no stable release for now, but a Flatpak version is available for the nightly
version:
```
@@ -18,11 +15,9 @@ flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kde
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/).
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)
## Features

View File

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

View File

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

View File

@@ -22,10 +22,8 @@ ToolBar {
property alias isReply: replyItem.visible
property bool isReaction: false
property var replyUser
property string replyEventID: ""
property string replyContent: ""
property string editEventId
property string replyEventID
property string replyContent
property alias isAutoCompleting: autoCompleteListView.visible
property var autoCompleteModel
@@ -33,10 +31,7 @@ ToolBar {
property int autoCompleteEndPosition
property bool hasAttachment: false
property url attachmentPath: ""
property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
property bool hasImageAttachment: hasAttachment && attachmentMimetype.valid
&& FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
property url attachmentPath
position: ToolBar.Footer
@@ -197,12 +192,13 @@ ToolBar {
Image {
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
source: attachmentPath
visible: hasImageAttachment
visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
fillMode: Image.PreserveAspectFit
Layout.preferredWidth: paintedWidth
RowLayout {
anchors.right: parent.right
Button {
visible: isImage
icon.name: "document-edit"
// HACK: Use a component because an url doesn't work
@@ -237,7 +233,7 @@ ToolBar {
}
}
Rectangle {
color: Qt.rgba(255, 255, 255, 40)
color: rgba(255, 255, 255, 40)
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
@@ -252,7 +248,7 @@ ToolBar {
}
RowLayout {
visible: hasAttachment && !hasImageAttachment
visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
ToolButton {
icon.name: "dialog-cancel"
onClicked: {
@@ -261,33 +257,12 @@ ToolBar {
}
}
Kirigami.Icon {
id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(horizontalFileLabel.implicitHeight)
implicitWidth: implicitHeight
source: attachmentMimetype.iconName
}
Label {
id: horizontalFileLabel
Layout.alignment: Qt.AlignVCenter
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 {
Layout.fillWidth: true
Layout.preferredHeight: 1
@@ -306,191 +281,187 @@ ToolBar {
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.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
// 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: ({})
wrapMode: Text.Wrap
placeholderText: i18n("Write your message...")
topPadding: 0
bottomPadding: 0
leftPadding: Kirigami.Units.smallSpacing
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
ChatDocumentHandler {
id: documentHandler
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
room: currentRoom ?? null
text: currentRoom != null ? currentRoom.cachedInput : ""
background: Item {}
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)
}
}
property int lineHeight: contentHeight / lineCount
Timer {
id: repeatTimer
wrapMode: Text.Wrap
placeholderText: i18n("Write your message...")
topPadding: 0
bottomPadding: 0
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;
}
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 = ""
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: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
// 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;
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() {
roomManager.actionsHandler.postMessage(inputField.text.trim(), attachmentPath,
replyEventID, editEventId, inputField.userAutocompleted);
clearAttachment();
currentRoom.markAllMessagesAsRead();
clear();
text = Qt.binding(function() {
return currentRoom != null ? currentRoom.cachedInput : "";
});
inputField.autoComplete();
}
onTextChanged: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
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() {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
if (!autoCompleteListView.currentItem.isEmoji) {
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
}
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
}
// 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.color: "transparent"
enabled: inputField.length > 0 || hasAttachment
onClicked: {
inputField.postMessage()
inputField.text = ""
root.clearEditReply()
root.clearReply()
root.closeAll()
}
@@ -582,46 +551,21 @@ ToolBar {
}
function clear() {
inputField.clear();
inputField.clear()
inputField.userAutocompleted = {};
}
function clearEditReply() {
isReply = false;
function clearReply() {
isReply = false
replyUser = null;
clear();
root.replyContent = "";
root.replyEventID = "";
root.editEventId = "";
focus();
replyContent = "";
replyEventID = ""
}
function focus() {
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() {
replyItem.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() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
function downloadAndOpen()

View File

@@ -17,8 +17,6 @@ import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Image {
id: img
readonly property bool isAnimated: contentType === "image/gif"
property bool openOnFinished: false
@@ -28,7 +26,8 @@ Image {
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
property bool readonly: false
id: img
source: "image://mxc/" + mediaId
@@ -37,12 +36,34 @@ Image {
fillMode: Image.PreserveAspectFit
ToolTip.text: display
ToolTip.visible: hoverHandler.hovered
Control {
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
HoverHandler {
id: hoverHandler
enabled: img.readonly
horizontalPadding: 8
verticalPadding: 4
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 {
@@ -66,8 +87,6 @@ Image {
MouseArea {
id: messageMouseArea
enabled: !img.readonly
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton

View File

@@ -51,7 +51,7 @@ RowLayout {
Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline
name: author.name ?? author.displayName
name: author.name
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
@@ -95,7 +95,7 @@ RowLayout {
visible: showAuthor && !isEmote
text: author.displayName
font.weight: Font.Bold
font.bold: true
color: author.color
wrapMode: Text.Wrap
}
@@ -112,9 +112,7 @@ RowLayout {
}
Connections {
target: replyLoader.item
function onClicked() {
replyClicked(reply.eventId)
}
onClicked: replyClicked(reply.eventId)
}
}
RowLayout {
@@ -134,13 +132,6 @@ RowLayout {
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.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered

View File

@@ -22,7 +22,7 @@ Flow {
contentItem: Label {
horizontalAlignment: Text.AlignHCenter
text: modelData.reaction + " " + modelData.count
text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "")
}
padding: Kirigami.Units.smallSpacing
@@ -31,8 +31,6 @@ Flow {
radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1
}
checkable: true
@@ -47,7 +45,7 @@ Flow {
for (var i = 0; i < modelData.authors.length; i++) {
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) {
text += ", "
}

View File

@@ -15,28 +15,8 @@ TextEdit {
property bool isEmote: false
text: "<style>
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;
}
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>") : "")
.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
font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
@@ -46,7 +26,15 @@ a{
textFormat: Text.RichText
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,15 @@ Kirigami.OverlaySheet {
wrapMode: Text.WordWrap
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 {
text: i18n("Add an account")
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 {

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.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Dialogs 1.2
import org.kde.kquickimageeditor 1.0 as KQuickImageEditor
import QtGraphicalEffects 1.12
import Qt.labs.platform 1.0 as Platform
@@ -80,6 +81,21 @@ Kirigami.Page {
onActivated: saveAsAction.trigger();
} 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 {
id: imageDoc
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
parent: applicationWindow().overlay
title: i18n("Invite a User")
actions {
main: Kirigami.Action {
icon.name: "dialog-close"
text: i18nc("@action", "Cancel")
onTriggered: applicationWindow().pageStack.layers.pop()
onTriggered: applicationWindow().pageStack.pop()
}
}
header: RowLayout {
@@ -123,7 +125,7 @@ Kirigami.ScrollablePage {
onClicked: {
room.inviteToRoom(userID);
applicationWindow().pageStack.layers.pop();
applicationWindow().pageStack.pop();
}
}
}

View File

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

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 {
id: page
property var roomListModel
property var enteredRoom
required property var activeConnection
signal enterRoom(var room)
signal leaveRoom(var room)
function goToNextRoom() {
do {
listView.incrementCurrentIndex();
@@ -61,10 +65,7 @@ Kirigami.ScrollablePage {
}
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: RoomListModel {
id: roomListModel
connection: page.activeConnection
}
sourceModel: roomListModel
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
}
@@ -76,50 +77,100 @@ Kirigami.ScrollablePage {
}
contentItem: RowLayout {
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 {
id: categoryName
level: 3
text: roomListModel.categoryName(section)
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
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
icon: undefined
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
var roomItem = roomManager.enterRoom(currentRoom)
roomListItem.KeyNavigation.right = roomItem
roomItem.focus = true;
if (category === RoomType.Invited) {
roomManager.openInvitation(currentRoom);
} else {
var roomItem = roomManager.enterRoom(currentRoom)
roomListItem.KeyNavigation.right = roomItem
roomItem.focus = true;
}
}
}
label: name ?? ""
subtitle: {
let txt = (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
if (txt.length) {
return txt
contentItem: RowLayout {
id: roomLayout
spacing: Kirigami.Units.largeSpacing
width: listView.width
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
}
return " "
}
leading: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: height
}
TapHandler {
onTapped: enterRoomAction.trigger()
onLongPressed: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
}
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 {
text: notificationCount
visible: notificationCount > 0
@@ -133,26 +184,6 @@ Kirigami.ScrollablePage {
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 {

View File

@@ -23,22 +23,11 @@ import NeoChat.Menu.Timeline 1.0
Kirigami.ScrollablePage {
id: page
required property var currentRoom
title: currentRoom.displayName
property var currentRoom
signal switchRoomUp()
signal switchRoomDown()
Connections {
target: Controller.activeConnection
function onJoinedRoom(room) {
if(room.id === invitation.id) {
roomManager.enterRoom(room);
}
}
}
Connections {
target: roomManager.actionsHandler
onShowMessage: {
@@ -60,46 +49,37 @@ Kirigami.ScrollablePage {
}
}
Kirigami.PlaceholderMessage {
id: invitation
title: currentRoom.displayName
property var id
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
titleDelegate: Component {
RowLayout {
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: {
page.currentRoom.forget()
roomManager.getBack();
}
visible: !Kirigami.Settings.isMobile
Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1 // The +1 is to make sure we do not trigger eliding at max width
Layout.minimumWidth: 0
spacing: Kirigami.Units.gridUnit * 0.8
Kirigami.Heading {
id: titleLabel
level: 2
Layout.alignment: Qt.AlignVCenter
text: page.title
opacity: page.isCurrentPage ? 1 : 0.4
maximumLineCount: 1
elide: Text.ElideRight
}
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
currentRoom.acceptInvitation();
invitation.id = currentRoom.id
currentRoom = null
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
anchors.baseline: lineCount < 2 ? titleLabel.baseline : undefined // necessary, since there is no way to do this with Layout.alignment
text: currentRoom.topic
maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight
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
Keys.onTabPressed: {
@@ -130,8 +110,6 @@ Kirigami.ScrollablePage {
ListView {
id: messageListView
visible: !invitation.visible
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
readonly property bool isLoaded: page.width * page.height > 10
@@ -168,6 +146,14 @@ Kirigami.ScrollablePage {
room: currentRoom
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: messageListView.count === 0 && !currentRoom.allHistoryLoaded
QQC2.BusyIndicator {
running: true
}
}
QQC2.Popup {
anchors.centerIn: parent
@@ -226,10 +212,25 @@ Kirigami.ScrollablePage {
}
MessageFilterModel {
KSortFilterProxyModel {
id: sortedMessageEventModel
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 {
@@ -322,23 +323,24 @@ Kirigami.ScrollablePage {
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
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 {
Layout.fillWidth: true
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 {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
}
]
}
@@ -382,35 +384,7 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
}
}
}
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
Layout.bottomMargin: 8
}
]
}
@@ -491,7 +465,6 @@ Kirigami.ScrollablePage {
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
@@ -590,8 +563,6 @@ Kirigami.ScrollablePage {
footer: ChatTextInput {
id: chatTextInput
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
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")
Kirigami.FormLayout {
QQC2.CheckBox {
Kirigami.FormData.label: i18nc("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.

View File

@@ -42,10 +42,7 @@ Kirigami.ScrollablePage {
text: i18n("Chat")
highlighted: true
onClicked: {
connection.requestDirectChat(identifierField.text);
applicationWindow().pageStack.layers.pop();
}
onClicked: Controller.createDirectChat(connection, identifierField.text)
}
}
}
@@ -106,27 +103,23 @@ Kirigami.ScrollablePage {
}
Button {
id: joinChatButton
Layout.alignment: Qt.AlignRight
visible: directChats && directChats.length > 0
visible: directChats != null
icon.name: "document-send"
onClicked: {
connection.requestDirectChat(userID);
applicationWindow().pageStack.layers.pop();
roomListForm.joinRoom(connection.room(directChats[0]))
root.close()
}
}
Button {
Layout.alignment: Qt.AlignRight
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: {
connection.requestDirectChat(userID);
applicationWindow().pageStack.layers.pop();
Controller.createDirectChat(connection, userID)
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
LoadingPage 1.0 LoadingPage.qml
LoginPage 1.0 LoginPage.qml
RoomListPage 1.0 RoomListPage.qml
RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml

View File

@@ -48,7 +48,7 @@ Kirigami.OverlayDrawer {
icon.name: "list-add-user"
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
applicationWindow().pageStack.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
roomDrawer.close();
}
}
@@ -84,6 +84,12 @@ Kirigami.OverlayDrawer {
}
}
Component {
id: fullScreenImage
FullScreenImage {}
}
Control {
Layout.fillWidth: true
bottomPadding: Kirigami.Units.largeSpacing
@@ -114,7 +120,6 @@ Kirigami.OverlayDrawer {
spacing: 0
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
font.bold: true
@@ -139,7 +144,6 @@ Kirigami.OverlayDrawer {
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
@@ -167,16 +171,6 @@ Kirigami.OverlayDrawer {
headerPositioning: ListView.OverlayHeader
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 {
id: sortedMessageEventModel
@@ -185,13 +179,11 @@ Kirigami.OverlayDrawer {
}
sortRole: "perm"
filterRole: "name"
}
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
z: 1
contentItem: RowLayout {
Kirigami.Avatar {
@@ -229,7 +221,7 @@ Kirigami.OverlayDrawer {
}
return ""
}
color: Kirigami.Theme.disabledTextColor
color: perm == UserType.Muted ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
font.pixelSize: 12
textFormat: Text.PlainText
wrapMode: Text.NoWrap
@@ -237,7 +229,7 @@ Kirigami.OverlayDrawer {
}
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

@@ -24,21 +24,28 @@ Name[uk]=Neochat
Name[x-test]=xxNeochatxx
Name[zh_CN]=Neochat
DesktopEntry=org.kde.neochat
Comment=A client for matrix, the decentralized communication protocol
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldings­protokollen.
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
Comment=IM client for the Matrix protocol
Comment[ca]=Client de MI per al protocol Matrix
Comment[ca@valencia]=Client de MI per al protocol Matrix
Comment[de]=IM-Programm für das Matrix-Protokoll
Comment[en_GB]=IM client for the Matrix protocol
Comment[es]=Cliente de MI para el protocolo Matrix
Comment[eu]=Matrix protokolorako bat-bateko mezularitza bezeroa
Comment[fi]=Pikaviestiasiakas Matrix-yhteyskäytännölle
Comment[fr]=Client « IM » pour le protocole « Matrix »
Comment[hu]=Azonnali üzenetküldő kliens a Matrix protokollhoz
Comment[it]=Client di messaggistica istantanea per il protocollo Matrix
Comment[nl]=IM-client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pl]=Komunikator internetowy dla protokołu Matrix
Comment[pt]=Cliente de MI para o protocolo Matrix
Comment[pt_BR]=Cliente de mensageiro instantâneo para o protocolo Matrix
Comment[sk]=IM klient pre protokol Matrix
Comment[sl]=Odjemalec neposrednega sporočanja po protokolu Matrix
Comment[sv]=Direktmeddelandeklient för protokollet Matrix
Comment[uk]=Клієнт служби миттєвого обміну повідомленнями для протоколу Matrix
Comment[x-test]=xxIM client for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的 IM 客户端
[Event/message]
Name=New message

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

@@ -102,29 +102,18 @@
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<value key="KDE::matrix">#neochat:kde.org</value>
<screenshots>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</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>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<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">
<description>
<p>This version fixes several bugs.</p>

View File

@@ -6,7 +6,6 @@
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
@@ -20,17 +19,11 @@ import NeoChat.Page 1.0
Kirigami.ApplicationWindow {
id: root
property var currentRoom: null
property int columnWidth: Kirigami.Units.gridUnit * 13
minimumWidth: Kirigami.Units.gridUnit * 15
minimumHeight: Kirigami.Units.gridUnit * 20
wideScreen: width > columnWidth * 5
onClosing: Controller.saveWindowGeometry(root)
pageStack.initialPage: LoadingPage {}
Connections {
target: root.quitAction
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
* TODO this should probably be moved to C++
*/
QtObject {
id: roomManager
property var actionsHandler: ActionsHandler {
room: roomManager.currentRoom
connection: Controller.activeConnection
onRoomJoined: {
roomManager.enterRoom(Controller.activeConnection.room(roomName))
}
}
property var currentRoom: null
property alias pageStack: root.pageStack
property bool invitationOpen: false
property var roomList: null
property Item roomItem: null
@@ -78,20 +48,11 @@ Kirigami.ApplicationWindow {
signal leaveRoom(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() {
if (Config.openRoom) {
const room = Controller.activeConnection.room(Config.openRoom);
currentRoom = room;
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
connectRoomToSignal(roomItem);
} else {
// TODO create welcome page
@@ -99,11 +60,12 @@ Kirigami.ApplicationWindow {
}
function enterRoom(room) {
if (currentRoom != null) {
let item = null;
if (currentRoom != null || invitationOpen) {
roomItem.currentRoom = room;
pageStack.currentIndex = pageStack.depth - 1;
} else {
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
}
currentRoom = room;
Config.openRoom = room.id;
@@ -112,14 +74,17 @@ Kirigami.ApplicationWindow {
return roomItem;
}
function getBack() {
pageStack.replace("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': currentRoom, });
function openInvitation(room) {
if (currentRoom != null) {
currentRoom = null;
pageStack.removePage(pageStack.lastItem);
}
invitationOpen = true;
pageStack.push("qrc:/imports/NeoChat/Page/InvitationPage.qml", {"room": room});
}
function openWindow(room) {
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
secondayWindow.width = root.width - roomList.width;
secondayWindow.show();
function getBack() {
pageStack.replace(roomPage, { 'currentRoom': currentRoom, });
}
function connectRoomToSignal(item) {
@@ -154,7 +119,7 @@ Kirigami.ApplicationWindow {
id: contextDrawer
contentItem.implicitWidth: columnWidth
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
modal: !root.wideScreen || !enabled
modal: !root.wideScreen
onEnabledChanged: drawerOpen = enabled && !modal
onModalChanged: drawerOpen = !modal
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
@@ -207,7 +172,7 @@ Kirigami.ApplicationWindow {
icon.name: "settings-configure"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: StandardKey.Preferences
shortcut: Controller.preferencesShortcuts[0]
},
Kirigami.Action {
text: i18n("About Neochat")
@@ -237,60 +202,45 @@ Kirigami.ApplicationWindow {
}
}
pageStack.initialPage: LoadingPage {}
Component {
id: roomListComponent
RoomListPage {
id: roomList
roomListModel: spectralRoomListModel
activeConnection: Controller.activeConnection
}
}
Connections {
target: LoginHelper
function onInitialSyncFinished() {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
Connections {
target: Controller
function onInitiated() {
if (roomManager.hasOpenRoom) {
return;
}
onInitiated: {
if (Controller.accountCount === 0) {
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {});
} else {
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
roomManager.loadInitialRoom();
}
}
function onBusyChanged() {
if(!Controller.busy && roomManager.roomList === null) {
onConnectionAdded: {
if (Controller.accountCount === 1) {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
function onRoomJoined(roomId) {
const room = Controller.activeConnection.room(roomId);
return roomManager.enterRoom(room);
}
function onConnectionDropped() {
onConnectionDropped: {
if (Controller.accountCount === 0) {
pageStack.clear();
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml");
}
}
function onGlobalErrorOccured(error, detail) {
showPassiveNotification(error + ": " + detail)
}
onGlobalErrorOccured: showPassiveNotification(error + ": " + detail)
function onShowWindow() {
root.showWindow()
}
onShowWindow: root.showWindow()
function onOpenRoom(room) {
roomManager.enterRoom(room)
@@ -302,13 +252,6 @@ Kirigami.ApplicationWindow {
}
}
Connections {
target: Controller.activeConnection
onDirectChatAvailable: {
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
}
}
Kirigami.OverlaySheet {
id: consentSheet
@@ -331,47 +274,21 @@ Kirigami.ApplicationWindow {
}
}
RoomListModel {
id: spectralRoomListModel
connection: Controller.activeConnection
}
Component {
id: roomPage
RoomPage {}
}
Component {
id: 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="/">
<file>qml/main.qml</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/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file>
<file>imports/NeoChat/Page/RoomWindow.qml</file>
<file>imports/NeoChat/Page/AccountsPage.qml</file>
<file>imports/NeoChat/Page/JoinRoomPage.qml</file>
<file>imports/NeoChat/Page/InviteUserPage.qml</file>
<file>imports/NeoChat/Page/SettingsPage.qml</file>
<file>imports/NeoChat/Page/InvitationPage.qml</file>
<file>imports/NeoChat/Page/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
<file>imports/NeoChat/Page/DevicesPage.qml</file>
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/ChatTextInput.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/FileDelegate.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/qmldir</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/MessageSourceSheet.qml</file>
<file>imports/NeoChat/Menu/RoomListContextMenu.qml</file>
<file>qtquickcontrols2.conf</file>
</qresource>
</RCC>

View File

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

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#ifndef ACCOUNTLISTMODEL_H
#define ACCOUNTLISTMODEL_H
#include "controller.h"
@@ -30,3 +30,5 @@ public:
private:
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) {
// ignore change, it was caused by autocompletion
return QVariantMap{
return QVariantMap {
{"type", AutoCompletionType::Ignore},
};
}
@@ -141,7 +141,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
if (autoCompletePrefix.isEmpty()) {
return QVariantMap{
return QVariantMap {
{"type", AutoCompletionType::None},
};
}
@@ -151,22 +151,77 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
if (autoCompletePrefix.startsWith("@")) {
autoCompletePrefix.remove(0, 1);
return QVariantMap{
return QVariantMap {
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::User},
};
}
return QVariantMap{
return QVariantMap {
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::Emoji},
};
}
return QVariantMap{
return QVariantMap {
{"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)
{
QTextCursor cursor = textCursor();

View File

@@ -14,7 +14,6 @@
class QTextDocument;
class QQuickTextDocument;
class NeoChatRoom;
class Controller;
class ChatDocumentHandler : public QObject
{
@@ -52,8 +51,10 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
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
/// is the possibility to autocomplete it.
/// is the posibility to autocomplete it.
Q_INVOKABLE QVariantMap getAutocompletionInfo();
Q_INVOKABLE void replaceAutoComplete(const QString &word);
@@ -63,7 +64,6 @@ Q_SIGNALS:
void selectionStartChanged();
void selectionEndChanged();
void roomChanged();
void joinRoom(QString roomName);
private:
[[nodiscard]] QTextCursor textCursor() const;

View File

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

View File

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

View File

@@ -8,42 +8,43 @@
#ifndef Q_OS_ANDROID
#include <qt5keychain/keychain.h>
#endif
#else
#include <KConfig>
#include <KConfigGroup>
#include <KWindowConfig>
#endif
#include <KLocalizedString>
#include <QClipboard>
#include <QDebug>
#include <QQuickWindow>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QStringBuilder>
#include <QSysInfo>
#include <QTimer>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QMovie>
#include <QPixmap>
#include <QAuthenticator>
#include <QNetworkReply>
#include <QStringBuilder>
#include <QtGui/QCloseEvent>
#include <QtGui/QDesktopServices>
#include <QtGui/QMovie>
#include <QtGui/QPixmap>
#include <QtNetwork/QAuthenticator>
#include <QtNetwork/QNetworkReply>
#include <utility>
#include "csapi/account-data.h"
#include "csapi/content-repo.h"
#include "csapi/joining.h"
#include "csapi/logout.h"
#include "csapi/profile.h"
#include "csapi/registration.h"
#include "csapi/wellknown.h"
#include "events/eventcontent.h"
#include "events/roommessageevent.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "neochatconfig.h"
#include "settings.h"
#include "utils.h"
#include <KStandardShortcut>
@@ -55,26 +56,16 @@
Controller::Controller(QObject *parent)
: QObject(parent)
{
QApplication::setQuitOnLastWindowClosed(false);
Connection::setRoomType<NeoChatRoom>();
Connection::setUserType<NeoChatUser>();
#ifndef Q_OS_ANDROID
TrayIcon *trayIcon = new TrayIcon(this);
if(NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
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());
});
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
trayIcon->setIconSource("org.kde.neochat");
trayIcon->setIsOnline(true);
#endif
QTimer::singleShot(0, this, [=] {
@@ -102,6 +93,51 @@ inline QString accessTokenFileName(const AccountSettings &account)
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)
{
if (user.isEmpty() || token.isEmpty()) {
@@ -224,7 +260,7 @@ void Controller::invokeLogin()
const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
AccountSettings account {accountId};
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
@@ -249,7 +285,6 @@ void Controller::invokeLogin()
Q_EMIT errorOccured(i18n("Login Failed"), error);
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured("Network Error", error);
@@ -257,14 +292,14 @@ void Controller::invokeLogin()
connection->connectWithToken(account.userId(), accessToken, account.deviceId());
}
}
if (accounts.isEmpty()) {
if (m_connections.isEmpty()) {
Q_EMIT initiated();
}
}
QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
{
QFile accountTokenFile{accessTokenFileName(account)};
QFile accountTokenFile {accessTokenFileName(account)};
if (accountTokenFile.open(QFile::ReadOnly)) {
if (accountTokenFile.size() < 1024) {
return accountTokenFile.readAll();
@@ -302,7 +337,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken);
if (saved) {
QFile accountTokenFile{accessTokenFileName(account)};
QFile accountTokenFile {accessTokenFileName(account)};
removed = accountTokenFile.remove();
}
if (!(saved && removed)) {
@@ -325,7 +360,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken)
{
// (Re-)Make a dedicated file for access_token.
QFile accountTokenFile{accessTokenFileName(account)};
QFile accountTokenFile {accessTokenFileName(account)};
accountTokenFile.remove(); // Just in case
auto fileDir = QFileInfo(accountTokenFile).dir();
@@ -364,6 +399,34 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
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)
{
@@ -502,21 +565,7 @@ void Controller::setActiveConnection(Connection *connection)
Q_EMIT activeConnectionChanged();
}
void Controller::saveWindowGeometry(QQuickWindow *window)
QList<QKeySequence> Controller::preferencesShortcuts() const
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
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));
return KStandardShortcut::preferences();
}

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QApplication>
#include <QMediaPlayer>
@@ -21,7 +21,6 @@ class QKeySequences;
#include "user.h"
class NeoChatRoom;
class QQuickWindow;
using namespace Quotient;
@@ -34,6 +33,9 @@ class Controller : public QObject
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
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:
static Controller &instance();
@@ -45,12 +47,15 @@ public:
void addConnection(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 changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
[[nodiscard]] int accountCount() const;
[[nodiscard]] QList<QKeySequence> preferencesShortcuts() const;
[[nodiscard]] static bool quitOnLastWindowClosed();
void setQuitOnLastWindowClosed(bool value);
@@ -60,9 +65,6 @@ public:
void setAboutData(const KAboutData &aboutData);
[[nodiscard]] KAboutData aboutData() const;
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
enum PasswordStatus {
Success,
Wrong,
@@ -81,6 +83,8 @@ private:
static QByteArray loadAccessTokenFromFile(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 saveSettings() const;
@@ -91,10 +95,10 @@ private Q_SLOTS:
Q_SIGNALS:
void busyChanged();
/// Error occurred because of user inputs
/// Error occured because of user inputs
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 syncDone();
void connectionAdded(Quotient::Connection *_t1);
@@ -110,14 +114,15 @@ Q_SIGNALS:
void showWindow();
void openRoom(NeoChatRoom *room);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
public Q_SLOTS:
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);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry(QQuickWindow *);
};
// TODO libQuotient 0.7: Drop
@@ -127,8 +132,4 @@ public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth = none);
};
class NeochatDeleteDeviceJob : public BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth = none);
};
#endif // CONTROLLER_H

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
*/
#pragma once
#ifndef EMOJIMODEL_H
#define EMOJIMODEL_H
#include <QObject>
#include <QSettings>
@@ -86,3 +86,5 @@ private:
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 <KAboutData>
#ifdef HAVE_KDBUSADDONS
#ifndef Q_OS_ANDROID
#include <KDBusService>
#endif
#include <KLocalizedContext>
#include <KLocalizedString>
#include <KWindowConfig>
#include "neochat-version.h"
@@ -29,13 +28,9 @@
#include "controller.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h"
#include "login.h"
#include "matriximageprovider.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
@@ -46,7 +41,6 @@
#include "sortfilterroomlistmodel.h"
#include "userdirectorylistmodel.h"
#include "userlistmodel.h"
#include "actionshandler.h"
using namespace Quotient;
@@ -70,10 +64,6 @@ int main(int argc, char *argv[])
}
#endif
#ifdef Q_OS_WINDOWS
QApplication::setStyle(QStringLiteral("breeze"));
#endif
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"));
@@ -85,39 +75,32 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about);
QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
#ifdef HAVE_KDBUSADDONS
#ifndef Q_OS_ANDROID
KDBusService service(KDBusService::Unique);
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// 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
Clipboard clipboard;
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, "Clipboard", &clipboard);
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<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel");
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<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM");
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
@@ -131,7 +114,6 @@ int main(int argc, char *argv[])
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
qRegisterMetaTypeStreamOperators<Emoji>();
@@ -157,7 +139,7 @@ int main(int argc, char *argv[])
return -1;
}
#ifdef HAVE_KDBUSADDONS
#ifndef Q_OS_ANDROID
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
const auto rootObjects = engine.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
return QApplication::exec();
}

View File

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

View File

@@ -11,7 +11,6 @@
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
#include <events/roommemberevent.h>
#include <events/stickerevent.h>
#include <events/simplestateevents.h>
#include <user.h>
@@ -46,7 +45,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowSectionRole] = "showSection";
roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited";
roles[FormattedBodyRole] = "formattedBody";
return roles;
}
@@ -64,8 +62,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
return;
}
m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [=]() {
if (m_currentRoom->readMarkerEventId().isEmpty()) {
connect(this, &QAbstractListModel::rowsInserted, this, [=](){
if(m_currentRoom->readMarkerEventId().isEmpty()) {
return;
}
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
@@ -301,7 +299,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
{
Q_UNUSED(evt)
return QVariantMap{
return QVariantMap {
{"isLocalUser", user->id() == room->localUser()->id()},
{"id", user->id()},
{"avatarMediaId", user->avatarMediaId(room)},
@@ -331,20 +329,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
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 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) {
return m_currentRoom->eventToString(evt);
}
@@ -375,9 +362,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return "message";
}
if (is<const StickerEvent>(evt)) {
return "sticker";
}
if (evt.isStateEvent()) {
return "state";
}
@@ -414,10 +398,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// content JSON stored in EventContent::Base
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) {
@@ -486,9 +466,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
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) {
@@ -525,7 +502,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
};
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) {
@@ -579,7 +556,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
authors.append(userAtEvent(author, m_currentRoom, evt));
}
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;
}

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#ifndef MESSAGEEVENTMODEL_H
#define MESSAGEEVENTMODEL_H
#include <QAbstractListModel>
@@ -32,7 +32,6 @@ public:
LongOperationRole,
AnnotationRole,
UserMarkerRole,
FormattedBodyRole,
ReplyRole,
@@ -40,7 +39,6 @@ public:
ShowSectionRole,
ReactionRole,
IsEditedRole,
// For debugging
@@ -91,3 +89,5 @@ private:
Q_SIGNALS:
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>
<default>true</default>
</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>
</kcfg>

View File

@@ -27,13 +27,12 @@
#include "events/roomcanonicalaliasevent.h"
#include "events/roommessageevent.h"
#include "events/roompowerlevelsevent.h"
#include "events/stickerevent.h"
#include "events/typingevent.h"
#include "jobs/downloadfilejob.h"
#include "neochatconfig.h"
#include "notificationsmanager.h"
#include "user.h"
#include "utils.h"
#include "neochatconfig.h"
#include <KLocalizedString>
@@ -51,7 +50,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return;
}
const RoomEvent *lastEvent = messageEvents().rbegin()->get();
if (lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
if(lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
return;
}
if (lastEvent->isStateEvent()) {
@@ -62,25 +61,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return;
}
QImage avatar_image;
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);
NotificationsManager::instance().postNotification(this, lastEvent->id(), displayName(), sender->displayname(this), eventToString(*lastEvent), avatar(128));
});
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
connect(this, &Room::aboutToAddHistoricalMessages,
this, &NeoChatRoom::readMarkerLoadedChanged);
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();
}
});
connect(this, &Quotient::Room::eventsHistoryJobChanged,
this, &NeoChatRoom::lastActiveTimeChanged);
}
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) {
if (id == txnId) {
qDebug() << "Progress:" << progress << total;
setFileUploadingProgress(int(float(progress) / float(total) * 100));
}
});
@@ -126,12 +115,7 @@ QVariantList NeoChatRoom::getUsersTyping() const
users.removeAll(localUser());
QVariantList userVariants;
for (User *user : users) {
userVariants.append(QVariantMap {
{"id", user->id()},
{"avatarMediaId", user->avatarMediaId(this)},
{"displayName", user->displayname(this)},
{"display", user->name()},
});
userVariants.append(QVariant::fromValue(user));
}
return userVariants;
}
@@ -182,6 +166,7 @@ QString NeoChatRoom::lastEventToString() const
return QLatin1String("");
}
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
{
return highlights.contains(e);
@@ -276,15 +261,7 @@ QVariantList NeoChatRoom::getUsers(const QString &keyword) const
QVariantList matchedList;
for (const auto u : userList) {
if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) {
NeoChatUser user(u->id(), u->connection());
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));
matchedList.append(QVariant::fromValue(u));
}
}
@@ -364,9 +341,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
return plainBody;
},
[](const StickerEvent &e) {
return e.body();
},
[this](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->user(e.userId())->displayname();
@@ -374,38 +348,25 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
switch (e.membership()) {
case MembershipType::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
return i18n("reinvited %1 to the room", subjectName);
}
Q_FALLTHROUGH();
break;
case MembershipType::Join: {
QString text {};
// Part 1: invites and joins
if (e.repeatsState()) {
text = 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");
return i18n("joined the room (repeated)");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
if (!e.prevContent() || e.membership() != e.prevContent()->membership) {
return e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
// Part 2: profile changes of joined members
if (e.isRename() && NeoChatConfig::self()->showRename()) {
if (!e.displayName().isEmpty()) {
QString text {};
if (e.isRename()) {
if (e.displayName().isEmpty()) {
text = i18n("cleared their display name");
} else {
text = i18n("changed their display name to %1", e.displayName().toHtmlEscaped());
}
}
if (e.isAvatarUpdate() && NeoChatConfig::self()->showAvatarUpdate()) {
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
@@ -466,7 +427,7 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
const auto job = connection()->uploadFile(localFile.toLocalFile());
if (isJobRunning(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;
}
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)
{
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 isEdit = !relateToEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) {
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) {
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/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + (isRichText ? html : text)
"</blockquote></mx-reply>" + text
}
};
// clang-format on
@@ -618,11 +553,52 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
return;
}
if (isRichText) {
Room::postHtmlMessage(text, html, type);
} else {
Room::postMessage(text, type);
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)
@@ -699,8 +675,3 @@ bool NeoChatRoom::readMarkerLoaded() const
const auto it = findInTimeline(readMarkerEventId());
return it != timelineEdge();
}
bool NeoChatRoom::isInvite() const
{
return joinState() == JoinState::Invite;
}

View File

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

View File

@@ -10,20 +10,13 @@
#include "csapi/profile.h"
#include "controller.h"
static Kirigami::PlatformTheme * s_theme = nullptr;
NeoChatUser::NeoChatUser(QString userId, Connection *connection)
: User(std::move(userId), connection)
{
if (!s_theme) {
s_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(&Controller::instance(), true));
Q_ASSERT(s_theme);
}
m_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true));
Q_ASSERT(m_theme);
connect(s_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
polishColor();
connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
}
QColor NeoChatUser::color()
@@ -38,11 +31,11 @@ void NeoChatUser::setColor(const QColor &color)
}
m_color = color;
Q_EMIT colorChanged(m_color);
emit colorChanged(m_color);
}
void NeoChatUser::polishColor()
{
// 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
*/
#pragma once
#include <QObject>
@@ -33,6 +32,7 @@ Q_SIGNALS:
void colorChanged(QColor _t1);
private:
Kirigami::PlatformTheme *m_theme = nullptr;
QColor m_color;
void polishColor();

View File

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

View File

@@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QImage>
@@ -22,7 +21,7 @@ class NotificationsManager : public QObject
public:
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:
NotificationsManager(QObject *parent = nullptr);

View File

@@ -115,7 +115,7 @@ void PublicRoomListModel::next(int count)
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, [=] {
attempted = true;

View File

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

View File

@@ -14,26 +14,11 @@
#include <QBrush>
#include <QColor>
#include <QDebug>
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusInterface>
#endif
#include <QStandardPaths>
#include <KLocalizedString>
#include <utility>
#ifndef Q_OS_ANDROID
bool useUnityCounter() {
static const auto Result = QDBusInterface(
"com.canonical.Unity",
"/").isValid();
return Result;
}
#endif
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -41,37 +26,6 @@ RoomListModel::RoomListModel(QObject *parent)
for (auto collapsedSection : collapsedSections) {
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;
@@ -228,9 +182,6 @@ void RoomListModel::refreshNotificationCount()
for (auto room : qAsConst(m_rooms)) {
count += room->notificationCount();
}
if (m_notificationCount == count) {
return;
}
m_notificationCount = count;
Q_EMIT notificationCountChanged();
}
@@ -429,13 +380,3 @@ bool RoomListModel::categoryVisible(int category) const
{
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
*/
#pragma once
#ifndef ROOMLISTMODEL_H
#define ROOMLISTMODEL_H
#include "connection.h"
#include "events/roomevent.h"
@@ -80,8 +80,6 @@ public:
return m_notificationCount;
}
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
private Q_SLOTS:
void doAddRoom(Quotient::Room *room);
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 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: 2021 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -12,28 +11,34 @@
#include <KLocalizedString>
TrayIcon::TrayIcon(QObject *parent)
: QSystemTrayIcon(parent)
: KStatusNotifierItem(parent)
{
setIcon(QIcon::fromTheme("org.kde.neochat"));
QMenu *menu = new QMenu();
auto viewAction_ = new QAction(i18n("Show"), parent);
connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow);
connect(this, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) {
connect(this, &KStatusNotifierItem::activateRequested, this, [this](bool active) {
if (active) {
Q_EMIT showWindow();
}
});
menu->addAction(viewAction_);
menu->addSeparator();
auto quitAction = new QAction(i18n("Quit"), parent);
quitAction->setIcon(QIcon::fromTheme("application-exit"));
connect(quitAction, &QAction::triggered, QCoreApplication::instance(), QCoreApplication::quit);
menu->addAction(quitAction);
setCategory(Communications);
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
*/
#pragma once
#ifndef TRAYICON_H
#define TRAYICON_H
// Modified from mujx/nheko's TrayIcon.
#include <QSystemTrayIcon>
#include <KStatusNotifierItem>
#include <QAction>
#include <QIcon>
#include <QIconEngine>
#include <QPainter>
#include <QRect>
class TrayIcon : public QSystemTrayIcon
class TrayIcon : public KStatusNotifierItem
{
Q_OBJECT
Q_PROPERTY(QString iconSource READ iconSource WRITE setIconSource NOTIFY iconSourceChanged)
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
public:
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:
void notificationCountChanged();
void iconSourceChanged();
void isOnlineChanged();
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
*/
#pragma once
#ifndef USERDIRECTORYLISTMODEL_H
#define USERDIRECTORYLISTMODEL_H
#include <QAbstractListModel>
#include <QObject>
@@ -71,3 +71,5 @@ Q_SIGNALS:
void keywordChanged();
void limitedChanged();
};
#endif // USERDIRECTORYLISTMODEL_H

View File

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

View File

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