Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
143bd456de | ||
|
|
513de82515 | ||
|
|
581fd5f212 | ||
|
|
2452f630d0 | ||
|
|
2cdc37c3d5 | ||
|
|
5af99a872c | ||
|
|
5fe7ba478b | ||
|
|
f25bc6bac6 | ||
|
|
46ee015775 | ||
|
|
f93be04b2b | ||
|
|
f2d9ca13d8 | ||
|
|
eb04f4adf1 | ||
|
|
6b983332af | ||
|
|
4e197c3cc8 | ||
|
|
bfd6d2ffe2 | ||
|
|
421422edd0 | ||
|
|
0eda9608ec | ||
|
|
06fd8630d9 | ||
|
|
e7d8c4b69c | ||
|
|
44d4269978 | ||
|
|
759244b5d2 | ||
|
|
92a307747f | ||
|
|
627929203f | ||
|
|
0c449ab4bd | ||
|
|
45c35b3cbe | ||
|
|
ecd6a63564 | ||
|
|
b75bf3a75b | ||
|
|
43288db000 | ||
|
|
0d6c793a5e | ||
|
|
6daf184a60 | ||
|
|
93173e3f43 | ||
|
|
ec4aa320c1 | ||
|
|
079a1c8a34 | ||
|
|
e9bb0972a9 | ||
|
|
1717790096 | ||
|
|
893bc79f1e | ||
|
|
e87ae48f17 | ||
|
|
97bbcf3062 | ||
|
|
9f9498541a | ||
|
|
50a4d0a33f | ||
|
|
85b4d7d049 | ||
|
|
b574849df3 | ||
|
|
e802d7f805 | ||
|
|
99438011ca | ||
|
|
99ccfaf93e | ||
|
|
c56973763c | ||
|
|
c96109e9b7 | ||
|
|
3f01b3badf | ||
|
|
093412c788 | ||
|
|
c5ddb61981 | ||
|
|
ad4e52b20d | ||
|
|
5991d59ddd | ||
|
|
de49a26462 | ||
|
|
4924702c15 | ||
|
|
8060edd1c6 | ||
|
|
89bf5d3a31 | ||
|
|
60762b934c | ||
|
|
7729fec259 | ||
|
|
026769b07f | ||
|
|
5be14a4b8f | ||
|
|
0b70d2b33f | ||
|
|
dab77b8d07 | ||
|
|
2acdf61b16 | ||
|
|
defa3d4b77 | ||
|
|
be709a2732 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,5 +3,3 @@ build
|
||||
.DS_Store
|
||||
.kdev4/
|
||||
neochat.kdev4
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
BIN
128-logo.png
BIN
128-logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.1)
|
||||
|
||||
project(Neochat)
|
||||
|
||||
set(KF5_MIN_VERSION "5.77.0")
|
||||
set(KF5_MIN_VERSION "5.76.0")
|
||||
set(QT_MIN_VERSION "5.15.0")
|
||||
|
||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
@@ -19,16 +19,13 @@ include(ECMQMLModules)
|
||||
include(KDEClangFormat)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
include(ECMAddAppIcon)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
include(cmake/Flatpak.cmake)
|
||||
endif()
|
||||
include(cmake/Flatpak.cmake)
|
||||
|
||||
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
|
||||
ecm_setup_version(1.1.0
|
||||
ecm_setup_version(1.0.1
|
||||
VARIABLE_PREFIX NEOCHAT
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
|
||||
)
|
||||
@@ -60,10 +57,11 @@ else()
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Secure storage of account secrets"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
|
||||
find_package(KF5DBusAddons ${KF5_MIN_VERSION})
|
||||
set_package_properties(KF5DBusAddons PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "DBus convenience functions"
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(Quotient 0.6)
|
||||
@@ -96,12 +94,9 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||
install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
|
||||
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16/apps RENAME org.kde.neochat.svg)
|
||||
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@2/apps RENAME org.kde.neochat.svg)
|
||||
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@3/apps RENAME org.kde.neochat.svg)
|
||||
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/symbolic/apps)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
|
||||
|
||||
# add_definitions(-DQT_NO_KEYWORDS) Need to fix libQuotient first
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,10 +22,8 @@ ToolBar {
|
||||
property alias isReply: replyItem.visible
|
||||
property bool isReaction: false
|
||||
property var replyUser
|
||||
property string replyEventID: ""
|
||||
property string replyContent: ""
|
||||
|
||||
property string editEventId
|
||||
property string replyEventID
|
||||
property string replyContent
|
||||
|
||||
property alias isAutoCompleting: autoCompleteListView.visible
|
||||
property var autoCompleteModel
|
||||
@@ -33,10 +31,7 @@ ToolBar {
|
||||
property int autoCompleteEndPosition
|
||||
|
||||
property bool hasAttachment: false
|
||||
property url attachmentPath: ""
|
||||
property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
|
||||
property bool hasImageAttachment: hasAttachment && attachmentMimetype.valid
|
||||
&& FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
||||
property url attachmentPath
|
||||
|
||||
position: ToolBar.Footer
|
||||
|
||||
@@ -197,12 +192,13 @@ ToolBar {
|
||||
Image {
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
|
||||
source: attachmentPath
|
||||
visible: hasImageAttachment
|
||||
visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
|
||||
fillMode: Image.PreserveAspectFit
|
||||
Layout.preferredWidth: paintedWidth
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
Button {
|
||||
visible: isImage
|
||||
icon.name: "document-edit"
|
||||
|
||||
// HACK: Use a component because an url doesn't work
|
||||
@@ -237,7 +233,7 @@ ToolBar {
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
color: Qt.rgba(255, 255, 255, 40)
|
||||
color: rgba(255, 255, 255, 40)
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -252,7 +248,7 @@ ToolBar {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: hasAttachment && !hasImageAttachment
|
||||
visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
|
||||
ToolButton {
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: {
|
||||
@@ -261,33 +257,12 @@ ToolBar {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
id: mimetypeIcon
|
||||
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(horizontalFileLabel.implicitHeight)
|
||||
implicitWidth: implicitHeight
|
||||
source: attachmentMimetype.iconName
|
||||
}
|
||||
|
||||
Label {
|
||||
id: horizontalFileLabel
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: editEventId.length > 0
|
||||
ToolButton {
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: clearEditReply();
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: i18n("Edit Message")
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
@@ -306,191 +281,187 @@ ToolBar {
|
||||
|
||||
icon.name: "dialog-cancel"
|
||||
|
||||
onClicked: clearEditReply()
|
||||
onClicked: clearReply()
|
||||
}
|
||||
|
||||
|
||||
ScrollView {
|
||||
TextArea {
|
||||
id: inputField
|
||||
property real progress: 0
|
||||
property bool autoAppeared: false
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
document: inputField.textDocument
|
||||
cursorPosition: inputField.cursorPosition
|
||||
selectionStart: inputField.selectionStart
|
||||
selectionEnd: inputField.selectionEnd
|
||||
room: currentRoom ?? null
|
||||
}
|
||||
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: inputField.lineHeight * 8
|
||||
TextArea {
|
||||
id: inputField
|
||||
property real progress: 0
|
||||
property bool autoAppeared: false
|
||||
|
||||
// store each user we autoComplete here, this will be helpful later to generate
|
||||
// the matrix.to links.
|
||||
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
|
||||
property var userAutocompleted: ({})
|
||||
wrapMode: Text.Wrap
|
||||
placeholderText: i18n("Write your message...")
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
selectByMouse: true
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
document: inputField.textDocument
|
||||
cursorPosition: inputField.cursorPosition
|
||||
selectionStart: inputField.selectionStart
|
||||
selectionEnd: inputField.selectionEnd
|
||||
room: currentRoom ?? null
|
||||
text: currentRoom != null ? currentRoom.cachedInput : ""
|
||||
|
||||
background: Item {}
|
||||
|
||||
Rectangle {
|
||||
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
|
||||
height: parent.height
|
||||
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timeoutTimer
|
||||
|
||||
repeat: false
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
repeatTimer.stop()
|
||||
currentRoom.sendTypingNotification(false)
|
||||
}
|
||||
}
|
||||
|
||||
property int lineHeight: contentHeight / lineCount
|
||||
Timer {
|
||||
id: repeatTimer
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
placeholderText: i18n("Write your message...")
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
selectByMouse: true
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
|
||||
text: currentRoom != null ? currentRoom.cachedInput : ""
|
||||
|
||||
background: MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
z: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
|
||||
height: parent.height
|
||||
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timeoutTimer
|
||||
|
||||
repeat: false
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
repeatTimer.stop()
|
||||
currentRoom.sendTypingNotification(false)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: repeatTimer
|
||||
|
||||
repeat: true
|
||||
interval: 5000
|
||||
triggeredOnStart: true
|
||||
onTriggered: currentRoom.sendTypingNotification(true)
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (isAutoCompleting) {
|
||||
inputField.autoComplete();
|
||||
|
||||
isAutoCompleting = false;
|
||||
return;
|
||||
}
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
insert(cursorPosition, "\n")
|
||||
} else {
|
||||
postMessage()
|
||||
text = ""
|
||||
clearEditReply()
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
clearEditReply();
|
||||
closeAll();
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
switchRoomDown();
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
switchRoomUp();
|
||||
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||
root.pasteImage();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomUp();
|
||||
return;
|
||||
}
|
||||
if (isAutoCompleting) {
|
||||
autoCompleteListView.decrementCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomDown();
|
||||
return;
|
||||
}
|
||||
if (!isAutoCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO detect moved cursor
|
||||
|
||||
// ignore first time tab was clicked so that user can select
|
||||
// first emoji/user
|
||||
if (autoAppeared === false) {
|
||||
autoCompleteListView.incrementCurrentIndex()
|
||||
} else {
|
||||
autoAppeared = false;
|
||||
}
|
||||
repeat: true
|
||||
interval: 5000
|
||||
triggeredOnStart: true
|
||||
onTriggered: currentRoom.sendTypingNotification(true)
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (isAutoCompleting) {
|
||||
inputField.autoComplete();
|
||||
|
||||
isAutoCompleting = false;
|
||||
return;
|
||||
}
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
insert(cursorPosition, "\n")
|
||||
} else {
|
||||
postMessage()
|
||||
text = ""
|
||||
clearReply()
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
clearReply();
|
||||
closeAll();
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
switchRoomDown();
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
switchRoomUp();
|
||||
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||
root.pasteImage();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomUp();
|
||||
return;
|
||||
}
|
||||
if (isAutoCompleting) {
|
||||
autoCompleteListView.decrementCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomDown();
|
||||
return;
|
||||
}
|
||||
if (!isAutoCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
timeoutTimer.restart()
|
||||
repeatTimer.start()
|
||||
currentRoom.cachedInput = text
|
||||
// TODO detect moved cursor
|
||||
|
||||
// ignore first time tab was clicked so that user can select
|
||||
// first emoji/user
|
||||
if (autoAppeared === false) {
|
||||
autoCompleteListView.incrementCurrentIndex()
|
||||
} else {
|
||||
autoAppeared = false;
|
||||
|
||||
const autocompletionInfo = documentHandler.getAutocompletionInfo();
|
||||
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
|
||||
return;
|
||||
}
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.None) {
|
||||
isAutoCompleting = false;
|
||||
autoCompleteListView.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.User) {
|
||||
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
|
||||
} else {
|
||||
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
|
||||
}
|
||||
|
||||
if (autoCompleteModel.length === 0) {
|
||||
isAutoCompleting = false;
|
||||
autoCompleteListView.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
isAutoCompleting = true
|
||||
autoAppeared = true;
|
||||
autoCompleteEndPosition = cursorPosition
|
||||
}
|
||||
|
||||
function postMessage() {
|
||||
roomManager.actionsHandler.postMessage(inputField.text.trim(), attachmentPath,
|
||||
replyEventID, editEventId, inputField.userAutocompleted);
|
||||
clearAttachment();
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
clear();
|
||||
text = Qt.binding(function() {
|
||||
return currentRoom != null ? currentRoom.cachedInput : "";
|
||||
});
|
||||
inputField.autoComplete();
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
timeoutTimer.restart()
|
||||
repeatTimer.start()
|
||||
currentRoom.cachedInput = text
|
||||
autoAppeared = false;
|
||||
|
||||
const autocompletionInfo = documentHandler.getAutocompletionInfo();
|
||||
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
|
||||
return;
|
||||
}
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.None) {
|
||||
isAutoCompleting = false;
|
||||
autoCompleteListView.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
function autoComplete() {
|
||||
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
|
||||
if (!autoCompleteListView.currentItem.isEmoji) {
|
||||
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
|
||||
}
|
||||
if (autocompletionInfo.type === ChatDocumentHandler.User) {
|
||||
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
|
||||
} else {
|
||||
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
|
||||
}
|
||||
|
||||
if (autoCompleteModel.length === 0) {
|
||||
isAutoCompleting = false;
|
||||
autoCompleteListView.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
isAutoCompleting = true
|
||||
autoAppeared = true;
|
||||
autoCompleteEndPosition = cursorPosition
|
||||
}
|
||||
|
||||
// store each user we autoComplete here, this will be helpful later to generate
|
||||
// the matrix.to links.
|
||||
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
|
||||
property var userAutocompleted: ({})
|
||||
|
||||
|
||||
function postMessage() {
|
||||
// Qt wraps lines so we need to use a small hack
|
||||
// to remove the wrapped lines but not break the empty
|
||||
// lines.
|
||||
documentHandler.postMessage(inputField.text.trim(), attachmentPath, replyEventID,
|
||||
inputField.userAutocompleted);
|
||||
clearAttachment();
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
clear();
|
||||
text = Qt.binding(function() {
|
||||
return currentRoom != null ? currentRoom.cachedInput : "";
|
||||
});
|
||||
}
|
||||
|
||||
function autoComplete() {
|
||||
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
|
||||
if (!autoCompleteListView.currentItem.isEmoji) {
|
||||
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,12 +519,10 @@ ToolBar {
|
||||
icon.name: "document-send"
|
||||
icon.color: "transparent"
|
||||
|
||||
enabled: inputField.length > 0 || hasAttachment
|
||||
|
||||
onClicked: {
|
||||
inputField.postMessage()
|
||||
inputField.text = ""
|
||||
root.clearEditReply()
|
||||
root.clearReply()
|
||||
root.closeAll()
|
||||
}
|
||||
|
||||
@@ -582,46 +551,21 @@ ToolBar {
|
||||
}
|
||||
|
||||
function clear() {
|
||||
inputField.clear();
|
||||
inputField.clear()
|
||||
inputField.userAutocompleted = {};
|
||||
}
|
||||
|
||||
function clearEditReply() {
|
||||
isReply = false;
|
||||
function clearReply() {
|
||||
isReply = false
|
||||
replyUser = null;
|
||||
clear();
|
||||
root.replyContent = "";
|
||||
root.replyEventID = "";
|
||||
root.editEventId = "";
|
||||
focus();
|
||||
replyContent = "";
|
||||
replyEventID = ""
|
||||
}
|
||||
|
||||
function focus() {
|
||||
inputField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function edit(editContent, editFormatedContent, editEventId) {
|
||||
console.log("Editing ", editContent, "html:", editFormatedContent)
|
||||
// Set the input field in edit mode
|
||||
inputField.text = editContent;
|
||||
root.editEventId = editEventId
|
||||
|
||||
// clean autocompletion list
|
||||
inputField.userAutocompleted = {};
|
||||
|
||||
// Fill autocompletion list with values extracted from message.
|
||||
// We can't just iterate on every user in the list and try to
|
||||
// find matching display name since some users have display name
|
||||
// matching frequent words and this will marks too many words as
|
||||
// mentions.
|
||||
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
|
||||
inputField.userAutocompleted[match[2]] = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
replyItem.visible = false
|
||||
autoCompleteListView.visible = false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -107,7 +107,6 @@ RowLayout {
|
||||
function saveFileAs() {
|
||||
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
||||
}
|
||||
|
||||
function downloadAndOpen()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,7 +51,7 @@ RowLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
visible: showAuthor && Config.showAvatarInTimeline
|
||||
name: author.name ?? author.displayName
|
||||
name: author.name
|
||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
||||
color: author.color
|
||||
|
||||
@@ -95,7 +95,7 @@ RowLayout {
|
||||
visible: showAuthor && !isEmote
|
||||
|
||||
text: author.displayName
|
||||
font.weight: Font.Bold
|
||||
font.bold: true
|
||||
color: author.color
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
@@ -112,9 +112,7 @@ RowLayout {
|
||||
}
|
||||
Connections {
|
||||
target: replyLoader.item
|
||||
function onClicked() {
|
||||
replyClicked(reply.eventId)
|
||||
}
|
||||
onClicked: replyClicked(reply.eventId)
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
@@ -134,13 +132,6 @@ RowLayout {
|
||||
onReact: currentRoom.toggleReaction(eventId, emoji)
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Edit")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message")
|
||||
icon.name: "document-edit"
|
||||
onClicked: chatTextInput.edit(message, model.formattedBody, eventId)
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Reply")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
|
||||
@@ -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 += ", "
|
||||
}
|
||||
|
||||
@@ -15,28 +15,8 @@ TextEdit {
|
||||
|
||||
property bool isEmote: false
|
||||
|
||||
text: "<style>
|
||||
table {
|
||||
width:100%;
|
||||
border-width: 1px;
|
||||
border-collapse: collapse;
|
||||
border-style: solid;
|
||||
}
|
||||
table th,
|
||||
table td {
|
||||
border: 1px solid black;
|
||||
padding: 3px;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
a{
|
||||
color: " + Kirigami.Theme.linkColor + ";
|
||||
text-decoration: none;
|
||||
}
|
||||
text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
|
||||
|
||||
.user-pill{}
|
||||
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
|
||||
@@ -46,7 +26,15 @@ a{
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: {
|
||||
applicationWindow().handleLink(link, currentRoom)
|
||||
if (link.startsWith("https://matrix.to/")) {
|
||||
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
|
||||
if (!result || result.length < 3) return
|
||||
if (result[1] != currentRoom.id) return
|
||||
if (!result[2]) return
|
||||
goToEvent(result[2])
|
||||
} else {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ QQC2.Popup {
|
||||
implicitHeight: Kirigami.Units.gridUnit * 20
|
||||
|
||||
contentItem: EmojiPicker {
|
||||
onChosen: react(emoji)
|
||||
onChosen: react(emoji);
|
||||
emojiModel: EmojiModel {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Setting 1.0
|
||||
|
||||
@@ -111,10 +109,7 @@ Kirigami.OverlaySheet {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Kick this user")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: {
|
||||
room.kickMember(user.id)
|
||||
root.close()
|
||||
}
|
||||
onTriggered: room.kickMember(user.id)
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
@@ -124,20 +119,7 @@ Kirigami.OverlaySheet {
|
||||
text: i18n("Ban this user")
|
||||
icon.name: "im-ban-user"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
room.banMember(user.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Open a private chat")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
Controller.activeConnection.requestDirectChat(user)
|
||||
root.close()
|
||||
}
|
||||
onTriggered: room.banMember(user.id)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
50
imports/NeoChat/Page/InvitationPage.qml
Normal file
50
imports/NeoChat/Page/InvitationPage.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@ Kirigami.ScrollablePage {
|
||||
|
||||
property var room
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
title: i18n("Invite a User")
|
||||
|
||||
actions {
|
||||
main: Kirigami.Action {
|
||||
icon.name: "dialog-close"
|
||||
text: i18nc("@action", "Cancel")
|
||||
onTriggered: applicationWindow().pageStack.layers.pop()
|
||||
onTriggered: applicationWindow().pageStack.pop()
|
||||
}
|
||||
}
|
||||
header: RowLayout {
|
||||
@@ -123,7 +125,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
onClicked: {
|
||||
room.inviteToRoom(userID);
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
applicationWindow().pageStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ Kirigami.ScrollablePage {
|
||||
property alias keyword: identifierField.text
|
||||
property string server
|
||||
|
||||
signal joinRoom(string room)
|
||||
|
||||
title: i18n("Explore Rooms")
|
||||
|
||||
header: Control {
|
||||
@@ -49,9 +51,9 @@ Kirigami.ScrollablePage {
|
||||
|
||||
onClicked: {
|
||||
if (!identifierField.isJoined) {
|
||||
roomManager.actionsHandler.joinRoom(identifierField.text);
|
||||
// When joining the room, the room will be opened
|
||||
Controller.joinRoom(connection, identifierField.text);
|
||||
}
|
||||
roomManager.enterRoom(connection.room(identifierField.room));
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
@@ -102,12 +104,12 @@ Kirigami.ScrollablePage {
|
||||
width: publicRoomsListView.width
|
||||
onClicked: {
|
||||
if (!isJoined) {
|
||||
roomManager.actionsHandler.joinRoom(connection, roomID)
|
||||
Controller.joinRoom(connection, roomID)
|
||||
justJoined = true;
|
||||
} else {
|
||||
roomManager.enterRoom(connection.room(roomID))
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
|
||||
97
imports/NeoChat/Page/LoginPage.qml
Normal file
97
imports/NeoChat/Page/LoginPage.qml
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,13 @@ import NeoChat.Menu 1.0
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
property var roomListModel
|
||||
property var enteredRoom
|
||||
required property var activeConnection
|
||||
|
||||
signal enterRoom(var room)
|
||||
signal leaveRoom(var room)
|
||||
|
||||
function goToNextRoom() {
|
||||
do {
|
||||
listView.incrementCurrentIndex();
|
||||
@@ -61,10 +65,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: page.activeConnection
|
||||
}
|
||||
sourceModel: roomListModel
|
||||
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
|
||||
}
|
||||
|
||||
@@ -76,50 +77,100 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
implicitHeight: categoryName.implicitHeight
|
||||
Kirigami.Icon {
|
||||
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
Kirigami.Heading {
|
||||
id: categoryName
|
||||
level: 3
|
||||
text: roomListModel.categoryName(section)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Kirigami.Icon {
|
||||
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Kirigami.BasicListItem {
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
id: roomListItem
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
property bool itemVisible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
visible: itemVisible
|
||||
highlighted: roomManager.currentRoom && roomManager.currentRoom.displayName === displayName
|
||||
focus: true
|
||||
icon: undefined
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
var roomItem = roomManager.enterRoom(currentRoom)
|
||||
roomListItem.KeyNavigation.right = roomItem
|
||||
roomItem.focus = true;
|
||||
if (category === RoomType.Invited) {
|
||||
roomManager.openInvitation(currentRoom);
|
||||
} else {
|
||||
var roomItem = roomManager.enterRoom(currentRoom)
|
||||
roomListItem.KeyNavigation.right = roomItem
|
||||
roomItem.focus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label: name ?? ""
|
||||
subtitle: {
|
||||
let txt = (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
|
||||
if (txt.length) {
|
||||
return txt
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: roomLayout
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
width: listView.width
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
leading: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
implicitWidth: height
|
||||
}
|
||||
TapHandler {
|
||||
onTapped: enterRoomAction.trigger()
|
||||
onLongPressed: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
|
||||
}
|
||||
|
||||
trailing: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
id: roomAvatar
|
||||
property int size: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing
|
||||
Layout.minimumHeight: size
|
||||
Layout.maximumHeight: size
|
||||
Layout.minimumWidth: size
|
||||
Layout.maximumWidth: size
|
||||
|
||||
source: avatar ? ("image://mxc/" + avatar) : ""
|
||||
name: model.name || i18n("No Name")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: roomitemcolumn
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: displayName ?? ""
|
||||
elide: Text.ElideRight
|
||||
font.bold: unreadCount >= 0 || highlightCount > 0 || notificationCount > 0
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.7)
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
text: notificationCount
|
||||
visible: notificationCount > 0
|
||||
@@ -133,26 +184,6 @@ Kirigami.ScrollablePage {
|
||||
radius: height / 2
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: roomListItem.hovered || Kirigami.Settings.isMobile
|
||||
Accessible.name: i18n("Configure room")
|
||||
|
||||
action: Kirigami.Action {
|
||||
id: optionAction
|
||||
icon.name: "configure"
|
||||
onTriggered: {
|
||||
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
|
||||
configButton.visible = true
|
||||
configButton.down = true
|
||||
menu.closed.connect(function() {
|
||||
configButton.down = undefined
|
||||
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
|
||||
})
|
||||
menu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
|
||||
@@ -23,22 +23,11 @@ import NeoChat.Menu.Timeline 1.0
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
required property var currentRoom
|
||||
|
||||
title: currentRoom.displayName
|
||||
property var currentRoom
|
||||
|
||||
signal switchRoomUp()
|
||||
signal switchRoomDown()
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
function onJoinedRoom(room) {
|
||||
if(room.id === invitation.id) {
|
||||
roomManager.enterRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: roomManager.actionsHandler
|
||||
onShowMessage: {
|
||||
@@ -60,46 +49,37 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: invitation
|
||||
title: currentRoom.displayName
|
||||
|
||||
property var id
|
||||
|
||||
visible: currentRoom && currentRoom.isInvite
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Accept this invitation?")
|
||||
titleDelegate: Component {
|
||||
RowLayout {
|
||||
QQC2.Button {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Reject")
|
||||
|
||||
onClicked: {
|
||||
page.currentRoom.forget()
|
||||
roomManager.getBack();
|
||||
}
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: implicitWidth + 1 // The +1 is to make sure we do not trigger eliding at max width
|
||||
Layout.minimumWidth: 0
|
||||
spacing: Kirigami.Units.gridUnit * 0.8
|
||||
Kirigami.Heading {
|
||||
id: titleLabel
|
||||
level: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: page.title
|
||||
opacity: page.isCurrentPage ? 1 : 0.4
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Accept")
|
||||
|
||||
onClicked: {
|
||||
currentRoom.acceptInvitation();
|
||||
invitation.id = currentRoom.id
|
||||
currentRoom = null
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
anchors.baseline: lineCount < 2 ? titleLabel.baseline : undefined // necessary, since there is no way to do this with Layout.alignment
|
||||
text: currentRoom.topic
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
|
||||
QQC2.BusyIndicator {
|
||||
running: true
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
@@ -130,8 +110,6 @@ Kirigami.ScrollablePage {
|
||||
ListView {
|
||||
id: messageListView
|
||||
|
||||
visible: !invitation.visible
|
||||
|
||||
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
|
||||
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
|
||||
readonly property bool isLoaded: page.width * page.height > 10
|
||||
@@ -168,6 +146,14 @@ Kirigami.ScrollablePage {
|
||||
room: currentRoom
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
visible: messageListView.count === 0 && !currentRoom.allHistoryLoaded
|
||||
QQC2.BusyIndicator {
|
||||
running: true
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Popup {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -226,10 +212,25 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
|
||||
MessageFilterModel {
|
||||
KSortFilterProxyModel {
|
||||
id: sortedMessageEventModel
|
||||
|
||||
sourceModel: messageEventModel
|
||||
|
||||
filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin
|
||||
|
||||
function dontFilterLeaveJoin(row, parent) {
|
||||
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
|
||||
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
|
||||
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other";
|
||||
}
|
||||
|
||||
function filterLeaveJoin(row, parent) {
|
||||
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
|
||||
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
|
||||
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"
|
||||
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state";
|
||||
}
|
||||
}
|
||||
|
||||
// populate: Transition {
|
||||
@@ -322,23 +323,24 @@ Kirigami.ScrollablePage {
|
||||
onReplyClicked: goToEvent(eventID)
|
||||
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
|
||||
innerObject: [
|
||||
MouseArea {
|
||||
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (mouse.button == Qt.RightButton) {
|
||||
openMessageContext(author, display, eventId, toolTip);
|
||||
}
|
||||
}
|
||||
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
|
||||
},
|
||||
TextDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext(author, display, eventId, toolTip)
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
//enabled: Kirigami.Settings.isMobile
|
||||
onLongPressed: openMessageContext(author, display, eventId, toolTip)
|
||||
}
|
||||
},
|
||||
ReactionDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -382,35 +384,7 @@ Kirigami.ScrollablePage {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 0
|
||||
Layout.maximumHeight: 320
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "sticker"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
onReplyClicked: goToEvent(eventID)
|
||||
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
|
||||
|
||||
innerObject: [
|
||||
ImageDelegate {
|
||||
readonly: true
|
||||
Layout.maximumWidth: parent.width / 2
|
||||
Layout.minimumWidth: 320
|
||||
Layout.preferredHeight: info.h / info.w * width
|
||||
},
|
||||
ReactionDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 0
|
||||
Layout.maximumHeight: 320
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -491,7 +465,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "other"
|
||||
delegate: Item {}
|
||||
@@ -590,8 +563,6 @@ Kirigami.ScrollablePage {
|
||||
|
||||
footer: ChatTextInput {
|
||||
id: chatTextInput
|
||||
|
||||
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,6 @@ Kirigami.ScrollablePage {
|
||||
title: i18n("Settings")
|
||||
|
||||
Kirigami.FormLayout {
|
||||
QQC2.CheckBox {
|
||||
Kirigami.FormData.label: i18nc("General settings:")
|
||||
text: i18n("Close to system tray")
|
||||
checked: Config.systemTray
|
||||
onToggled: {
|
||||
Config.systemTray = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
// TODO: When there are enough notification and timeline event
|
||||
// settings, make 2 separate groups with FormData labels.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ Kirigami.OverlayDrawer {
|
||||
icon.name: "list-add-user"
|
||||
text: i18n("Invite")
|
||||
onClicked: {
|
||||
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
|
||||
applicationWindow().pageStack.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
|
||||
roomDrawer.close();
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,12 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
@@ -114,7 +120,6 @@ Kirigami.OverlayDrawer {
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
|
||||
Layout.fillWidth: true
|
||||
level: 1
|
||||
font.bold: true
|
||||
@@ -139,7 +144,6 @@ Kirigami.OverlayDrawer {
|
||||
selectByMouse: true
|
||||
color: Kirigami.Theme.textColor
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
readOnly: true
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
@@ -167,16 +171,6 @@ Kirigami.OverlayDrawer {
|
||||
headerPositioning: ListView.OverlayHeader
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
header: Pane {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
implicitWidth: parent.width
|
||||
z: 2
|
||||
contentItem: Kirigami.SearchField {
|
||||
id: userListSearchField
|
||||
onTextChanged: sortedMessageEventModel.filterString = text;
|
||||
}
|
||||
}
|
||||
|
||||
model: KSortFilterProxyModel {
|
||||
id: sortedMessageEventModel
|
||||
|
||||
@@ -185,13 +179,11 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
|
||||
sortRole: "perm"
|
||||
filterRole: "name"
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
width: userListView.width
|
||||
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||
z: 1
|
||||
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
@@ -229,7 +221,7 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
color: perm == UserType.Muted ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
|
||||
font.pixelSize: 12
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.NoWrap
|
||||
@@ -237,7 +229,7 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
|
||||
action: Kirigami.Action {
|
||||
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open()
|
||||
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,21 +24,28 @@ Name[uk]=Neochat
|
||||
Name[x-test]=xxNeochatxx
|
||||
Name[zh_CN]=Neochat
|
||||
DesktopEntry=org.kde.neochat
|
||||
Comment=A client for matrix, the decentralized communication protocol
|
||||
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[en_GB]=A client for matrix, the decentralised communication protocol
|
||||
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
|
||||
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
|
||||
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
|
||||
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
|
||||
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
|
||||
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldingsprotokollen.
|
||||
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
|
||||
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado
|
||||
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
|
||||
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
|
||||
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
|
||||
|
||||
Comment=IM client for the Matrix protocol
|
||||
Comment[ca]=Client de MI per al protocol Matrix
|
||||
Comment[ca@valencia]=Client de MI per al protocol Matrix
|
||||
Comment[de]=IM-Programm für das Matrix-Protokoll
|
||||
Comment[en_GB]=IM client for the Matrix protocol
|
||||
Comment[es]=Cliente de MI para el protocolo Matrix
|
||||
Comment[eu]=Matrix protokolorako bat-bateko mezularitza bezeroa
|
||||
Comment[fi]=Pikaviestiasiakas Matrix-yhteyskäytännölle
|
||||
Comment[fr]=Client « IM » pour le protocole « Matrix »
|
||||
Comment[hu]=Azonnali üzenetküldő kliens a Matrix protokollhoz
|
||||
Comment[it]=Client di messaggistica istantanea per il protocollo Matrix
|
||||
Comment[nl]=IM-client voor het Matrix-protocol
|
||||
Comment[nn]=Lynmeldingsklient for Matrix-protokollen
|
||||
Comment[pl]=Komunikator internetowy dla protokołu Matrix
|
||||
Comment[pt]=Cliente de MI para o protocolo Matrix
|
||||
Comment[pt_BR]=Cliente de mensageiro instantâneo para o protocolo Matrix
|
||||
Comment[sk]=IM klient pre protokol Matrix
|
||||
Comment[sl]=Odjemalec neposrednega sporočanja po protokolu Matrix
|
||||
Comment[sv]=Direktmeddelandeklient för protokollet Matrix
|
||||
Comment[uk]=Клієнт служби миттєвого обміну повідомленнями для протоколу Matrix
|
||||
Comment[x-test]=xxIM client for the Matrix protocolxx
|
||||
Comment[zh_CN]=为 Matrix 协议打造的 IM 客户端
|
||||
|
||||
[Event/message]
|
||||
Name=New message
|
||||
|
||||
@@ -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 |
@@ -102,29 +102,18 @@
|
||||
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<value key="KDE::matrix">#neochat:kde.org</value>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application-mobile.png</image>
|
||||
<image>https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="1.1.0" date="2021-02-22">
|
||||
<description>
|
||||
<p>Probably the highlight of this release is the completely new login page. It detects the server configuration based on your Matrix Id. This allows you to login to servers requiring Single Sign On (SSO) (like the Mozilla or the incoming Fedora Matrix instance).</p>
|
||||
<p>Servers that require agreeing to the TOS before usage are correctly detected now and redirect to their TOS webpage, allowing the user to agree to them instead of silently failing to load the account.</p>
|
||||
<p>It is now possible to open a room into a new window. This allows you to view and interact with multiple rooms at the same time.</p>
|
||||
<p>We added a few commands to NeoChat (/shrug, /lenny, /join, /ignore, ...).</p>
|
||||
<p>We improved the Plasma integration a bit. Now the number of unread messages is displayed in the Plasma Taskbar.</p>
|
||||
</description>
|
||||
<url>https://carlschwan.eu/2020/02/22/neochat-1.1/</url>
|
||||
</release>
|
||||
<release version="1.0.1" date="2021-01-13">
|
||||
<description>
|
||||
<p>This version fixes several bugs.</p>
|
||||
|
||||
163
qml/main.qml
163
qml/main.qml
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14 as QQC2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
@@ -20,17 +19,11 @@ import NeoChat.Page 1.0
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property var currentRoom: null
|
||||
property int columnWidth: Kirigami.Units.gridUnit * 13
|
||||
|
||||
minimumWidth: Kirigami.Units.gridUnit * 15
|
||||
minimumHeight: Kirigami.Units.gridUnit * 20
|
||||
|
||||
wideScreen: width > columnWidth * 5
|
||||
|
||||
onClosing: Controller.saveWindowGeometry(root)
|
||||
|
||||
pageStack.initialPage: LoadingPage {}
|
||||
|
||||
Connections {
|
||||
target: root.quitAction
|
||||
function onTriggered() {
|
||||
@@ -38,38 +31,15 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// This timer allows to batch update the window size change to reduce
|
||||
// the io load and also work around the fact that x/y/width/height are
|
||||
// changed when loading the page and overwrite the saved geometry from
|
||||
// the previous session.
|
||||
Timer {
|
||||
id: saveWindowGeometryTimer
|
||||
interval: 1000
|
||||
onTriggered: Controller.saveWindowGeometry(root)
|
||||
}
|
||||
|
||||
onWidthChanged: saveWindowGeometryTimer.restart()
|
||||
onHeightChanged: saveWindowGeometryTimer.restart()
|
||||
onXChanged: saveWindowGeometryTimer.restart()
|
||||
onYChanged: saveWindowGeometryTimer.restart()
|
||||
|
||||
/**
|
||||
* Manage opening and close rooms
|
||||
* TODO this should probably be moved to C++
|
||||
*/
|
||||
QtObject {
|
||||
id: roomManager
|
||||
|
||||
property var actionsHandler: ActionsHandler {
|
||||
room: roomManager.currentRoom
|
||||
connection: Controller.activeConnection
|
||||
onRoomJoined: {
|
||||
roomManager.enterRoom(Controller.activeConnection.room(roomName))
|
||||
}
|
||||
}
|
||||
|
||||
property var currentRoom: null
|
||||
property alias pageStack: root.pageStack
|
||||
property bool invitationOpen: false
|
||||
property var roomList: null
|
||||
property Item roomItem: null
|
||||
|
||||
@@ -78,20 +48,11 @@ Kirigami.ApplicationWindow {
|
||||
signal leaveRoom(string room);
|
||||
signal openRoom(string room);
|
||||
|
||||
function roomByAliasOrId(aliasOrId) {
|
||||
return Controller.activeConnection.room(aliasOrId)
|
||||
}
|
||||
|
||||
function openRoomAndEvent(room, event) {
|
||||
enterRoom(room)
|
||||
roomItem.goToEvent(event)
|
||||
}
|
||||
|
||||
function loadInitialRoom() {
|
||||
if (Config.openRoom) {
|
||||
const room = Controller.activeConnection.room(Config.openRoom);
|
||||
currentRoom = room;
|
||||
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
|
||||
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
|
||||
connectRoomToSignal(roomItem);
|
||||
} else {
|
||||
// TODO create welcome page
|
||||
@@ -99,11 +60,12 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
|
||||
function enterRoom(room) {
|
||||
if (currentRoom != null) {
|
||||
let item = null;
|
||||
if (currentRoom != null || invitationOpen) {
|
||||
roomItem.currentRoom = room;
|
||||
pageStack.currentIndex = pageStack.depth - 1;
|
||||
} else {
|
||||
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
|
||||
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
|
||||
}
|
||||
currentRoom = room;
|
||||
Config.openRoom = room.id;
|
||||
@@ -112,14 +74,17 @@ Kirigami.ApplicationWindow {
|
||||
return roomItem;
|
||||
}
|
||||
|
||||
function getBack() {
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': currentRoom, });
|
||||
function openInvitation(room) {
|
||||
if (currentRoom != null) {
|
||||
currentRoom = null;
|
||||
pageStack.removePage(pageStack.lastItem);
|
||||
}
|
||||
invitationOpen = true;
|
||||
pageStack.push("qrc:/imports/NeoChat/Page/InvitationPage.qml", {"room": room});
|
||||
}
|
||||
|
||||
function openWindow(room) {
|
||||
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
|
||||
secondayWindow.width = root.width - roomList.width;
|
||||
secondayWindow.show();
|
||||
function getBack() {
|
||||
pageStack.replace(roomPage, { 'currentRoom': currentRoom, });
|
||||
}
|
||||
|
||||
function connectRoomToSignal(item) {
|
||||
@@ -154,7 +119,7 @@ Kirigami.ApplicationWindow {
|
||||
id: contextDrawer
|
||||
contentItem.implicitWidth: columnWidth
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
modal: !root.wideScreen || !enabled
|
||||
modal: !root.wideScreen
|
||||
onEnabledChanged: drawerOpen = enabled && !modal
|
||||
onModalChanged: drawerOpen = !modal
|
||||
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||
@@ -207,7 +172,7 @@ Kirigami.ApplicationWindow {
|
||||
icon.name: "settings-configure"
|
||||
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
|
||||
shortcut: StandardKey.Preferences
|
||||
shortcut: Controller.preferencesShortcuts[0]
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("About Neochat")
|
||||
@@ -237,60 +202,45 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
pageStack.initialPage: LoadingPage {}
|
||||
|
||||
Component {
|
||||
id: roomListComponent
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
roomListModel: spectralRoomListModel
|
||||
activeConnection: Controller.activeConnection
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onInitialSyncFinished() {
|
||||
roomManager.roomList = pageStack.replace(roomListComponent);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
|
||||
function onInitiated() {
|
||||
if (roomManager.hasOpenRoom) {
|
||||
return;
|
||||
}
|
||||
onInitiated: {
|
||||
if (Controller.accountCount === 0) {
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {});
|
||||
} else {
|
||||
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
|
||||
roomManager.loadInitialRoom();
|
||||
}
|
||||
}
|
||||
|
||||
function onBusyChanged() {
|
||||
if(!Controller.busy && roomManager.roomList === null) {
|
||||
onConnectionAdded: {
|
||||
if (Controller.accountCount === 1) {
|
||||
roomManager.roomList = pageStack.replace(roomListComponent);
|
||||
}
|
||||
}
|
||||
|
||||
function onRoomJoined(roomId) {
|
||||
const room = Controller.activeConnection.room(roomId);
|
||||
return roomManager.enterRoom(room);
|
||||
}
|
||||
|
||||
function onConnectionDropped() {
|
||||
onConnectionDropped: {
|
||||
if (Controller.accountCount === 0) {
|
||||
pageStack.clear();
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
|
||||
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml");
|
||||
}
|
||||
}
|
||||
|
||||
function onGlobalErrorOccured(error, detail) {
|
||||
showPassiveNotification(error + ": " + detail)
|
||||
}
|
||||
onGlobalErrorOccured: showPassiveNotification(error + ": " + detail)
|
||||
|
||||
function onShowWindow() {
|
||||
root.showWindow()
|
||||
}
|
||||
onShowWindow: root.showWindow()
|
||||
|
||||
function onOpenRoom(room) {
|
||||
roomManager.enterRoom(room)
|
||||
@@ -302,13 +252,6 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
onDirectChatAvailable: {
|
||||
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: consentSheet
|
||||
|
||||
@@ -331,47 +274,21 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
RoomListModel {
|
||||
id: spectralRoomListModel
|
||||
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomPage
|
||||
|
||||
RoomPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: createRoomDialog
|
||||
|
||||
CreateRoomDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomWindow
|
||||
RoomWindow {}
|
||||
}
|
||||
|
||||
function handleLink(link, currentRoom) {
|
||||
if (link.startsWith("https://matrix.to/")) {
|
||||
var content = link.replace("https://matrix.to/#/", "").replace(/\?.*/, "")
|
||||
if(content.match("^[#!]")) {
|
||||
if(content.includes("/")) {
|
||||
var result = content.match("([!#].*:.*)/(\\$.*)")
|
||||
if(!result) {
|
||||
return
|
||||
}
|
||||
if(result[1] == currentRoom.id) {
|
||||
roomManager.roomItem.goToEvent(result[2])
|
||||
} else {
|
||||
roomManager.openRoomAndEvent(roomManager.roomByAliasOrId(result[1]), result[2])
|
||||
}
|
||||
} else {
|
||||
roomManager.enterRoom(roomManager.roomByAliasOrId(content))
|
||||
}
|
||||
} else if(content.match("^@")) {
|
||||
let dialog = userDialog.createObject(root.overlay, {room: currentRoom, user: currentRoom.user(content)})
|
||||
dialog.open()
|
||||
console.log(dialog.user)
|
||||
}
|
||||
} else {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDialog
|
||||
UserDetailDialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[Material]
|
||||
Primary=Blue
|
||||
Accent=Blue
|
||||
Theme=System
|
||||
15
res.qrc
15
res.qrc
@@ -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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -126,7 +126,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
|
||||
|
||||
if (cursor.block().text() == m_lastState) {
|
||||
// ignore change, it was caused by autocompletion
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"type", AutoCompletionType::Ignore},
|
||||
};
|
||||
}
|
||||
@@ -141,7 +141,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
|
||||
QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
|
||||
|
||||
if (autoCompletePrefix.isEmpty()) {
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"type", AutoCompletionType::None},
|
||||
};
|
||||
}
|
||||
@@ -151,22 +151,77 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
|
||||
|
||||
if (autoCompletePrefix.startsWith("@")) {
|
||||
autoCompletePrefix.remove(0, 1);
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"keyword", autoCompletePrefix},
|
||||
{"type", AutoCompletionType::User},
|
||||
};
|
||||
}
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"keyword", autoCompletePrefix},
|
||||
{"type", AutoCompletionType::Emoji},
|
||||
};
|
||||
}
|
||||
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"type", AutoCompletionType::None},
|
||||
};
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath,
|
||||
const QString &replyEventId, const QVariantMap usernames) const
|
||||
{
|
||||
if (!m_room || !m_document) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString cleanedText = text;
|
||||
|
||||
cleanedText = cleanedText.trimmed();
|
||||
|
||||
for (const auto username : usernames.keys()) {
|
||||
const auto replacement = usernames.value(username);
|
||||
cleanedText = cleanedText.replace(username,
|
||||
"[" + username + "](https://matrix.to/#/" + replacement.toString() + ")");
|
||||
}
|
||||
|
||||
|
||||
if (attachementPath.length() > 0) {
|
||||
m_room->uploadFile(attachementPath, cleanedText);
|
||||
}
|
||||
|
||||
if (cleanedText.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto messageEventType = RoomMessageEvent::MsgType::Text;
|
||||
|
||||
const QString rainbowPrefix = QStringLiteral("/rainbow ");
|
||||
const QString mePrefix = QStringLiteral("/me ");
|
||||
const QString noticePrefix = QStringLiteral("/notice ");
|
||||
|
||||
if (cleanedText.indexOf(rainbowPrefix) == 0) {
|
||||
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
|
||||
QString rainbowText;
|
||||
QStringList rainbowColors {"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff",
|
||||
"#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
|
||||
|
||||
for (int i = 0; i < cleanedText.length(); i++) {
|
||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
||||
}
|
||||
m_room->postHtmlMessage(text, rainbowText, messageEventType, replyEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedText.indexOf(mePrefix) == 0) {
|
||||
cleanedText = cleanedText.remove(0, mePrefix.length());
|
||||
messageEventType = RoomMessageEvent::MsgType::Emote;
|
||||
} else if (cleanedText.indexOf(noticePrefix) == 0) {
|
||||
cleanedText = cleanedText.remove(0, noticePrefix.length());
|
||||
messageEventType = RoomMessageEvent::MsgType::Notice;
|
||||
}
|
||||
m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId);
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::replaceAutoComplete(const QString &word)
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
class QTextDocument;
|
||||
class QQuickTextDocument;
|
||||
class NeoChatRoom;
|
||||
class Controller;
|
||||
|
||||
class ChatDocumentHandler : public QObject
|
||||
{
|
||||
@@ -52,8 +51,10 @@ public:
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QVariantMap usernames) const;
|
||||
|
||||
/// This function will look at the current QTextCursor and determine if there
|
||||
/// is the possibility to autocomplete it.
|
||||
/// is the posibility to autocomplete it.
|
||||
Q_INVOKABLE QVariantMap getAutocompletionInfo();
|
||||
Q_INVOKABLE void replaceAutoComplete(const QString &word);
|
||||
|
||||
@@ -63,7 +64,6 @@ Q_SIGNALS:
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void roomChanged();
|
||||
void joinRoom(QString roomName);
|
||||
|
||||
private:
|
||||
[[nodiscard]] QTextCursor textCursor() const;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
@@ -8,42 +8,43 @@
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <qt5keychain/keychain.h>
|
||||
#endif
|
||||
#else
|
||||
#include <KConfig>
|
||||
#include <KConfigGroup>
|
||||
#include <KWindowConfig>
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QQuickWindow>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringBuilder>
|
||||
#include <QSysInfo>
|
||||
#include <QTimer>
|
||||
#include <QCloseEvent>
|
||||
#include <QDesktopServices>
|
||||
#include <QMovie>
|
||||
#include <QPixmap>
|
||||
#include <QAuthenticator>
|
||||
#include <QNetworkReply>
|
||||
#include <QStringBuilder>
|
||||
#include <QtGui/QCloseEvent>
|
||||
#include <QtGui/QDesktopServices>
|
||||
#include <QtGui/QMovie>
|
||||
#include <QtGui/QPixmap>
|
||||
#include <QtNetwork/QAuthenticator>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <utility>
|
||||
|
||||
#include "csapi/account-data.h"
|
||||
#include "csapi/content-repo.h"
|
||||
#include "csapi/joining.h"
|
||||
#include "csapi/logout.h"
|
||||
#include "csapi/profile.h"
|
||||
#include "csapi/registration.h"
|
||||
#include "csapi/wellknown.h"
|
||||
#include "events/eventcontent.h"
|
||||
#include "events/roommessageevent.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "settings.h"
|
||||
#include "utils.h"
|
||||
#include <KStandardShortcut>
|
||||
@@ -55,26 +56,16 @@
|
||||
Controller::Controller(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QApplication::setQuitOnLastWindowClosed(false);
|
||||
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
Connection::setUserType<NeoChatUser>();
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
TrayIcon *trayIcon = new TrayIcon(this);
|
||||
if(NeoChatConfig::self()->systemTray()) {
|
||||
trayIcon->show();
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
QApplication::setQuitOnLastWindowClosed(false);
|
||||
}
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [=](){
|
||||
if(NeoChatConfig::self()->systemTray()) {
|
||||
trayIcon->show();
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
} else {
|
||||
trayIcon->hide();
|
||||
disconnect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
}
|
||||
QApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
|
||||
});
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
trayIcon->setIconSource("org.kde.neochat");
|
||||
trayIcon->setIsOnline(true);
|
||||
#endif
|
||||
|
||||
QTimer::singleShot(0, this, [=] {
|
||||
@@ -102,6 +93,51 @@ inline QString accessTokenFileName(const AccountSettings &account)
|
||||
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName;
|
||||
}
|
||||
|
||||
void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName)
|
||||
{
|
||||
if (user.isEmpty() || pass.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (deviceName.isEmpty()) {
|
||||
deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture();
|
||||
}
|
||||
|
||||
auto conn = new Connection(this);
|
||||
const QUrl serverUrl = QUrl::fromUserInput(serverAddr);
|
||||
// we are using a fake mixd since resolveServer just set the homeserver url :sigh:
|
||||
conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443)));
|
||||
|
||||
connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() {
|
||||
conn->loginWithPassword(user, pass, deviceName, "");
|
||||
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
|
||||
AccountSettings account(conn->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(conn->homeserver());
|
||||
account.setDeviceId(conn->deviceId());
|
||||
account.setDeviceName(deviceName);
|
||||
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
addConnection(conn);
|
||||
setActiveConnection(conn);
|
||||
});
|
||||
connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
connect(conn, &Connection::loginError, [=](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed"), std::move(error));
|
||||
});
|
||||
});
|
||||
|
||||
connect(conn, &Connection::resolveError, this, [=](QString error) {
|
||||
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
|
||||
{
|
||||
if (user.isEmpty() || token.isEmpty()) {
|
||||
@@ -224,7 +260,7 @@ void Controller::invokeLogin()
|
||||
const auto accounts = SettingsGroup("Accounts").childGroups();
|
||||
QString id = NeoChatConfig::self()->activeConnection();
|
||||
for (const auto &accountId : accounts) {
|
||||
AccountSettings account{accountId};
|
||||
AccountSettings account {accountId};
|
||||
if (id.isEmpty()) {
|
||||
// handle case where the account config is empty
|
||||
id = accountId;
|
||||
@@ -249,7 +285,6 @@ void Controller::invokeLogin()
|
||||
Q_EMIT errorOccured(i18n("Login Failed"), error);
|
||||
logout(connection, true);
|
||||
}
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured("Network Error", error);
|
||||
@@ -257,14 +292,14 @@ void Controller::invokeLogin()
|
||||
connection->connectWithToken(account.userId(), accessToken, account.deviceId());
|
||||
}
|
||||
}
|
||||
if (accounts.isEmpty()) {
|
||||
if (m_connections.isEmpty()) {
|
||||
Q_EMIT initiated();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
|
||||
{
|
||||
QFile accountTokenFile{accessTokenFileName(account)};
|
||||
QFile accountTokenFile {accessTokenFileName(account)};
|
||||
if (accountTokenFile.open(QFile::ReadOnly)) {
|
||||
if (accountTokenFile.size() < 1024) {
|
||||
return accountTokenFile.readAll();
|
||||
@@ -302,7 +337,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
|
||||
bool removed = false;
|
||||
bool saved = saveAccessTokenToKeyChain(account, accessToken);
|
||||
if (saved) {
|
||||
QFile accountTokenFile{accessTokenFileName(account)};
|
||||
QFile accountTokenFile {accessTokenFileName(account)};
|
||||
removed = accountTokenFile.remove();
|
||||
}
|
||||
if (!(saved && removed)) {
|
||||
@@ -325,7 +360,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
|
||||
bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken)
|
||||
{
|
||||
// (Re-)Make a dedicated file for access_token.
|
||||
QFile accountTokenFile{accessTokenFileName(account)};
|
||||
QFile accountTokenFile {accessTokenFileName(account)};
|
||||
accountTokenFile.remove(); // Just in case
|
||||
|
||||
auto fileDir = QFileInfo(accountTokenFile).dir();
|
||||
@@ -364,6 +399,34 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::joinRoom(Connection *c, const QString &alias)
|
||||
{
|
||||
if (!alias.contains(":")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto knownServer = alias.mid(alias.indexOf(":") + 1);
|
||||
auto joinRoomJob = c->joinRoom(alias, QStringList {knownServer});
|
||||
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::failure, [=] {
|
||||
Q_EMIT errorOccured("Join Room Failed", joinRoomJob->errorString());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::createRoom(Connection *c, const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
|
||||
Q_EMIT errorOccured("Create Room Failed", createRoomJob->errorString());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::createDirectChat(Connection *c, const QString &userID)
|
||||
{
|
||||
auto createRoomJob = c->createDirectChat(userID);
|
||||
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
|
||||
Q_EMIT errorOccured("Create Direct Chat Failed", createRoomJob->errorString());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::playAudio(const QUrl &localFile)
|
||||
{
|
||||
@@ -502,21 +565,7 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
Q_EMIT activeConnectionChanged();
|
||||
}
|
||||
|
||||
void Controller::saveWindowGeometry(QQuickWindow *window)
|
||||
QList<QKeySequence> Controller::preferencesShortcuts() const
|
||||
{
|
||||
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
|
||||
KConfigGroup windowGroup(&dataResource, "Window");
|
||||
KWindowConfig::saveWindowPosition(window, windowGroup);
|
||||
KWindowConfig::saveWindowSize(window, windowGroup);
|
||||
dataResource.sync();
|
||||
}
|
||||
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
|
||||
return KStandardShortcut::preferences();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef CONTROLLER_H
|
||||
#define CONTROLLER_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMediaPlayer>
|
||||
@@ -21,7 +21,6 @@ class QKeySequences;
|
||||
#include "user.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
class QQuickWindow;
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -34,6 +33,9 @@ class Controller : public QObject
|
||||
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
|
||||
Q_PROPERTY(KAboutData aboutData READ aboutData WRITE setAboutData NOTIFY aboutDataChanged)
|
||||
|
||||
/// Get the list of shortcuts activating the preferences page
|
||||
Q_PROPERTY(QList<QKeySequence> preferencesShortcuts READ preferencesShortcuts CONSTANT)
|
||||
|
||||
public:
|
||||
static Controller &instance();
|
||||
|
||||
@@ -45,12 +47,15 @@ public:
|
||||
void addConnection(Connection *c);
|
||||
void dropConnection(Connection *c);
|
||||
|
||||
Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString);
|
||||
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
|
||||
|
||||
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString ¤tPassword, const QString &newPassword);
|
||||
|
||||
[[nodiscard]] int accountCount() const;
|
||||
|
||||
[[nodiscard]] QList<QKeySequence> preferencesShortcuts() const;
|
||||
|
||||
[[nodiscard]] static bool quitOnLastWindowClosed();
|
||||
void setQuitOnLastWindowClosed(bool value);
|
||||
|
||||
@@ -60,9 +65,6 @@ public:
|
||||
void setAboutData(const KAboutData &aboutData);
|
||||
[[nodiscard]] KAboutData aboutData() const;
|
||||
|
||||
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
|
||||
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
enum PasswordStatus {
|
||||
Success,
|
||||
Wrong,
|
||||
@@ -81,6 +83,8 @@ private:
|
||||
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
|
||||
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
|
||||
|
||||
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
|
||||
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
|
||||
@@ -91,10 +95,10 @@ private Q_SLOTS:
|
||||
|
||||
Q_SIGNALS:
|
||||
void busyChanged();
|
||||
/// Error occurred because of user inputs
|
||||
/// Error occured because of user inputs
|
||||
void errorOccured(QString error, QString detail);
|
||||
|
||||
/// Error occurred because of server or bug in NeoChat
|
||||
/// Error occured because of server or bug in NeoChat
|
||||
void globalErrorOccured(QString error, QString detail);
|
||||
void syncDone();
|
||||
void connectionAdded(Quotient::Connection *_t1);
|
||||
@@ -110,14 +114,15 @@ Q_SIGNALS:
|
||||
void showWindow();
|
||||
void openRoom(NeoChatRoom *room);
|
||||
void userConsentRequired(QUrl url);
|
||||
void testConnectionResult(const QString &connection, bool usable);
|
||||
|
||||
public Q_SLOTS:
|
||||
void logout(Quotient::Connection *conn, bool serverSideLogout);
|
||||
void joinRoom(Quotient::Connection *c, const QString &alias);
|
||||
void createRoom(Quotient::Connection *c, const QString &name, const QString &topic);
|
||||
void createDirectChat(Quotient::Connection *c, const QString &userID);
|
||||
static void playAudio(const QUrl &localFile);
|
||||
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
|
||||
static void markAllMessagesAsRead(Quotient::Connection *conn);
|
||||
void saveWindowGeometry(QQuickWindow *);
|
||||
};
|
||||
|
||||
// TODO libQuotient 0.7: Drop
|
||||
@@ -127,8 +132,4 @@ public:
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth = none);
|
||||
};
|
||||
|
||||
class NeochatDeleteDeviceJob : public BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth = none);
|
||||
};
|
||||
#endif // CONTROLLER_H
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
2586
src/emojimodel.cpp
2586
src/emojimodel.cpp
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
206
src/login.cpp
206
src/login.cpp
@@ -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;
|
||||
}
|
||||
85
src/login.h
85
src/login.h
@@ -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;
|
||||
};
|
||||
39
src/main.cpp
39
src/main.cpp
@@ -14,12 +14,11 @@
|
||||
#include <QQuickWindow>
|
||||
|
||||
#include <KAboutData>
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KDBusService>
|
||||
#endif
|
||||
#include <KLocalizedContext>
|
||||
#include <KLocalizedString>
|
||||
#include <KWindowConfig>
|
||||
|
||||
#include "neochat-version.h"
|
||||
|
||||
@@ -29,13 +28,9 @@
|
||||
#include "controller.h"
|
||||
#include "csapi/joining.h"
|
||||
#include "csapi/leaving.h"
|
||||
#include "devicesmodel.h"
|
||||
#include "emojimodel.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagefiltermodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
@@ -46,7 +41,6 @@
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
#include "userdirectorylistmodel.h"
|
||||
#include "userlistmodel.h"
|
||||
#include "actionshandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -70,10 +64,6 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QApplication::setStyle(QStringLiteral("breeze"));
|
||||
#endif
|
||||
|
||||
QApplication::setOrganizationName("KDE");
|
||||
|
||||
KAboutData about(QStringLiteral("neochat"), i18n("Neochat"), QStringLiteral(NEOCHAT_VERSION_STRING), i18n("Matrix client"), KAboutLicense::GPL_V3, i18n("© 2018-2020 Black Hat, 2020 KDE Community"));
|
||||
@@ -85,39 +75,32 @@ int main(int argc, char *argv[])
|
||||
KAboutData::setApplicationData(about);
|
||||
QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
#ifndef Q_OS_ANDROID
|
||||
KDBusService service(KDBusService::Unique);
|
||||
#endif
|
||||
|
||||
#ifdef NEOCHAT_FLATPAK
|
||||
// Copy over the included FontConfig configuration to the
|
||||
// app's config dir:
|
||||
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
|
||||
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf",
|
||||
"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
|
||||
#endif
|
||||
|
||||
Clipboard clipboard;
|
||||
auto config = NeoChatConfig::self();
|
||||
FileTypeSingleton fileTypeSingleton;
|
||||
|
||||
Login *login = new Login();
|
||||
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
||||
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
|
||||
qmlRegisterType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel");
|
||||
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
|
||||
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||
qmlRegisterUncreatableType<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM");
|
||||
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
|
||||
@@ -131,7 +114,6 @@ int main(int argc, char *argv[])
|
||||
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
|
||||
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
|
||||
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
|
||||
qRegisterMetaType<QMimeType>("QMimeType");
|
||||
|
||||
qRegisterMetaTypeStreamOperators<Emoji>();
|
||||
|
||||
@@ -157,7 +139,7 @@ int main(int argc, char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
#ifndef Q_OS_ANDROID
|
||||
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
|
||||
const auto rootObjects = engine.rootObjects();
|
||||
for (auto obj : rootObjects) {
|
||||
@@ -169,17 +151,6 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
});
|
||||
const auto rootObjects = engine.rootObjects();
|
||||
for (auto obj : rootObjects) {
|
||||
auto view = qobject_cast<QQuickWindow*>(obj);
|
||||
if (view) {
|
||||
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
|
||||
KConfigGroup windowGroup(&dataResource, "Window");
|
||||
KWindowConfig::restoreWindowSize(view, windowGroup);
|
||||
KWindowConfig::restoreWindowPosition(view, windowGroup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return QApplication::exec();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <events/redactionevent.h>
|
||||
#include <events/roomavatarevent.h>
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/stickerevent.h>
|
||||
#include <events/simplestateevents.h>
|
||||
#include <user.h>
|
||||
|
||||
@@ -46,7 +45,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[IsEditedRole] = "isEdited";
|
||||
roles[FormattedBodyRole] = "formattedBody";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -64,8 +62,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
return;
|
||||
}
|
||||
m_currentRoom->getPreviousContent(50);
|
||||
connect(this, &QAbstractListModel::rowsInserted, this, [=]() {
|
||||
if (m_currentRoom->readMarkerEventId().isEmpty()) {
|
||||
connect(this, &QAbstractListModel::rowsInserted, this, [=](){
|
||||
if(m_currentRoom->readMarkerEventId().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
|
||||
@@ -301,7 +299,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
|
||||
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
|
||||
{
|
||||
Q_UNUSED(evt)
|
||||
return QVariantMap{
|
||||
return QVariantMap {
|
||||
{"isLocalUser", user->id() == room->localUser()->id()},
|
||||
{"id", user->id()},
|
||||
{"avatarMediaId", user->avatarMediaId(room)},
|
||||
@@ -331,20 +329,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
auto reason = evt.redactedBecause()->reason();
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>").arg(evt.redactedBecause()->reason());
|
||||
}
|
||||
|
||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
||||
}
|
||||
|
||||
if (role == FormattedBodyRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
||||
return static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == MessageRole) {
|
||||
return m_currentRoom->eventToString(evt);
|
||||
}
|
||||
@@ -375,9 +362,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
return "message";
|
||||
}
|
||||
if (is<const StickerEvent>(evt)) {
|
||||
return "sticker";
|
||||
}
|
||||
if (evt.isStateEvent()) {
|
||||
return "state";
|
||||
}
|
||||
@@ -414,10 +398,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
// content JSON stored in EventContent::Base
|
||||
return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
|
||||
};
|
||||
|
||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
return QVariant::fromValue(e->image().originalJson);
|
||||
}
|
||||
}
|
||||
|
||||
if (role == HighlightRole) {
|
||||
@@ -486,9 +466,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
|
||||
}
|
||||
}
|
||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
|
||||
}
|
||||
}
|
||||
|
||||
if (role == AnnotationRole) {
|
||||
@@ -525,7 +502,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
};
|
||||
const auto &replyEvt = **replyIt;
|
||||
|
||||
return QVariantMap{{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
|
||||
return QVariantMap {{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
@@ -579,7 +556,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
authors.append(userAtEvent(author, m_currentRoom, evt));
|
||||
}
|
||||
bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser()));
|
||||
res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
|
||||
res.append(QVariantMap {{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
|
||||
++i;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef MESSAGEEVENTMODEL_H
|
||||
#define MESSAGEEVENTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
@@ -32,7 +32,6 @@ public:
|
||||
LongOperationRole,
|
||||
AnnotationRole,
|
||||
UserMarkerRole,
|
||||
FormattedBodyRole,
|
||||
|
||||
ReplyRole,
|
||||
|
||||
@@ -40,7 +39,6 @@ public:
|
||||
ShowSectionRole,
|
||||
|
||||
ReactionRole,
|
||||
|
||||
IsEditedRole,
|
||||
|
||||
// For debugging
|
||||
@@ -91,3 +89,5 @@ private:
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
};
|
||||
|
||||
#endif // MESSAGEEVENTMODEL_H
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -27,13 +27,12 @@
|
||||
#include "events/roomcanonicalaliasevent.h"
|
||||
#include "events/roommessageevent.h"
|
||||
#include "events/roompowerlevelsevent.h"
|
||||
#include "events/stickerevent.h"
|
||||
#include "events/typingevent.h"
|
||||
#include "jobs/downloadfilejob.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "user.h"
|
||||
#include "utils.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
@@ -51,7 +50,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
return;
|
||||
}
|
||||
const RoomEvent *lastEvent = messageEvents().rbegin()->get();
|
||||
if (lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
|
||||
if(lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
|
||||
return;
|
||||
}
|
||||
if (lastEvent->isStateEvent()) {
|
||||
@@ -62,25 +61,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
return;
|
||||
}
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender->avatarUrl(this).isEmpty()) {
|
||||
avatar_image = sender->avatar(128, this);
|
||||
} else {
|
||||
avatar_image = this->avatar(128);
|
||||
}
|
||||
|
||||
NotificationsManager::instance().postNotification(this, displayName(), sender->displayname(this), eventToString(*lastEvent), avatar_image);
|
||||
NotificationsManager::instance().postNotification(this, lastEvent->id(), displayName(), sender->displayname(this), eventToString(*lastEvent), avatar(128));
|
||||
});
|
||||
|
||||
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
|
||||
connect(this, &Room::aboutToAddHistoricalMessages,
|
||||
this, &NeoChatRoom::readMarkerLoadedChanged);
|
||||
|
||||
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
|
||||
|
||||
connect(this, &Room::joinStateChanged, this, [=](JoinState oldState, JoinState newState) {
|
||||
if(oldState == JoinState::Invite && newState != JoinState::Invite) {
|
||||
Q_EMIT isInviteChanged();
|
||||
}
|
||||
});
|
||||
connect(this, &Quotient::Room::eventsHistoryJobChanged,
|
||||
this, &NeoChatRoom::lastActiveTimeChanged);
|
||||
}
|
||||
|
||||
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||
@@ -105,6 +93,7 @@ void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||
});
|
||||
connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) {
|
||||
if (id == txnId) {
|
||||
qDebug() << "Progress:" << progress << total;
|
||||
setFileUploadingProgress(int(float(progress) / float(total) * 100));
|
||||
}
|
||||
});
|
||||
@@ -126,12 +115,7 @@ QVariantList NeoChatRoom::getUsersTyping() const
|
||||
users.removeAll(localUser());
|
||||
QVariantList userVariants;
|
||||
for (User *user : users) {
|
||||
userVariants.append(QVariantMap {
|
||||
{"id", user->id()},
|
||||
{"avatarMediaId", user->avatarMediaId(this)},
|
||||
{"displayName", user->displayname(this)},
|
||||
{"display", user->name()},
|
||||
});
|
||||
userVariants.append(QVariant::fromValue(user));
|
||||
}
|
||||
return userVariants;
|
||||
}
|
||||
@@ -182,6 +166,7 @@ QString NeoChatRoom::lastEventToString() const
|
||||
return QLatin1String("");
|
||||
}
|
||||
|
||||
|
||||
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
|
||||
{
|
||||
return highlights.contains(e);
|
||||
@@ -276,15 +261,7 @@ QVariantList NeoChatRoom::getUsers(const QString &keyword) const
|
||||
QVariantList matchedList;
|
||||
for (const auto u : userList) {
|
||||
if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) {
|
||||
NeoChatUser user(u->id(), u->connection());
|
||||
QVariantMap userVariant {
|
||||
{ QStringLiteral("id"), user.id() },
|
||||
{ QStringLiteral("displayName"), user.displayname(this) },
|
||||
{ QStringLiteral("avatarMediaId"), user.avatarMediaId(this) },
|
||||
{ QStringLiteral("color"), user.color() }
|
||||
};
|
||||
|
||||
matchedList.append(QVariant::fromValue(userVariant));
|
||||
matchedList.append(QVariant::fromValue(u));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,9 +341,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
}
|
||||
return plainBody;
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->user(e.userId())->displayname();
|
||||
@@ -374,38 +348,25 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
switch (e.membership()) {
|
||||
case MembershipType::Invite:
|
||||
if (e.repeatsState()) {
|
||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
||||
}
|
||||
return text;
|
||||
return i18n("reinvited %1 to the room", subjectName);
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
break;
|
||||
case MembershipType::Join: {
|
||||
QString text {};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == MembershipType::Invite
|
||||
? i18n("invited %1 to the room", subjectName)
|
||||
: i18n("joined the room");
|
||||
return i18n("joined the room (repeated)");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18n(": %1", e.reason().toHtmlEscaped());
|
||||
}
|
||||
return text;
|
||||
if (!e.prevContent() || e.membership() != e.prevContent()->membership) {
|
||||
return e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename() && NeoChatConfig::self()->showRename()) {
|
||||
if (!e.displayName().isEmpty()) {
|
||||
QString text {};
|
||||
if (e.isRename()) {
|
||||
if (e.displayName().isEmpty()) {
|
||||
text = i18n("cleared their display name");
|
||||
} else {
|
||||
text = i18n("changed their display name to %1", e.displayName().toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate() && NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
@@ -466,7 +427,7 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||
if (isJobRunning(job)) {
|
||||
connect(job, &BaseJob::success, this, [this, job] {
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject{{"url", job->contentUri()}});
|
||||
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject {{"url", job->contentUri()}});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -514,6 +475,18 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
|
||||
return result;
|
||||
}
|
||||
|
||||
void NeoChatRoom::postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId)
|
||||
{
|
||||
const auto parsedHTML = markdownToHTML(text);
|
||||
const bool isRichText = Qt::mightBeRichText(parsedHTML);
|
||||
|
||||
if (isRichText) { // Markdown
|
||||
postHtmlMessage(text, parsedHTML, type, replyEventId);
|
||||
} else { // Plain text
|
||||
postPlainMessage(text, type, replyEventId);
|
||||
}
|
||||
}
|
||||
|
||||
QString msgTypeToString(MessageEventType msgType)
|
||||
{
|
||||
switch (msgType) {
|
||||
@@ -538,52 +511,14 @@ QString msgTypeToString(MessageEventType msgType)
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
void NeoChatRoom::postPlainMessage(const QString &text, MessageEventType type, const QString &replyEventId)
|
||||
{
|
||||
const auto html = markdownToHTML(text);
|
||||
QString cleanText(text);
|
||||
cleanText.replace(QRegularExpression("\\[(.+)\\]\\(.+\\)"), "\\1");
|
||||
postHtmlMessage(rawText, html, type, replyEventId, relateToEventId);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
{
|
||||
bool isRichText = Qt::mightBeRichText(html);
|
||||
bool isReply = !replyEventId.isEmpty();
|
||||
bool isEdit = !relateToEventId.isEmpty();
|
||||
const auto replyIt = findInTimeline(replyEventId);
|
||||
if (replyIt == timelineEdge()) {
|
||||
isReply = false;
|
||||
}
|
||||
|
||||
|
||||
if (isEdit) {
|
||||
QJsonObject json {
|
||||
{"type", "m.room.message"},
|
||||
{"msgtype", msgTypeToString(type)},
|
||||
{"body", "* " + text},
|
||||
{"format", "org.matrix.custom.html"},
|
||||
{"formatted_body", html},
|
||||
{"m.new_content",
|
||||
QJsonObject {
|
||||
{"body", text},
|
||||
{"msgtype", msgTypeToString(type)},
|
||||
{"format", "org.matrix.custom.html"},
|
||||
{"formatted_body", html}
|
||||
}
|
||||
},
|
||||
{"m.relates_to",
|
||||
QJsonObject {
|
||||
{"rel_type", "m.replace"},
|
||||
{"event_id", relateToEventId}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
postJson("m.room.message", json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReply) {
|
||||
const auto &replyEvt = **replyIt;
|
||||
|
||||
@@ -608,7 +543,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
|
||||
replyEvt.senderId() + "\">" + replyEvt.senderId() +
|
||||
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
|
||||
"</blockquote></mx-reply>" + (isRichText ? html : text)
|
||||
"</blockquote></mx-reply>" + text
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
@@ -618,11 +553,52 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRichText) {
|
||||
Room::postHtmlMessage(text, html, type);
|
||||
} else {
|
||||
Room::postMessage(text, type);
|
||||
Room::postMessage(text, type);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId)
|
||||
{
|
||||
bool isReply = !replyEventId.isEmpty();
|
||||
const auto replyIt = findInTimeline(replyEventId);
|
||||
if (replyIt == timelineEdge()) {
|
||||
isReply = false;
|
||||
}
|
||||
|
||||
if (isReply) {
|
||||
const auto &replyEvt = **replyIt;
|
||||
|
||||
// clang-format off
|
||||
QJsonObject json{
|
||||
{"msgtype", msgTypeToString(type)},
|
||||
{"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text},
|
||||
{"format", "org.matrix.custom.html"},
|
||||
{"m.relates_to",
|
||||
QJsonObject {
|
||||
{"m.in_reply_to",
|
||||
QJsonObject {
|
||||
{"event_id", replyEventId}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{"formatted_body",
|
||||
"<mx-reply><blockquote><a href=\"https://matrix.to/#/" +
|
||||
id() + "/" +
|
||||
replyEventId +
|
||||
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
|
||||
replyEvt.senderId() + "\">" + replyEvt.senderId() +
|
||||
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
|
||||
"</blockquote></mx-reply>" + html
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
postJson("m.room.message", json);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Room::postHtmlMessage(text, html, type);
|
||||
}
|
||||
|
||||
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
|
||||
@@ -699,8 +675,3 @@ bool NeoChatRoom::readMarkerLoaded() const
|
||||
const auto it = findInTimeline(readMarkerEventId());
|
||||
return it != timelineEdge();
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isInvite() const
|
||||
{
|
||||
return joinState() == JoinState::Invite;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,26 +14,11 @@
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QDebug>
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusInterface>
|
||||
#endif
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <utility>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool useUnityCounter() {
|
||||
static const auto Result = QDBusInterface(
|
||||
"com.canonical.Unity",
|
||||
"/").isValid();
|
||||
|
||||
return Result;
|
||||
}
|
||||
#endif
|
||||
|
||||
RoomListModel::RoomListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
@@ -41,37 +26,6 @@ RoomListModel::RoomListModel(QObject *parent)
|
||||
for (auto collapsedSection : collapsedSections) {
|
||||
m_categoryVisibility[collapsedSection] = false;
|
||||
}
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
|
||||
if (useUnityCounter()) {
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop";
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(m_notificationCount, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
|
||||
if (counterSlice > 0) {
|
||||
dbusUnityProperties["count"] = counterSlice;
|
||||
dbusUnityProperties["count-visible"] = true;
|
||||
} else {
|
||||
dbusUnityProperties["count-visible"] = false;
|
||||
}
|
||||
|
||||
auto signal = QDBusMessage::createSignal(
|
||||
"/com/canonical/unity/launcherentry/neochat",
|
||||
"com.canonical.Unity.LauncherEntry",
|
||||
"Update");
|
||||
|
||||
signal.setArguments({
|
||||
launcherUrl,
|
||||
dbusUnityProperties
|
||||
});
|
||||
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
RoomListModel::~RoomListModel() = default;
|
||||
@@ -228,9 +182,6 @@ void RoomListModel::refreshNotificationCount()
|
||||
for (auto room : qAsConst(m_rooms)) {
|
||||
count += room->notificationCount();
|
||||
}
|
||||
if (m_notificationCount == count) {
|
||||
return;
|
||||
}
|
||||
m_notificationCount = count;
|
||||
Q_EMIT notificationCountChanged();
|
||||
}
|
||||
@@ -429,13 +380,3 @@ bool RoomListModel::categoryVisible(int category) const
|
||||
{
|
||||
return m_categoryVisibility.value(category, true);
|
||||
}
|
||||
|
||||
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
|
||||
{
|
||||
for(const auto &room : m_rooms) {
|
||||
if(room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef ROOMLISTMODEL_H
|
||||
#define ROOMLISTMODEL_H
|
||||
|
||||
#include "connection.h"
|
||||
#include "events/roomevent.h"
|
||||
@@ -80,8 +80,6 @@ public:
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
|
||||
|
||||
private Q_SLOTS:
|
||||
void doAddRoom(Quotient::Room *room);
|
||||
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
|
||||
@@ -107,3 +105,5 @@ Q_SIGNALS:
|
||||
void newMessage(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
|
||||
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
|
||||
};
|
||||
|
||||
#endif // ROOMLISTMODEL_H
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
16
src/utils.h
16
src/utils.h
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user