Compare commits

..

19 Commits
1.1 ... v1.0

Author SHA1 Message Date
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
97 changed files with 2369 additions and 4455 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,50 +19,24 @@ 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.1
ecm_setup_version(1.0.0
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt5 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Widgets Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons)
set_package_properties(KF5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF5Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Widgets Core Quick Gui QuickControls2 Multimedia Svg)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n Notifications Config CoreAddons)
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES
TYPE REQUIRED
PURPOSE "Encrypted communications"
)
find_package(OpenSSL REQUIRED)
else()
find_package(Qt5Keychain)
set_package_properties(Qt5Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(Qt5Keychain REQUIRED)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
endif()
@@ -83,7 +57,6 @@ set_package_properties(cmark PROPERTIES
)
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -96,12 +69,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
@@ -94,8 +89,8 @@ ToolBar {
Layout.preferredWidth: Kirigami.Units.gridUnit
Layout.preferredHeight: Kirigami.Units.gridUnit
source: replyUser ? ("image://mxc/" + replyUser.avatarMediaId) : ""
name: replyUser ? replyUser.name : i18n("No name")
source: replyUser ? "image://mxc/" + replyUser.avatarMediaId: ""
name: replyUser ? replyUser.displayName : i18n("No name")
}
Label {
@@ -138,7 +133,6 @@ ToolBar {
keyNavigationWraps: true
delegate: Control {
readonly property string userId: modelData.id ?? ""
readonly property string displayText: modelData.displayName ?? modelData.unicode
readonly property bool isEmoji: modelData.unicode != null
readonly property bool highlighted: autoCompleteListView.currentIndex == index
@@ -162,7 +156,7 @@ ToolBar {
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit
Layout.preferredHeight: Kirigami.Units.gridUnit
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
source: modelData.avatarMediaId ? "image://mxc/" + modelData.avatarMediaId : ""
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
visible: !isEmoji
}
@@ -197,12 +191,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 +232,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 +247,7 @@ ToolBar {
}
RowLayout {
visible: hasAttachment && !hasImageAttachment
visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
ToolButton {
icon.name: "dialog-cancel"
onClicked: {
@@ -261,33 +256,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,192 +280,177 @@ 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: 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
}
function postMessage() {
// Qt wraps lines so we need to use a small hack
// to remove the wrapped lines but not break the empty
// lines.
const updatedText = inputField.text.trim()
.replace(/@([^: ]*):([^ ]*\.[^ ]*)/, "[@$1:$2](https://matrix.to/#/@$1:$2)");
documentHandler.postMessage(updatedText, attachmentPath, replyEventID);
clearAttachment();
currentRoom.markAllMessagesAsRead();
clear();
text = Qt.binding(function() {
return currentRoom != null ? currentRoom.cachedInput : "";
});
}
function autoComplete() {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
}
}
@@ -548,12 +507,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 +539,20 @@ ToolBar {
}
function clear() {
inputField.clear();
inputField.userAutocompleted = {};
inputField.clear()
}
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()
@@ -116,7 +115,7 @@ RowLayout {
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}

View File

@@ -7,7 +7,7 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0
import Qt.labs.platform 1.0 as Platform
import org.kde.neochat 1.0
import NeoChat.Setting 1.0
@@ -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
@@ -124,21 +143,15 @@ Image {
}
function saveFileAs() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
Component {
id: fileDialog
folderDialog.chosen.connect(function(path) {
if (!path) return
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
})
folderDialog.open()
}
function downloadAndOpen()
@@ -147,7 +160,7 @@ Image {
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}

View File

@@ -51,8 +51,8 @@ RowLayout {
Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline
name: author.name ?? author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {
@@ -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

@@ -30,8 +30,8 @@ QQC2.AbstractButton {
Layout.preferredHeight: Kirigami.Units.gridUnit
Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline
source: replyVisible && reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : ""
name: replyVisible ? reply.author.name : "H"
source: replyVisible && reply.author.avatarMediaId ? "image://mxc/" + reply.author.avatarMediaId : ""
name: replyVisible ? reply.author.displayName : "H"
color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor
}

View File

@@ -25,8 +25,8 @@ RowLayout {
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
name: author.name
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {

View File

@@ -15,28 +15,7 @@ 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;
}
.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>") : "")
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
color: Kirigami.Theme.textColor
font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
@@ -46,7 +25,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

@@ -42,8 +42,8 @@ Kirigami.OverlaySheet {
Layout.preferredHeight: 72
Layout.alignment: Qt.AlignTop
name: room.name
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
name: room.displayName
source: room.avatarMediaId ? "image://mxc/" + room.avatarMediaId : ""
MouseArea {
anchors.fill: parent

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
@@ -53,7 +51,7 @@ Kirigami.OverlaySheet {
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: displayName
source: avatarMediaId ? ("image://mxc/" + avatarMediaId) : ""
source: avatarMediaId ? "image://mxc/" + avatarMediaId : ""
MouseArea {
anchors.fill: parent
@@ -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

@@ -38,7 +38,7 @@ Kirigami.OverlaySheet {
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
@@ -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 {
@@ -84,7 +86,7 @@ Kirigami.ScrollablePage {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar ? ("image://mxc/" + avatar) : ""
source: avatar ? "image://mxc/" + avatar : ""
name: name
}
@@ -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,19 +104,19 @@ 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 {
Layout.preferredWidth: Kirigami.Units.iconSizes.normal
Layout.preferredHeight: Kirigami.Units.iconSizes.normal
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
source: model.avatar ? ("image://mxc/" + model.avatar) : ""
source: model.avatar ? "image://mxc/" + model.avatar : ""
name: name
}
ColumnLayout {

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.name === name
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: name ?? ""
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,83 +23,41 @@ 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: {
page.header.contentItem.text = message;
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
page.header.contentItem.visible = true;
}
onHideMessage: page.header.contentItem.visible = false
}
header: QQC2.Control {
height: visible ? implicitHeight : 0
visible: contentItem.visible
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
showCloseButton: true
visible: false
}
}
Kirigami.PlaceholderMessage {
id: invitation
property var id
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
title: currentRoom.name
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: {
@@ -115,10 +73,10 @@ Kirigami.ScrollablePage {
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown && (event.modifiers & Qt.ControlModifier)) {
switchRoomUp();
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true;
chatTextInput.addText(event.text);
@@ -130,8 +88,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 +124,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
@@ -187,7 +151,7 @@ Kirigami.ScrollablePage {
onClicked: {
attachDialog.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
@@ -226,10 +190,23 @@ 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.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other";
}
function filterLeaveJoin(row, parent) {
return 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 {
@@ -319,26 +296,27 @@ Kirigami.ScrollablePage {
innerObject: MessageDelegate {
Layout.fillWidth: true
Layout.maximumWidth: messageListView.width
mouseArea: 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);
}
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
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 +360,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 +441,6 @@ Kirigami.ScrollablePage {
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
@@ -590,8 +539,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,59 +16,35 @@ Kirigami.ScrollablePage {
title: i18n("Settings")
Kirigami.FormLayout {
QQC2.CheckBox {
Kirigami.FormData.label: i18n("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.
Kirigami.FormData.label: i18n("Notifications and events:")
text: i18n("Show notifications")
checked: Config.showNotifications
onToggled: {
Config.showNotifications = checked
Config.save()
}
onToggled: Config.showNotifications = checked
}
QQC2.CheckBox {
text: i18n("Show leave and join events")
checked: Config.showLeaveJoinEvent
onToggled: {
Config.showLeaveJoinEvent = checked
Config.save()
}
onToggled: Config.showLeaveJoinEvent = checked
}
QQC2.RadioButton {
Kirigami.FormData.label: i18n("Rooms and private chats:")
text: i18n("Separated")
checked: !Config.mergeRoomList
onToggled: {
Config.mergeRoomList = false
Config.save()
}
onToggled: Config.mergeRoomList = false
}
QQC2.RadioButton {
text: i18n("Intermixed")
checked: Config.mergeRoomList
onToggled: {
Config.mergeRoomList = true
Config.save()
}
onToggled: Config.mergeRoomList = true
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Timeline:")
text: i18n("Show User Avatar")
checked: Config.showAvatarInTimeline
onToggled: {
Config.showAvatarInTimeline = checked
Config.save()
}
onToggled: Config.showAvatarInTimeline = checked
}
}
}

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
@@ -104,8 +110,8 @@ Kirigami.OverlayDrawer {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.name : i18n("No name")
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
name: room ? room.displayName : i18n("No name")
source: room ? "image://mxc/" + room.avatarMediaId : undefined
}
ColumnLayout {
@@ -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,20 +179,18 @@ Kirigami.OverlayDrawer {
}
sortRole: "perm"
filterRole: "name"
}
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
z: 1
contentItem: RowLayout {
Kirigami.Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar ? ("image://mxc/" + avatar) : ""
source: "image://mxc/" + avatar
name: name
}
@@ -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

@@ -3,101 +3,72 @@ IconName=org.kde.neochat
Name=Neochat
Name[ca]=Neochat
Name[ca@valencia]=Neochat
Name[cs]=Neochat
Name[da]=Neochat
Name[de]=Neochat
Name[en_GB]=Neochat
Name[es]=Neochat
Name[eu]=Neochat
Name[fi]=Neochat
Name[fr]=Neochat
Name[hu]=Neochat
Name[ia]=Neochat
Name[it]=Neochat
Name[nl]=Neochat
Name[nn]=Neochat
Name[pl]=Neochat
Name[pt]=Neochat
Name[pt_BR]=Neochat
Name[ro]=Neochat
Name[sk]=Neochat
Name[sl]=Neochat
Name[sv]=Neochat
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[ca@valencia]=Un client per al Matrix, el protocol de comunicacions descentralitzat
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[ia]=Un cliente per matrix, le protocollo de communication decentralisate
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[ro]=Client pentru Matrix, protocolul de comunicare descentralizată
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
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[zh_CN]=分布式通讯协议 Matrix 的客户端
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[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[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[pt]=Cliente de MI para o protocolo 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
[Event/message]
Name=New message
Name[ca]=Missatge nou
Name[ca@valencia]=Missatge nou
Name[cs]=Nová zpráva
Name[de]=Neue Nachricht
Name[en_GB]=New message
Name[es]=Nuevo mensaje
Name[eu]=Mezu berria
Name[fi]=Uusi viesti
Name[fr]=Nouveau message
Name[hu]=Új üzenet
Name[ia]=Nove message
Name[it]=Nuovo messaggio
Name[nl]=Nieuw bericht
Name[nn]=Ny melding
Name[pl]=Nowa wiadomość
Name[pt]=Nova mensagem
Name[pt_BR]=Nova mensagem
Name[ro]=Mesaj nou
Name[sk]=Nová správa
Name[sl]=Novo sporočilo
Name[sv]=Nytt meddelande
Name[uk]=Нове повідомлення
Name[x-test]=xxNew messagexx
Name[zh_CN]=新消息
Comment=There is a new message
Comment[ca]=Hi ha un missatge nou
Comment[ca@valencia]=Hi ha un missatge nou
Comment[de]=Es ist eine neue Nachricht vorhanden
Comment[en_GB]=There is a new message
Comment[es]=Hay un mensaje nuevo
Comment[eu]=Mezu berri bat dago
Comment[fi]=Saapui uusi viesti
Comment[fr]=Il y a un nouveau message
Comment[hu]=Új üzenet érkezett
Comment[ia]=Isto es un nove message
Comment[it]=È presente un nuovo messaggio
Comment[nl]=Er is een nieuw bericht
Comment[nn]=Du har ei ny melding
Comment[pl]=Dostępna jest nowa wiadomość
Comment[pt]=Tem uma mensagem nova
Comment[pt_BR]=Existe uma nova mensagem
Comment[ro]=Este un mesaj nou
Comment[sk]=Je nová správa
Comment[sl]=Prišlo je novo sporočilo
Comment[sv]=Det finns ett nytt meddelande
Comment[uk]=Надійшло нове повідомлення
Comment[x-test]=xxThere is a new messagexx
Comment[zh_CN]=有新消息
Action=Popup

View File

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

Before

Width:  |  Height:  |  Size: 1007 B

View File

@@ -7,23 +7,16 @@
<name>Neochat</name>
<name xml:lang="ca">Neochat</name>
<name xml:lang="ca-valencia">Neochat</name>
<name xml:lang="cs">Neochat</name>
<name xml:lang="da">Neochat</name>
<name xml:lang="de">Neochat</name>
<name xml:lang="en-GB">Neochat</name>
<name xml:lang="es">Neochat</name>
<name xml:lang="eu">Neochat</name>
<name xml:lang="fi">Neochat</name>
<name xml:lang="fr">Neochat</name>
<name xml:lang="hu">Neochat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">Neochat</name>
<name xml:lang="it">Neochat</name>
<name xml:lang="nl">Neochat</name>
<name xml:lang="nn">Neochat</name>
<name xml:lang="pl">Neochat</name>
<name xml:lang="pt">Neochat</name>
<name xml:lang="pt-BR">Neochat</name>
<name xml:lang="sk">Neochat</name>
<name xml:lang="sl">Neochat</name>
<name xml:lang="sv">Neochat</name>
@@ -32,23 +25,15 @@
<summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldings­protokollen</summary>
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
@@ -57,23 +42,15 @@
<p>A client for matrix, the decentralized communication protocol.</p>
<p xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p>
<p xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat.</p>
<p xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix.</p>
<p xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll.</p>
<p xml:lang="en-GB">A client for matrix, the decentralised communication protocol.</p>
<p xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado.</p>
<p xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat.</p>
<p xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle.</p>
<p xml:lang="fr">Un client « Matrix », le protocole décentralisé de communications.</p>
<p xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz.</p>
<p xml:lang="ia">Un cliente per matrix, le protocollo de communication decentralisate.</p>
<p xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi.</p>
<p xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato.</p>
<p xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol.</p>
<p xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldings­protokollen.</p>
<p xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się.</p>
<p xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado.</p>
<p xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado.</p>
<p xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol.</p>
<p xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol.</p>
<p xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet.</p>
<p xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними.</p>
@@ -82,27 +59,21 @@
<url type="homepage">https://kde.org</url>
<url type="bugtracker">https://bugs.kde.org</url>
<categories>
<category>Network</category>
<category>Matrix</category>
<category>Internet</category>
</categories>
<developer_name>The KDE Community</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">A Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A comunidade KDE</developer_name>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
@@ -110,49 +81,15 @@
<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 urgency="critical" version="1.1.1" date="2021-02-23"/>
<release version="1.1.0" date="2021-02-22">
<description>
<p>Probably the highlight of this release is the completely new login page. It detects the server configuration based on your Matrix Id. This allows you to login to servers requiring Single Sign On (SSO) (like the Mozilla or the incoming Fedora Matrix instance).</p>
<p>Servers that require agreeing to the TOS before usage are correctly detected now and redirect to their TOS webpage, allowing the user to agree to them instead of silently failing to load the account.</p>
<p>It is now possible to open a room into a new window. This allows you to view and interact with multiple rooms at the same time.</p>
<p>We added a few commands to NeoChat (/shrug, /lenny, /join, /ignore, ...).</p>
<p>We improved the Plasma integration a bit. Now the number of unread messages is displayed in the Plasma Taskbar.</p>
</description>
<url>https://carlschwan.eu/2020/02/22/neochat-1.1/</url>
</release>
<release version="1.0.1" date="2021-01-13">
<description>
<p>This version fixes several bugs.</p>
<ul>
<li>NeoChat doesn't require a .well-know configuration in the server to work.</li>
<li>Edited messages won't show up duplicated anymore.</li>
<li>Various graphic glitches have been fixed.</li>
<li>NeoChat now ask for consent to terms and conditions if required instead of displaying nothing.</li>
<li>Users avatar in the room list are now displayed correctly.</li>
<li>Fix image saving</li>
</ul>
</description>
<url>https://carlschwan.eu/2020/01/13/neochat-1.0.1/</url>
</release>
<release version="1.0" date="2020-12-23">
<description>
<p>Initial release of NeoChat, the KDE matrix client.</p>
</description>
<url>https://carlschwan.eu/2020/12/23/announcing-neochat-1.0-the-kde-matrix-client/</url>
</release>
<release version="alpha" date="2020-12-04"/>
</releases>
</component>

View File

@@ -2,78 +2,54 @@
Name=Neochat
Name[ca]=Neochat
Name[ca@valencia]=Neochat
Name[cs]=Neochat
Name[da]=Neochat
Name[de]=Neochat
Name[en_GB]=Neochat
Name[es]=Neochat
Name[eu]=Neochat
Name[fi]=Neochat
Name[fr]=Neochat
Name[hu]=Neochat
Name[ia]=Neochat
Name[it]=Neochat
Name[nl]=Neochat
Name[nn]=Neochat
Name[pl]=Neochat
Name[pt]=Neochat
Name[pt_BR]=Neochat
Name[ro]=Neochat
Name[sk]=Neochat
Name[sl]=Neochat
Name[sv]=Neochat
Name[uk]=Neochat
Name[x-test]=xxNeochatxx
Name[zh_CN]=Neochat
GenericName=Matrix Client
GenericName[ca]=Client del Matrix
GenericName[ca@valencia]=Client del Matrix
GenericName[cs]=Klient protokolu Matrix
GenericName[de]=Matrix-Programm
GenericName[en_GB]=Matrix Client
GenericName[es]=Cliente para Matrix
GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix »
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrix
GenericName[it]=Client Matrix
GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient
GenericName[pl]=Program Matriksa
GenericName[pt]=Cliente de Matrix
GenericName[pt_BR]=Cliente Matrix
GenericName[ro]=Client Matrix
GenericName[sk]=Matrix Client
GenericName[sl]=Odjemalec Matrix
GenericName[sv]=Matrix-klient
GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
Comment=Client for the Matrix protocol
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
Comment[de]=Programm für das Matrix-Protokoll
Comment[en_GB]=Client for the Matrix protocol
Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[it]=Client per il protocollo Matrix
Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix
Comment[uk]=Клієнт протоколу Matrix
Comment[x-test]=xxClient for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的客户端
Exec=neochat
Terminal=false
Icon=org.kde.neochat

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
@@ -171,13 +136,14 @@ Kirigami.ApplicationWindow {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms")
},
Kirigami.Action {
text: i18n("Start a Chat")
icon.name: "irc-join-channel"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat")
},
Kirigami.Action {
text: i18n("Create a Room")
@@ -186,46 +152,27 @@ Kirigami.ApplicationWindow {
let dialog = createRoomDialog.createObject(root.overlay);
dialog.open();
}
shortcut: StandardKey.New
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat")
},
Kirigami.Action {
text: i18n("Accounts")
icon.name: "im-user"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/AccountsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Accounts") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Devices")
iconName: "network-connect"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/DevicesPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Devices") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Accounts")
},
Kirigami.Action {
text: i18n("Settings")
icon.name: "settings-configure"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: StandardKey.Preferences
shortcut: Controller.preferencesShortcuts[0]
},
Kirigami.Action {
text: i18n("About Neochat")
icon.name: "help-about"
onTriggered: pushReplaceLayer(aboutPage)
enabled: pageStack.layers.currentItem.title !== i18n("About")
},
Kirigami.Action {
text: i18n("Logout")
icon.name: "list-remove-user"
enabled: Controller.accountCount > 0
onTriggered: Controller.logout(Controller.activeConnection, true)
},
Kirigami.Action {
text: i18n("Quit")
icon.name: "gtk-quit"
shortcut: StandardKey.Quit
onTriggered: Qt.quit()
}
]
}
@@ -237,98 +184,59 @@ 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)
}
function onUserConsentRequired(url) {
consentSheet.url = url
consentSheet.open()
}
onOpenRoom: roomManager.enterRoom(room)
}
Connections {
target: Controller.activeConnection
onDirectChatAvailable: {
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
}
RoomListModel {
id: spectralRoomListModel
connection: Controller.activeConnection
}
Kirigami.OverlaySheet {
id: consentSheet
Component {
id: roomPage
property string url: ""
header: Kirigami.Heading {
text: i18n("User consent")
}
QQC2.Label {
id: label
text: i18n("Your homeserver requires you to agree to its terms and conditions before being able to use it. Please click the button below to read them.")
wrapMode: Text.WordWrap
width: parent.width
}
footer: QQC2.Button {
text: i18n("Open")
onClicked: Qt.openUrlExternally(consentSheet.url)
}
RoomPage {}
}
Component {
@@ -336,42 +244,4 @@ Kirigami.ApplicationWindow {
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,69 @@ 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
{
if (!m_room || !m_document) {
return;
}
QString cleanedText = text;
cleanedText = cleanedText.trimmed();
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(cleanedText, 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;
/// 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, [=] {
@@ -85,6 +76,7 @@ Controller::Controller(QObject *parent)
Controller::~Controller()
{
for (auto c : qAsConst(m_connections)) {
c->stopSync();
c->saveState();
}
}
@@ -102,6 +94,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(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::homeserverChanged, 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()) {
@@ -161,7 +198,7 @@ void Controller::logout(Connection *conn, bool serverSideLogout)
conn->stopSync();
Q_EMIT conn->stateChanged();
Q_EMIT conn->loggedOut();
if (conn == activeConnection() && !m_connections.isEmpty()) {
if (!m_connections.isEmpty()) {
setActiveConnection(m_connections[0]);
} else {
setActiveConnection(nullptr);
@@ -195,18 +232,11 @@ void Controller::addConnection(Connection *c)
dropConnection(c);
});
connect(c, &Connection::requestFailed, this, [=] (BaseJob *job) {
if(job->error() == BaseJob::UserConsentRequiredError) {
Q_EMIT userConsentRequired(job->errorUrl());
}
});
setBusy(true);
c->sync();
Q_EMIT connectionAdded(c);
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(Connection *c)
@@ -215,56 +245,55 @@ void Controller::dropConnection(Connection *c)
m_connections.removeOne(c);
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
c->deleteLater();
}
void Controller::invokeLogin()
{
const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
}
AccountSettings account {accountId};
if (!account.homeserver().isEmpty()) {
auto accessToken = loadAccessTokenFromKeyChain(account);
auto connection = new Connection(account.homeserver(), this);
connect(connection, &Connection::connected, this, [=] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
Q_EMIT initiated();
}
auto c = new Connection(account.homeserver(), this);
connect(c, &Connection::connected, this, [=] {
c->loadState();
addConnection(c);
});
connect(connection, &Connection::loginError, this, [=](const QString &error, const QString &) {
connect(c, &Connection::loginError, this, [=](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed"), i18n("Access Token invalid or revoked"));
logout(connection, false);
logout(c, false);
} else {
Q_EMIT errorOccured(i18n("Login Failed"), error);
logout(connection, true);
logout(c, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
connect(c, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured("Network Error", error);
});
connection->connectWithToken(account.userId(), accessToken, account.deviceId());
c->connectWithToken(account.userId(), accessToken, account.deviceId());
}
}
if (accounts.isEmpty()) {
Q_EMIT initiated();
if (!m_connections.isEmpty()) {
const QString id = NeoChatConfig::self()->activeConnection();
for (auto *connection : qAsConst(m_connections)) {
if (connection->userId() == id) {
setActiveConnection(connection);
Q_EMIT initiated();
return;
}
}
setActiveConnection(m_connections[0]);
}
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 +331,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 +354,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 +393,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 +559,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,19 +21,21 @@ class QKeySequences;
#include "user.h"
class NeoChatRoom;
class QQuickWindow;
using namespace Quotient;
class Controller : public QObject
{
Q_OBJECT
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
Q_PROPERTY(int accountCount READ accountCount NOTIFY connectionAdded NOTIFY connectionDropped)
Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged)
Q_PROPERTY(Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
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,15 +95,14 @@ 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);
void connectionDropped(Quotient::Connection *_t1);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
void quitOnLastWindowClosedChanged();
@@ -109,15 +112,15 @@ Q_SIGNALS:
void passwordStatus(Controller::PasswordStatus _t1);
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 +130,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;
@@ -64,14 +58,6 @@ int main(int argc, char *argv[])
QQuickStyle::setStyle(QStringLiteral("Material"));
#else
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
}
#endif
#ifdef Q_OS_WINDOWS
QApplication::setStyle(QStringLiteral("breeze"));
#endif
QApplication::setOrganizationName("KDE");
@@ -85,39 +71,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 +110,6 @@ int main(int argc, char *argv[])
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
qRegisterMetaTypeStreamOperators<Emoji>();
@@ -157,7 +135,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 +147,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

@@ -54,9 +54,6 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
void ThumbnailResponse::startRequest()
{
if (!Controller::instance().activeConnection()) {
return;
}
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == Controller::instance().activeConnection()->thread());
job = Controller::instance().activeConnection()->getThumbnail(mediaId, requestedSize);
@@ -97,9 +94,6 @@ void ThumbnailResponse::prepareResult()
void ThumbnailResponse::doCancel()
{
if (!Controller::instance().activeConnection()) {
return;
}
// Runs in the main thread, not QML thread
if (job) {
Q_ASSERT(QThread::currentThread() == job->thread());

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

@@ -14,8 +14,6 @@
#include <events/simplestateevents.h>
#include <user.h>
#include "stickerevent.h"
#include <QDebug>
#include <QQmlEngine> // for qmlRegisterType()
#include <QTimeZone>
@@ -46,8 +44,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited";
roles[FormattedBodyRole] = "formattedBody";
return roles;
}
@@ -65,8 +61,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());
@@ -93,9 +89,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
m_currentRoom = room;
if (room) {
room->setDisplayed();
if (m_currentRoom->timelineSize() < 10) {
room->getPreviousContent(50);
}
lastReadEventId = room->readMarkerEventId();
using namespace Quotient;
@@ -302,13 +295,12 @@ 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)},
{"avatarUrl", user->avatarUrl(room)},
{"displayName", user->displayname(room)},
{"display", user->name()},
{"color", user->color()},
{"object", QVariant::fromValue(user)},
};
@@ -332,20 +324,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);
}
@@ -376,9 +357,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return "message";
}
if (is<const StickerEvent>(evt)) {
return "sticker";
}
if (evt.isStateEvent()) {
return "state";
}
@@ -415,10 +393,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) {
@@ -441,11 +415,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(&evt))
if (!e->replacedEvent().isEmpty())
return EventStatus::Hidden;
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
return EventStatus::Hidden;
}
@@ -470,13 +439,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return EventStatus::Normal;
}
if (role == IsEditedRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
return !e->unsignedJson().isEmpty() && e->unsignedJson().contains("m.relations") && e->unsignedJson()["m.relations"].toObject().contains("m.replace");
}
return false;
}
if (role == EventIdRole) {
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
}
@@ -487,9 +449,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) {
@@ -526,7 +485,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) {
@@ -580,7 +539,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,
@@ -41,8 +40,6 @@ public:
ReactionRole,
IsEditedRole,
// For debugging
EventResolvedTypeRole,
};
@@ -91,3 +88,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 "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;
}
@@ -177,11 +161,12 @@ const RoomMessageEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
QString NeoChatRoom::lastEventToString() const
{
if (auto event = lastEvent()) {
return user(event->senderId())->displayname(this) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
return user(event->senderId())->displayname() + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
}
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));
}
}
@@ -306,7 +283,7 @@ QString NeoChatRoom::avatarMediaId() const
const auto dcUsers = directChatUsers();
for (const auto u : dcUsers) {
if (u != localUser()) {
return u->avatarMediaId(this);
return u->avatarMediaId();
}
}
@@ -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();
}
@@ -305,9 +256,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
}
NeoChatRoom *room = m_rooms.at(index.row());
if (role == NameRole) {
return !room->name().isEmpty() ? room->name() : room->displayName();
}
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == AvatarRole) {
@@ -376,7 +324,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[DisplayNameRole] = "displayName";
roles[AvatarRole] = "avatar";
roles[TopicRole] = "topic";
roles[CategoryRole] = "category";
@@ -405,7 +352,7 @@ QString RoomListModel::categoryName(int section)
case 5:
return i18n("Low priority");
default:
return "Deadbeef";
return i18n("Deadbeef");
}
}
@@ -429,13 +376,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"
@@ -39,7 +39,6 @@ class RoomListModel : public QAbstractListModel
public:
enum EventRoles {
NameRole = Qt::UserRole + 1,
DisplayNameRole,
AvatarRole,
TopicRole,
CategoryRole,
@@ -80,8 +79,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 +104,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