Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b848961bc | ||
|
|
51574f5125 | ||
|
|
a779907500 | ||
|
|
f195db323d | ||
|
|
da47d76a7f | ||
|
|
6dc8c4976c | ||
|
|
fec7680068 | ||
|
|
b84264891d | ||
|
|
465a981033 | ||
|
|
06b4c40b33 | ||
|
|
efae510fda | ||
|
|
f9fc8c5c0b | ||
|
|
49c9c63bf5 | ||
|
|
90cee0f437 | ||
|
|
5bc9362fde | ||
|
|
10922aeb52 | ||
|
|
f9a96ccdab | ||
|
|
04056d9ed1 | ||
|
|
ecf373e317 | ||
|
|
9c2e0669f6 | ||
|
|
2b8aa9f975 | ||
|
|
2f61090413 | ||
|
|
aa9daad704 | ||
|
|
0e79d3506d | ||
|
|
72994d0349 | ||
|
|
0ee5ba76c9 | ||
|
|
d1398f6726 | ||
|
|
9084817450 | ||
|
|
083a2f9772 | ||
|
|
b44e81c849 | ||
|
|
014826bd09 | ||
|
|
2858fcfad2 | ||
|
|
b6db36a9f2 | ||
|
|
ede860c99f | ||
|
|
f8951fc760 | ||
|
|
525015fe78 | ||
|
|
b834510be0 | ||
|
|
8700611235 | ||
|
|
15ddcef115 | ||
|
|
7216da8b6f | ||
|
|
7bd4aac692 | ||
|
|
10e17d9f0f | ||
|
|
db5e328869 | ||
|
|
921667565e | ||
|
|
9cd8a380ed | ||
|
|
29816730e4 | ||
|
|
7214936eaa | ||
|
|
5a7c3295dc | ||
|
|
dce4a409c7 | ||
|
|
bd27904f17 | ||
|
|
731b234dda | ||
|
|
ce0fc637c4 | ||
|
|
070fe45a2d | ||
|
|
3a8d078e6c | ||
|
|
fb9183e5c3 | ||
|
|
853113df3f | ||
|
|
e62288e6f1 | ||
|
|
4f978a950b | ||
|
|
36b2868933 | ||
|
|
1763dc13c5 | ||
|
|
b7e4c2c6a2 | ||
|
|
5969612ead | ||
|
|
0d00d4200c | ||
|
|
35f30c293b | ||
|
|
77e20ec446 | ||
|
|
101b57c581 | ||
|
|
b994907be4 | ||
|
|
97ce81daca | ||
|
|
4e61c5e53c | ||
|
|
6871ed051c | ||
|
|
10da870ab3 | ||
|
|
6b5f76296a | ||
|
|
93a4930301 | ||
|
|
7b393f2681 | ||
|
|
fb6266fa15 | ||
|
|
334b245669 | ||
|
|
3a969189b8 | ||
|
|
cef5d11130 | ||
|
|
216c751d81 | ||
|
|
154109dde1 | ||
|
|
312db10439 | ||
|
|
f4f540e805 | ||
|
|
b3ca71580f | ||
|
|
2fc2ac113e | ||
|
|
2ea95ea080 | ||
|
|
22168dcef9 | ||
|
|
e25ffd0c41 | ||
|
|
5595d8f896 | ||
|
|
abed37518d | ||
|
|
57493e87ee | ||
|
|
1bcff6503f | ||
|
|
98571cb37d | ||
|
|
b64cd3c1b8 | ||
|
|
69ced8406b | ||
|
|
1f551b5f59 | ||
|
|
48a2a793c8 | ||
|
|
be116e1ba7 | ||
|
|
3396f831d4 | ||
|
|
a0f6170539 | ||
|
|
731c6f924c | ||
|
|
3011c3d885 | ||
|
|
709b2c8fd9 | ||
|
|
0cfa87e23d | ||
|
|
538ed7dd02 | ||
|
|
180a754e67 | ||
|
|
81ba5f6ee5 | ||
|
|
a15b406cff | ||
|
|
d0bc8f3d05 | ||
|
|
c83f4b4f75 | ||
|
|
0f5425e030 | ||
|
|
f381cc4623 | ||
|
|
decd528079 | ||
|
|
0c5bd57976 | ||
|
|
7362b90c42 | ||
|
|
aef6d6fc85 | ||
|
|
432e209b16 | ||
|
|
a72cac5ea3 | ||
|
|
b9152dc93c | ||
|
|
e5791970da | ||
|
|
c157625645 | ||
|
|
026c7660bc | ||
|
|
be10e66974 | ||
|
|
024fb1a97a | ||
|
|
e4c8b6b676 | ||
|
|
863508b629 | ||
|
|
ef5550bafd | ||
|
|
1cc8d915bc | ||
|
|
9a5f2e4938 | ||
|
|
a747d44cac | ||
|
|
334c13b36c | ||
|
|
aac96da2e2 | ||
|
|
12f3f72a67 | ||
|
|
62f6cfbf9a | ||
|
|
c59e3db1dd | ||
|
|
9252e0e65e | ||
|
|
80ee5e9356 | ||
|
|
be802a28c2 | ||
|
|
b2a8430fa2 | ||
|
|
db5f328539 | ||
|
|
9ac1fbd99b | ||
|
|
022951a9df | ||
|
|
47a0d30e57 | ||
|
|
faeb1964bd | ||
|
|
db8b2fd64b | ||
|
|
37c7fe380b | ||
|
|
537a1e44b1 | ||
|
|
dc9d574b58 | ||
|
|
7c807e6a25 | ||
|
|
f74c6a41ae | ||
|
|
7d5a8c87a1 | ||
|
|
ca719b835e | ||
|
|
9b5ad3a3a0 | ||
|
|
fdfbbb1b04 | ||
|
|
dd91cb91d0 | ||
|
|
290b2249c4 | ||
|
|
8b8e521c56 | ||
|
|
cba88e1af7 | ||
|
|
1661d34d7c | ||
|
|
dc3b1a3c87 | ||
|
|
f55dc19d95 | ||
|
|
6014c15b4f | ||
|
|
a5f835b1eb |
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-reuse.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
|
||||
@@ -23,3 +23,6 @@ Dependencies:
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@stable'
|
||||
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux', 'Windows' ]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(NeoChat)
|
||||
set(PROJECT_VERSION "22.02")
|
||||
set(PROJECT_VERSION "22.06")
|
||||
|
||||
set(KF5_MIN_VERSION "5.88.0")
|
||||
set(QT_MIN_VERSION "5.15.2")
|
||||
@@ -24,7 +24,7 @@ set(KDE_COMPILERSETTINGS_LEVEL 5.84)
|
||||
include(FeatureSummary)
|
||||
include(ECMSetupVersion)
|
||||
include(KDEInstallDirs)
|
||||
include(ECMQMLModules)
|
||||
include(ECMFindQmlModule)
|
||||
include(KDEClangFormat)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
@@ -84,7 +84,7 @@ endif()
|
||||
find_package(Quotient 0.6)
|
||||
set_package_properties(Quotient PROPERTIES
|
||||
TYPE REQUIRED
|
||||
DESCRIPTION "Qt wrapper arround Matrix API"
|
||||
DESCRIPTION "Qt wrapper around Matrix API"
|
||||
URL "https://github.com/quotient-im/libQuotient/"
|
||||
PURPOSE "Talk with matrix server"
|
||||
)
|
||||
@@ -108,7 +108,7 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
PURPOSE "Add image editing capability to image attachments"
|
||||
)
|
||||
|
||||
find_package(QCoro5 COMPONENTS Coro QUIET)
|
||||
find_package(QCoro5 COMPONENTS Core QUIET)
|
||||
if(NOT QCoro5_FOUND)
|
||||
find_package(QCoro REQUIRED)
|
||||
endif()
|
||||
@@ -123,6 +123,8 @@ if(ANDROID)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
ki18n_install(po)
|
||||
|
||||
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)
|
||||
@@ -138,5 +140,9 @@ file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
|
||||
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})
|
||||
|
||||
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
|
||||
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
|
||||
# CI installs dependency headers to _install and _build, which break the reuse check
|
||||
# Fixes the test by excluding this directory
|
||||
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
|
||||
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
|
||||
|
||||
@@ -14,7 +14,7 @@ KConfig and KI18n.
|
||||
|
||||
## Get it
|
||||
|
||||
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions.
|
||||
A stable release [is available](https://apps.kde.org/neochat) for download for Linux distributions.
|
||||
|
||||
|
||||
Along with the stable release, a Flatpak version is available for the nightly
|
||||
|
||||
@@ -120,36 +120,21 @@ ToolBar {
|
||||
room: currentRoom ?? null
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function sendMessage(event) {
|
||||
if (isCompleting) {
|
||||
if (isCompleting && completionMenu.count > 0) {
|
||||
chatBar.complete();
|
||||
|
||||
isCompleting = false;
|
||||
return;
|
||||
}
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
inputField.insert(cursorPosition, "\n")
|
||||
} else {
|
||||
currentRoom.sendTypingNotification(false)
|
||||
chatBar.postMessage()
|
||||
}
|
||||
isCompleting = false;
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: { sendMessage(event) }
|
||||
@@ -244,8 +229,11 @@ ToolBar {
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
timeoutTimer.restart()
|
||||
if (!repeatTimer.running && Config.typingNotifications) {
|
||||
currentRoom.sendTypingNotification(true)
|
||||
}
|
||||
repeatTimer.start()
|
||||
|
||||
currentRoom.cachedInput = text
|
||||
autoAppeared = false;
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property alias source: image.source
|
||||
property string filename
|
||||
property url localPath
|
||||
property string blurhash: ""
|
||||
property int imageWidth: -1
|
||||
property int imageHeight: -1
|
||||
@@ -45,8 +45,6 @@ ApplicationWindow {
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: localPath
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: image.width
|
||||
|
||||
@@ -23,5 +23,6 @@ Kirigami.PlaceholderMessage {
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ LoginStep {
|
||||
}
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
|
||||
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(\:[0-9]+)?$/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,12 @@ QQC2.Popup {
|
||||
quickSearch.forceActiveFocus()
|
||||
quickSearch.text = ""
|
||||
}
|
||||
|
||||
anchors.centerIn: QQC2.Overlay.overlay
|
||||
background: Kirigami.Card {}
|
||||
height: 2 * Math.round(implicitHeight / 2)
|
||||
padding: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
@@ -77,11 +79,30 @@ QQC2.Popup {
|
||||
|
||||
required property string avatar
|
||||
required property var currentRoom
|
||||
required property int index
|
||||
|
||||
// When an item is hovered set the currentIndex of listview to it so that it is highlighted
|
||||
onHoveredChanged: {
|
||||
if (!hovered) {
|
||||
return
|
||||
}
|
||||
cView.currentIndex = index
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
|
||||
_popup.close()
|
||||
}
|
||||
}
|
||||
|
||||
source: avatar != "" ? "image://mxc/" + avatar : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ TimelineContainer {
|
||||
innerObject: Image {
|
||||
id: img
|
||||
|
||||
|
||||
Layout.maximumWidth: imageDelegate.bubbleMaxWidth
|
||||
source: "image://mxc/" + mediaId
|
||||
Layout.maximumHeight: imageDelegate.bubbleMaxWidth / imageDelegate.info.w * imageDelegate.info.h
|
||||
Layout.preferredWidth: imageDelegate.info.w
|
||||
Layout.preferredHeight: imageDelegate.info.h
|
||||
source: model.mediaUrl
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
@@ -95,7 +97,7 @@ TimelineContainer {
|
||||
onTapped: {
|
||||
fullScreenImage.createObject(parent, {
|
||||
filename: eventId,
|
||||
localPath: currentRoom.urlToDownload(eventId),
|
||||
source: model.mediaUrl,
|
||||
blurhash: model.content.info["xyz.amorgan.blurhash"],
|
||||
imageWidth: content.info.w,
|
||||
imageHeight: content.info.h
|
||||
|
||||
@@ -10,41 +10,57 @@ import org.kde.kirigami 2.15 as Kirigami
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
|
||||
RowLayout {
|
||||
Control {
|
||||
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
|
||||
height: label.contentHeight
|
||||
width: ListView.view.width - Kirigami.Units.largeSpacing - x
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: icon
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
}
|
||||
height: sectionDelegate.height + rowLayout.height
|
||||
SectionDelegate {
|
||||
id: sectionDelegate
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||
visible: model.showSection
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: icon.height
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
text: "<style>a {text-decoration: none;}</style><a href=\"https://matrix.to/#/" + author.id + "\" style='color: " + author.color + "'>" + currentRoom.htmlSafeMemberName(author.id) + "</a> " + display
|
||||
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
height: label.contentHeight
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: icon
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: icon.height
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${currentRoom.htmlSafeMemberName(author.id)}</a> ${aggregateDisplay}`
|
||||
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,22 @@ QQC2.ItemDelegate {
|
||||
property bool isEmote: false
|
||||
property bool cardBackground: true
|
||||
|
||||
readonly property int bubbleMaxWidth: Config.compactLayout && !Config.showAvatarInTimeline ? width : (Config.compactLayout ? width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, Kirigami.Units.gridUnit * 20))
|
||||
readonly property int bubbleMaxWidth: Config.compactLayout && !Config.showAvatarInTimeline ? width - Kirigami.Units.largeSpacing * 4 : (Config.compactLayout ? width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, Kirigami.Units.gridUnit * 20))
|
||||
|
||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !applicationWindow().wideScreen
|
||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight &&
|
||||
model.author.isLocalUser &&
|
||||
!applicationWindow().wideScreen &&
|
||||
!Config.compactLayout
|
||||
|
||||
signal openExternally()
|
||||
signal replyClicked(string eventID)
|
||||
|
||||
Component.onCompleted: {
|
||||
if (model.isReply && model.reply === undefined) {
|
||||
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
background: null
|
||||
@@ -72,7 +81,9 @@ QQC2.ItemDelegate {
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
visible: model.showAuthor && Config.showAvatarInTimeline && !showUserMessageOnRight
|
||||
visible: model.showAuthor &&
|
||||
Config.showAvatarInTimeline &&
|
||||
(Config.compactLayout || !showUserMessageOnRight)
|
||||
name: model.author.name ?? model.author.displayName
|
||||
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
|
||||
color: model.author.color
|
||||
@@ -94,8 +105,8 @@ QQC2.ItemDelegate {
|
||||
|
||||
QQC2.Control {
|
||||
id: bubble
|
||||
topPadding: !Config.compactLayout ? Kirigami.Units.largeSpacing : 0
|
||||
bottomPadding: !Config.compactLayout ? Kirigami.Units.largeSpacing : 0
|
||||
topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
|
||||
bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Config.compactLayout ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
|
||||
hoverEnabled: true
|
||||
@@ -239,7 +250,7 @@ QQC2.ItemDelegate {
|
||||
left: bubble.left
|
||||
right: parent.right
|
||||
top: bubble.bottom
|
||||
topMargin: active && !Config.compactLayout ? Kirigami.Units.smallSpacing : 0
|
||||
topMargin: active && Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
|
||||
}
|
||||
height: active ? item.implicitHeight : 0
|
||||
//Layout.bottomMargin: readMarker ? Kirigami.Units.smallSpacing : 0
|
||||
|
||||
@@ -55,14 +55,6 @@ TimelineContainer {
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
|
||||
Component.onCompleted: {
|
||||
if (downloaded) {
|
||||
source = progressInfo.localPath
|
||||
} else {
|
||||
source = currentRoom.urlToMxcUrl(content.url)
|
||||
}
|
||||
}
|
||||
|
||||
onDurationChanged: {
|
||||
if (!duration) {
|
||||
vid.supportStreaming = false;
|
||||
|
||||
@@ -52,7 +52,7 @@ Kirigami.OverlaySheet {
|
||||
|
||||
onClicked: {
|
||||
if (avatarMediaId) {
|
||||
fullScreenImage.createObject(parent, {"filename": displayName, "localPath": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
|
||||
fullScreenImage.createObject(parent, {"filename": displayName, "source": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Kirigami.Page {
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Loading…")
|
||||
QQC2.BusyIndicator {
|
||||
running: loadingIndicator.visible
|
||||
running: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,10 @@ Kirigami.ScrollablePage {
|
||||
property var enteredRoom
|
||||
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
|
||||
|
||||
verticalScrollBarPolicy: collapsedMode ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||
|
||||
onCollapsedModeChanged: if (collapsedMode) {
|
||||
sortFilterRoomListModel.filterText = "";
|
||||
if (page.contentItem && page.contentItem.flickableItem && page.contentItem.flickableItem.QQC2.ScrollBar.vertical) {
|
||||
page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = false;
|
||||
}
|
||||
} else {
|
||||
page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = true;
|
||||
}
|
||||
|
||||
// HACK: the scrollbar is created with a 0 timer, so we need to set the visible flag
|
||||
// after it has been created
|
||||
Timer {
|
||||
running: true
|
||||
interval: 200
|
||||
onTriggered: page.contentItem.flickableItem.QQC2.ScrollBar.vertical.visible = !collapsedMode;
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -79,31 +68,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
visible: page.collapsedMode
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: quickView.item.open();
|
||||
}
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: visible ? page.width : 0
|
||||
height: visible ? Kirigami.Units.gridUnit * 2 : 0
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "search"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
@@ -111,6 +75,31 @@ Kirigami.ScrollablePage {
|
||||
activeFocusOnTab: true
|
||||
clip: accountList.count > 1
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
visible: page.collapsedMode
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: quickView.item.open();
|
||||
}
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: visible ? page.width : 0
|
||||
height: visible ? Kirigami.Units.gridUnit * 2 : 0
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "search"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||
@@ -236,22 +225,15 @@ Kirigami.ScrollablePage {
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
bold: unreadCount > 0
|
||||
label: name ?? ""
|
||||
subtitle: {
|
||||
const txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
|
||||
if (txt.length) {
|
||||
return txt
|
||||
}
|
||||
return " "
|
||||
}
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: subtitleText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
onPressAndHold: {
|
||||
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.open()
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: createRoomListContextMenu()
|
||||
}
|
||||
|
||||
leading: Kirigami.Avatar {
|
||||
@@ -286,18 +268,22 @@ Kirigami.ScrollablePage {
|
||||
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.open()
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRoomListContextMenu() {
|
||||
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.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ Kirigami.ScrollablePage {
|
||||
if(pageStack.lastItem == page) {
|
||||
pageStack.pop()
|
||||
}
|
||||
} else if (page.currentRoom.isInvite) {
|
||||
page.currentRoom.clearInvitationNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,78 +167,9 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: hoverActions
|
||||
property var event: null
|
||||
property bool showEdit: event && (event.author.id === Controller.activeConnection.localUserId && (event.eventType === "emote" || event.eventType === "message"))
|
||||
property var bubble: null
|
||||
property var hovered: bubble && bubble.hovered
|
||||
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
|
||||
onVisibleDelayedChanged: if (visibleDelayed) {
|
||||
visible = true;
|
||||
} else {
|
||||
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
|
||||
// See https://invent.kde.org/network/neochat/-/issues/333
|
||||
hoverActionsTimer.restart();
|
||||
}
|
||||
Timer {
|
||||
id: hoverActionsTimer
|
||||
interval: 200
|
||||
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
|
||||
}
|
||||
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0) - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0)) : 0
|
||||
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
|
||||
visible: false
|
||||
|
||||
property var updateFunction
|
||||
|
||||
property alias childWidth: hoverActionsRow.width
|
||||
property alias childHeight: hoverActionsRow.height
|
||||
|
||||
RowLayout {
|
||||
id: hoverActionsRow
|
||||
z: 4
|
||||
spacing: 0
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
margin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("React")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onClicked: emojiDialog.open();
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
onReact: {
|
||||
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Edit")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
visible: hoverActions.showEdit
|
||||
icon.name: "document-edit"
|
||||
onClicked: {
|
||||
if (hoverActions.showEdit) {
|
||||
ChatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
|
||||
}
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Reply")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onClicked: {
|
||||
ChatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
CollapseStateProxyModel {
|
||||
id: collapseStateProxyModel
|
||||
sourceModel: sortedMessageEventModel
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -251,7 +184,7 @@ Kirigami.ScrollablePage {
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
highlightMoveDuration: 500
|
||||
|
||||
model: !isLoaded ? undefined : sortedMessageEventModel
|
||||
model: !isLoaded ? undefined : collapseStateProxyModel
|
||||
|
||||
MessageEventModel {
|
||||
id: messageEventModel
|
||||
@@ -400,12 +333,6 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (currentRoom) {
|
||||
if (currentRoom.timelineSize < 20) {
|
||||
currentRoom.getPreviousContent(50);
|
||||
}
|
||||
}
|
||||
|
||||
positionViewAtBeginning();
|
||||
}
|
||||
|
||||
@@ -477,6 +404,80 @@ Kirigami.ScrollablePage {
|
||||
function goToEvent(eventID) {
|
||||
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: hoverActions
|
||||
property var event: null
|
||||
property bool showEdit: event && (event.author.id === Controller.activeConnection.localUserId && (event.eventType === "emote" || event.eventType === "message"))
|
||||
property var bubble: null
|
||||
property var hovered: bubble && bubble.hovered
|
||||
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
|
||||
onVisibleDelayedChanged: if (visibleDelayed) {
|
||||
visible = true;
|
||||
} else {
|
||||
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
|
||||
// See https://invent.kde.org/network/neochat/-/issues/333
|
||||
hoverActionsTimer.restart();
|
||||
}
|
||||
Timer {
|
||||
id: hoverActionsTimer
|
||||
interval: 200
|
||||
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
|
||||
}
|
||||
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0) - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0)) : 0
|
||||
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
|
||||
visible: false
|
||||
|
||||
property var updateFunction
|
||||
|
||||
property alias childWidth: hoverActionsRow.width
|
||||
property alias childHeight: hoverActionsRow.height
|
||||
|
||||
RowLayout {
|
||||
id: hoverActionsRow
|
||||
z: 4
|
||||
spacing: 0
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
margin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("React")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onClicked: emojiDialog.open();
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
onReact: {
|
||||
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Edit")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
visible: hoverActions.showEdit
|
||||
icon.name: "document-edit"
|
||||
onClicked: {
|
||||
if (hoverActions.showEdit) {
|
||||
ChatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
|
||||
}
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Reply")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onClicked: {
|
||||
ChatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -491,14 +492,14 @@ Kirigami.ScrollablePage {
|
||||
onEditLastUserMessage: {
|
||||
const targetMessage = messageEventModel.getLastLocalUserMessageEventId();
|
||||
if (targetMessage) {
|
||||
ChatBoxHelper.edit(targetMessage["body"], targetMessage["body"], targetMessage["event_id"]);
|
||||
ChatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], targetMessage["event_id"]);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
onReplyPreviousUserMessage: {
|
||||
const replyResponse = messageEventModel.getLatestMessageFromIndex(0);
|
||||
if (replyResponse && replyResponse["event_id"]) {
|
||||
ChatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["event"], replyResponse["sender_id"]);
|
||||
ChatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["message"], replyResponse["sender_id"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,7 +607,7 @@ Kirigami.ScrollablePage {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(page, {
|
||||
selectedText: selectedText,
|
||||
author: event.author,
|
||||
message: event.message,
|
||||
message: event.display,
|
||||
eventId: event.eventId,
|
||||
formattedBody: event.formattedBody,
|
||||
source: event.source,
|
||||
|
||||
@@ -37,6 +37,13 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
function onInitiated() {
|
||||
pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Kirigami.Icon {
|
||||
source: "org.kde.neochat"
|
||||
|
||||
@@ -19,6 +19,7 @@ Kirigami.OverlayDrawer {
|
||||
readonly property var room: RoomManager.currentRoom
|
||||
|
||||
width: modal ? undefined : actualWidth
|
||||
|
||||
readonly property int minWidth: Kirigami.Units.gridUnit * 15
|
||||
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
|
||||
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
|
||||
@@ -62,14 +63,20 @@ Kirigami.OverlayDrawer {
|
||||
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
|
||||
// If modal has been changed and the drawer is closed automatically then dim on popup open will have been switched off in main.qml so switch it back on after the animation completes.
|
||||
// This is to avoid dim being active for a split second when the drawer is switched to modal which looks terrible.
|
||||
onAnimatingChanged: if (dim === false) dim = undefined
|
||||
|
||||
topPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
contentItem: Loader {
|
||||
id: loader
|
||||
active: roomDrawer.drawerOpen
|
||||
sourceComponent: ColumnLayout {
|
||||
id: columnLayout
|
||||
property alias userSearchText: userListSearchField.text
|
||||
spacing: 0
|
||||
Kirigami.AbstractApplicationHeader {
|
||||
Layout.fillWidth: true
|
||||
@@ -289,6 +296,7 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
|
||||
onRoomChanged: {
|
||||
loader.item.userSearchText = ""
|
||||
if (room == null) {
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -23,42 +23,42 @@ Kirigami.ScrollablePage {
|
||||
|
||||
ListView {
|
||||
model: AccountRegistry
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
delegate: Kirigami.BasicListItem {
|
||||
text: model.connection.localUser.displayName
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: model.connection.localUserId
|
||||
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
|
||||
|
||||
text: model.connection.localUser.displayName
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: model.connection.localUserId
|
||||
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
|
||||
onClicked: {
|
||||
Controller.activeConnection = model.connection
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
Controller.activeConnection = model.connection
|
||||
pageStack.layers.pop()
|
||||
trailing: RowLayout {
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection, true)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection, true)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
Controls.Button {
|
||||
visible: avatar.source.length !== 0
|
||||
visible: avatar.source.toString().length !== 0
|
||||
icon.name: "edit-clear"
|
||||
|
||||
onClicked: avatar.source = ""
|
||||
|
||||
@@ -175,6 +175,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
Kirigami.FormLayout {
|
||||
Layout.maximumWidth: parent.width
|
||||
QQC2.CheckBox {
|
||||
Kirigami.FormData.label: "Show Avatar:"
|
||||
text: i18n("In Chat")
|
||||
|
||||
@@ -26,36 +26,35 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
delegate: Kirigami.BasicListItem {
|
||||
text: model.displayName
|
||||
subtitle: model.id
|
||||
icon: "network-connect"
|
||||
trailing: RowLayout {
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Edit device name")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
renameSheet.index = model.index
|
||||
renameSheet.name = model.displayName
|
||||
renameSheet.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Controls.ToolButton {
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Logout device")
|
||||
iconName: "edit-delete-remove"
|
||||
onTriggered: {
|
||||
passwordSheet.index = index
|
||||
passwordSheet.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
@@ -96,7 +98,7 @@ Kirigami.ScrollablePage {
|
||||
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
|
||||
|
||||
this.fileDialog.chosen.connect((url) => {
|
||||
emojiModel.addEmoji(emojiField.text, url)
|
||||
emojiModel.addEmoji(emojiCreator.name, url)
|
||||
this.fileDialog = null
|
||||
})
|
||||
this.fileDialog.onRejected.connect(() => {
|
||||
|
||||
@@ -94,7 +94,15 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Use s/text/replacement syntax to edit your last message")
|
||||
id: quickEditCheckbox
|
||||
Layout.maximumWidth: parent.width
|
||||
contentItem: QQC2.Label {
|
||||
text: i18n("Use s/text/replacement syntax to edit your last message")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: quickEditCheckbox.indicator.width + quickEditCheckbox.spacing
|
||||
wrapMode: QQC2.Label.Wrap
|
||||
}
|
||||
checked: Config.allowQuickEdit
|
||||
enabled: !Config.isAllowQuickEditImmutable
|
||||
onToggled: {
|
||||
@@ -102,6 +110,24 @@ Kirigami.ScrollablePage {
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Send Typing Notifications")
|
||||
checked: Config.typingNotifications
|
||||
enabled: !Config.isTypingNotificationsImmutable
|
||||
onToggled: {
|
||||
Config.typingNotifications = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
QQC2.CheckBox {
|
||||
text: i18n("Automatically hide/unhide the room information when resizing the window")
|
||||
checked: Config.autoRoomInfoDrawer
|
||||
enabled: !Config.isAutoRoomInfoDrawerImmutable
|
||||
onToggled: {
|
||||
Config.autoRoomInfoDrawer = checked
|
||||
Config.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<name xml:lang="sl">NeoChat</name>
|
||||
<name xml:lang="sv">NeoChat</name>
|
||||
<name xml:lang="ta">நியோச்சாட்</name>
|
||||
<name xml:lang="tr">NeoChat</name>
|
||||
<name xml:lang="uk">NeoChat</name>
|
||||
<name xml:lang="x-test">xxNeoChatxx</name>
|
||||
<name xml:lang="zh-CN">NeoChat</name>
|
||||
@@ -65,6 +66,7 @@
|
||||
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
|
||||
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
|
||||
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
|
||||
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
|
||||
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
|
||||
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
|
||||
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
|
||||
@@ -73,7 +75,7 @@
|
||||
<p xml:lang="ar">نيوتشات هو عميل ماتركس Matrix. يتيح لك إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك باستخدام بروتوكول ماتركس</p>
|
||||
<p xml:lang="az">NeoChat Mtrix müştərisidir. O, Matrix protokolundan istifadə edərək, ailənizə, dostlarınıza, iş yoldaşlarınıza mətn, səsli və görüntülü ismarıclar göndərməyə imkan verir.</p>
|
||||
<p xml:lang="ca">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
|
||||
<p xml:lang="ca-valencia">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics utilitzant el protocol Matrix.</p>
|
||||
<p xml:lang="de">NeoChat ist ein Matrix-Client. Er ermöglicht Ihnen das Senden von Textnachrichten, Videos und Audiodateien an Ihre Familie, Kollegen und Freunde unter Verwendung des Matrix-Protokolls.</p>
|
||||
<p xml:lang="en-GB">NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
|
||||
<p xml:lang="es">NeoChat es un cliente para Matrix. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos usando el protocolo Matrix.</p>
|
||||
@@ -82,6 +84,7 @@
|
||||
<p xml:lang="fr">NeoChat est un client Matrix. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis en utilisant le protocole Matrix.</p>
|
||||
<p xml:lang="hu">A NeoChat egy Matrix kliens. Szöveges üzeneteket, videókat ésaudio fájlokat küldhet családjának, kollégáinak és barátainak a Matrix protokoll használatával.</p>
|
||||
<p xml:lang="ia">NeoChat es un cliente de Matrix. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante le protocollo de Matrix.</p>
|
||||
<p xml:lang="id">NeoChat adalah sebuah klien Matrix. Memungkinkan Anda untuk mengirim pesan teks, file video dan audio ke keluarga, kolega dan teman Anda menggunakan protokol Matrix.</p>
|
||||
<p xml:lang="it">NeoChat è un client Matrix. Ti consente di inviare messaggi di testo, file video e audio a familiari, colleghi e amici utilizzando il protocollo Matrix.</p>
|
||||
<p xml:lang="ko">NeoChat은 Matrix 클라이언트입니다. Matrix 프로토콜을 사용하여 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다.</p>
|
||||
<p xml:lang="nl">NeoChat is een Matrix-client. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden met het Matrix-protocol.</p>
|
||||
@@ -91,6 +94,7 @@
|
||||
<p xml:lang="sk">NeoChat je Matrix klient. Umožňuje vám posielať textové správy, videá a zvukové súbory rodine, kolegom a priateľom pomocou protokolu Matrix.</p>
|
||||
<p xml:lang="sl">NeoChat je odjemalec Matrixa. Dovoljuje vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, kolegom in prijateljem z uporabo protokola Matrix.</p>
|
||||
<p xml:lang="sv">NeoChat är en Matrix-klient. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner med användning av Matrix-protokollet.</p>
|
||||
<p xml:lang="tr">NeoChat, bir Matrix istemcisidir. Matrix protokolünü kullanarak ailenize, iş arkadaşlarınıza, arkadaşlarınıza ve başkalarına metin iletileri, video ve ses dosyaların göndermenize olanak verir.</p>
|
||||
<p xml:lang="uk">NeoChat — клієнт мережі обміну повідомленнями Matrix. За допомогою цієї програми ви зможете надсилати текстові повідомлення, відео та звукові файли вашій родині, колегам та друзям за допомогою протоколу Matrix.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.xx</p>
|
||||
<p xml:lang="zh-CN">NeoChat 是一个 Matrix 客户端。 它允许您使用 Matrix 协议向您的家人、同事和朋友发送文本消息、视频和音频文件。</p>
|
||||
@@ -98,7 +102,7 @@
|
||||
<p xml:lang="ar">ماتريكس هو بروتوكول اتصال لامركزي ، يعيد المستخدم إلى السيطرة. يطبق نيوتشات حاليًا جزءًا كبيرًا من الميفاق باستثناء الدردشات المشفرة ودردشة الفيديو.</p>
|
||||
<p xml:lang="az">Matrix, istifadəçini nəzarətdə saxlayan, mərkəzləşməmişi rabitə protokoludur. NeoChat, söhbətin və video əlaqəsinin şifrələnməsindən başqa bir çox protokolları həyata keçirə bilir.</p>
|
||||
<p xml:lang="ca">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
|
||||
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
|
||||
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment NeoChat implementa una gran part del protocol amb l'excepció dels chats encriptats i els chats de vídeo.</p>
|
||||
<p xml:lang="de">Matrix ist ein dezentralisiertes Kommunikationsprotokoll, das dem Benutzer wieder die Kontrolle zurückgibt. Derzeit implementiert NeoChat einen großen Teil des Protokolls mit der Ausnahme von verschlüsselten Chats und Video-Chat.</p>
|
||||
<p xml:lang="en-GB">Matrix is a decentralised communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
|
||||
<p xml:lang="es">Matrix es un protocolo de comunicaciones descentralizado, que devuelve el control al usuario. En la actualidad, NeoChat implementa gran parte del protocolo con la excepción de chats cifrados y chats de vídeo.</p>
|
||||
@@ -107,6 +111,7 @@
|
||||
<p xml:lang="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions chiffrées et du chat vidéo.</p>
|
||||
<p xml:lang="hu">A Matrix egy decentralizált kommunikációs protokoll, amely a felhasználók kezébe adja az irányítást.</p>
|
||||
<p xml:lang="ia">Matrix es un protocollo de communication decentrate, ponente le usator in le controlo. Currentemente NeoChat implementa un grande parte del protocollo con le exception de conversationes cryptate e conversationes video.</p>
|
||||
<p xml:lang="id">Matrix adalah protokol komunikasi terdesentralisasi, menempatkan pengguna kembali dalam kendali. Saat ini NeoChat mengimplementasikan sebagian besar protokol dengan pengecualian obrolan terenkripsi dan obrolan video.</p>
|
||||
<p xml:lang="it">Matrix è un protocollo di comunicazione decentralizzato, che restituisce all'utente il controllo. Attualmente NeoChat implementa gran parte del protocollo ad eccezione delle chat cifrate e delle chat video.</p>
|
||||
<p xml:lang="ko">Matrix는 사용자에게 제어권을 돌려 주는 분산 통신 프로토콜입니다. NeoChat은 암호화된 대화 및 영상 통화를 제외한 프로토콜의 대부분 기능을 구현합니다.</p>
|
||||
<p xml:lang="nl">Matrix is een gedecentraliseerd communicatieprotocol, dat de gebruiker de controle teruggeeft. Op dit moment implementeert NeoChat grote delen van het protocol met de uitzondering van versleutelde chats en video-chat.</p>
|
||||
@@ -116,6 +121,7 @@
|
||||
<p xml:lang="sk">Matrix je decentralizovaný komunikačný protokol, ktorý používateľovi vracia kontrolu. V súčasnosti NeoChat implementuje veľkú časť protokolu s výnimkou šifrovaných chatov a videohovorov.</p>
|
||||
<p xml:lang="sl">Matrix je decentraliziran komunikacijski protokol, kjer ima uporabnik uporabnik kontrolo rabe. Trenutno ima NeoChat izveden velik del protokola z izjemo šifriranih klepetov in video klepetov.</p>
|
||||
<p xml:lang="sv">Matrix är ett decentraliserat kommunikationsprotokoll, som ger tillbaka kontrollen till användaren. För närvarande implementerar NeoChat en stor del av protokollet, med undantag för krypterad chatt och videochatt.</p>
|
||||
<p xml:lang="tr">Matrix; tam denetimi kullanıcıya bırakan, merkezi olmayan bir iletişim protokolüdür. Şu anda NeoChat, uçtan uca şifrelenmiş metin ve video sohbetleri dışında protokolün büyük bir bölümünü bünyesinde bulundurur.</p>
|
||||
<p xml:lang="uk">Matrix — протокол децентралізованого спілкування, який передає контроль над даними користувачеві. У поточній версії NeoChat реалізовано більшу частину протоколу, окрім зашифрованого спілкування та відеоспілкування.</p>
|
||||
<p xml:lang="x-test">xxMatrix is a decentralized communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.xx</p>
|
||||
<p xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前,NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
|
||||
@@ -123,7 +129,7 @@
|
||||
<p xml:lang="ar">يعمل نيوتشات على كل من الأجهزة المحمولة وسطح المكتب مع توفير تجربة مستخدم متسقة.</p>
|
||||
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
|
||||
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
|
||||
<p xml:lang="ca-valencia">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
|
||||
<p xml:lang="de">NeoChat funktioniert sowohl auf dem Mobiltelefon als auch auf dem Arbeitsfläche und bietet ein einheitliches Benutzererlebnis. </p>
|
||||
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
|
||||
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
|
||||
@@ -132,6 +138,7 @@
|
||||
<p xml:lang="fr">NeoChat fonctionne aussi bien sur les mobiles que sur les ordinateurs de bureau, tout en offrant une expérience utilisateur cohérente.</p>
|
||||
<p xml:lang="hu">A NeoChat mobilon és asztali számítógépen is működik, egységes felhasználói élményt nyújtva.</p>
|
||||
<p xml:lang="ia">NeoChat functiona sia sur mobile que ur scriptorio durante que forni un experientia de usator consistente.</p>
|
||||
<p xml:lang="id">NeoChat berfungsi baik di ponsel dan desktop sambil memberikan pengalaman pengguna yang konsisten.</p>
|
||||
<p xml:lang="it">NeoChat funziona sia su dispositivi mobili che desktop, fornendo un'esperienza utente coerente.</p>
|
||||
<p xml:lang="ko">NeoChat은 모바일과 데스크톱 모두에서 일관된 사용자 경험을 제공합니다.</p>
|
||||
<p xml:lang="nl">NeoChat werkt zowel op de mobiel en het bureaublad met het leveren van een consistente gebruikerservaring.</p>
|
||||
@@ -141,12 +148,13 @@
|
||||
<p xml:lang="sk">NeoChat funguje na mobilných aj stolových počítačoch a poskytuje konzistentný používateľský zážitok.</p>
|
||||
<p xml:lang="sl">NeoChat deluje tako na mobilnih kot na namiznih platformah z zagotavljanjem konsistentne uporabniške izkušnje.</p>
|
||||
<p xml:lang="sv">NeoChat fungerar både på mobil och skrivbord och tillhandahåller en konsekvent användarupplevelse.</p>
|
||||
<p xml:lang="tr">NeoChat, hem masaüstü hem de taşınabilir ortamlarda çalışarak tutarlı bir kullanıcı deneyimi sunar.</p>
|
||||
<p xml:lang="uk">NeoChat працює на мобільних пристроях та звичайних комп'ютерах, маючи однорідний інтерфейс на усіх підтримуваних пристроях.</p>
|
||||
<p xml:lang="x-test">xxNeoChat works both on mobile and desktop while providing a consistent user experience.xx</p>
|
||||
<p xml:lang="zh-CN">NeoChat 在移动设备和桌面上均可用,并提供一致的用户体验。</p>
|
||||
</description>
|
||||
<url type="homepage">https://apps.kde.org/neochat/</url>
|
||||
<url type="bugtracker">https://invent.kde.org/network/neochat/-/issues</url>
|
||||
<url type="bugtracker">https://bugs.kde.org/buglist.cgi?component=General&product=NeoChat</url>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
@@ -176,12 +184,16 @@
|
||||
<developer_name xml:lang="sk">KDE Komunita</developer_name>
|
||||
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
|
||||
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
|
||||
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
|
||||
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
|
||||
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
|
||||
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<value key="KDE::matrix">#neochat:kde.org</value>
|
||||
<custom>
|
||||
<value key="KDE::matrix">#neochat:kde.org</value>
|
||||
</custom>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application-mobile.png</image>
|
||||
@@ -194,15 +206,38 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="22.06" date="2022-06-24">
|
||||
<url>https://www.plasma-mobile.org/2022/06/28/plasma-mobile-gear-22-06/</url>
|
||||
<description>
|
||||
<p>This release brings you various small bugfixes and improvements:</p>
|
||||
<ul>
|
||||
<li>Sending of typing notifications can now be disabled.</li>
|
||||
<li>In the room list, the scrollbar will now disappear correctly when it is not needed.</li>
|
||||
<li>On wayland, NeoChat will now raise correctly when clicking on a notification.</li>
|
||||
<li>Several bugs have been fixed that would sometimes cause messages containing markdown and/or HTML elements to be sent incorrectly.</li>
|
||||
<li>The quick switcher can now be controlled using the mouse.</li>
|
||||
<li>There is now an option to disable automatic room sidebar opening when resizing the window.</li>
|
||||
<li>Creation of custom emojis has been fixed.</li>
|
||||
<li>Editing or replying to the last message using the keyboard shortcuts now works correctly.</li>
|
||||
<li>When switching between rooms using the keyboard, the switching direction is now correct.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="22.04" date="2022-04-26">
|
||||
<url>https://www.plasma-mobile.org/2022/04/26/plasma-mobile-gear-22-04/</url>
|
||||
<description>
|
||||
<p>NeoChat now lets you filter and enter a room directly from KRunner (Plasma Search). Aside from that there is also various bug fixes regarding the typing notifications.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="22.02" date="2022-02-09">
|
||||
<description>
|
||||
<p>NeoChat 22.02 focus on stability and adds a few quality of life improvements</p>
|
||||
<ul>
|
||||
<li>Add support for minimizing to system tray on startup</li>
|
||||
<li>Improved internet connectivity check</li>
|
||||
<li>Add support for sharing images and files with other apps (Nextcloud, Imgur, ...)</li>
|
||||
<li>Implement adding labels for account. This allow for an easier organization when using multiple accounts.</li>
|
||||
<li>Redesign of our config dialogs to follow the new Plasma System Settings style</li>
|
||||
<ul>
|
||||
<li>Add support for minimizing to system tray on startup</li>
|
||||
<li>Improved internet connectivity check</li>
|
||||
<li>Add support for sharing images and files with other apps (Nextcloud, Imgur, ...)</li>
|
||||
<li>Implement adding labels for account. This allow for an easier organization when using multiple accounts.</li>
|
||||
<li>Redesign of our config dialogs to follow the new Plasma System Settings style</li>
|
||||
<li>Fix various others issues and small feature requests. Decreasing the total amount of open issues by 20%.</li>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Name=NeoChat
|
||||
Name[ar]=نيوتشات
|
||||
Name[az]=NeoChat
|
||||
@@ -15,6 +16,7 @@ Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[id]=NeoChat
|
||||
Name[it]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
@@ -29,6 +31,7 @@ Name[sk]=NeoChat
|
||||
Name[sl]=NeoChat
|
||||
Name[sv]=NeoChat
|
||||
Name[ta]=நியோச்சாட்
|
||||
Name[tr]=NeoChat
|
||||
Name[uk]=NeoChat
|
||||
Name[x-test]=xxNeoChatxx
|
||||
Name[zh_CN]=NeoChat
|
||||
@@ -46,6 +49,7 @@ GenericName[fi]=Matrix-asiakas
|
||||
GenericName[fr]=Client « Matrix »
|
||||
GenericName[hu]=Matrix kliens
|
||||
GenericName[ia]=Cliente de Matrice
|
||||
GenericName[id]=Klien Matrix
|
||||
GenericName[it]=Client Matrix
|
||||
GenericName[ko]=Matrix 클라이언트
|
||||
GenericName[lt]=Matrix kliento programą
|
||||
@@ -60,6 +64,7 @@ GenericName[sk]=Matrix Client
|
||||
GenericName[sl]=Odjemalec Matrix
|
||||
GenericName[sv]=Matrix-klient
|
||||
GenericName[ta]=Matrix வாங்கி
|
||||
GenericName[tr]=Matrix İstemcisi
|
||||
GenericName[uk]=Клієнт Matrix
|
||||
GenericName[x-test]=xxMatrix Clientxx
|
||||
GenericName[zh_CN]=Matrix 客户端
|
||||
@@ -76,6 +81,7 @@ Comment[fi]=Asiakas Matrix-yhteyskäytännölle
|
||||
Comment[fr]=Client pour le protocole « Matrix »
|
||||
Comment[hu]=Kliens a Matrix protokollhoz
|
||||
Comment[ia]=Cliente per le protocollo de Matrix
|
||||
Comment[id]=Klien untuk protokol Matrix
|
||||
Comment[it]=Client per il protocollo Matrix
|
||||
Comment[ko]=Matrix 프로토콜용 클라이언트
|
||||
Comment[lt]=Matrix protokolo kliento programa
|
||||
@@ -90,6 +96,7 @@ Comment[sk]=Klient protokolu Matrix
|
||||
Comment[sl]=Odjemalec za protokol Matrix
|
||||
Comment[sv]=Klient för protokollet Matrix
|
||||
Comment[ta]=Matrix நெறிமுறைக்கான வாங்கி
|
||||
Comment[tr]=Matrix protokolü için istemci
|
||||
Comment[uk]=Клієнт протоколу Matrix
|
||||
Comment[x-test]=xxClient for the Matrix protocolxx
|
||||
Comment[zh_CN]=为 Matrix 协议打造的客户端
|
||||
|
||||
2030
po/ar/neochat.po
Normal file
2030
po/ar/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2163
po/az/neochat.po
Normal file
2163
po/az/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2026
po/ca/neochat.po
Normal file
2026
po/ca/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2028
po/ca@valencia/neochat.po
Normal file
2028
po/ca@valencia/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2009
po/cs/neochat.po
Normal file
2009
po/cs/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2111
po/da/neochat.po
Normal file
2111
po/da/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2176
po/de/neochat.po
Normal file
2176
po/de/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2178
po/en_GB/neochat.po
Normal file
2178
po/en_GB/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2209
po/es/neochat.po
Normal file
2209
po/es/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2199
po/eu/neochat.po
Normal file
2199
po/eu/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2168
po/fi/neochat.po
Normal file
2168
po/fi/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2197
po/fr/neochat.po
Normal file
2197
po/fr/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2205
po/hu/neochat.po
Normal file
2205
po/hu/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2103
po/ia/neochat.po
Normal file
2103
po/ia/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2018
po/id/neochat.po
Normal file
2018
po/id/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2195
po/it/neochat.po
Normal file
2195
po/it/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2000
po/ja/neochat.po
Normal file
2000
po/ja/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2091
po/ko/neochat.po
Normal file
2091
po/ko/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2207
po/nl/neochat.po
Normal file
2207
po/nl/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2011
po/nn/neochat.po
Normal file
2011
po/nn/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2161
po/pa/neochat.po
Normal file
2161
po/pa/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2176
po/pl/neochat.po
Normal file
2176
po/pl/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2016
po/pt/neochat.po
Normal file
2016
po/pt/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2177
po/pt_BR/neochat.po
Normal file
2177
po/pt_BR/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2009
po/ru/neochat.po
Normal file
2009
po/ru/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2190
po/sk/neochat.po
Normal file
2190
po/sk/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2025
po/sl/neochat.po
Normal file
2025
po/sl/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2206
po/sv/neochat.po
Normal file
2206
po/sv/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2091
po/ta/neochat.po
Normal file
2091
po/ta/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2017
po/tok/neochat.po
Normal file
2017
po/tok/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2012
po/tr/neochat.po
Normal file
2012
po/tr/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2227
po/uk/neochat.po
Normal file
2227
po/uk/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2004
po/zh_CN/neochat.po
Normal file
2004
po/zh_CN/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
2004
po/zh_TW/neochat.po
Normal file
2004
po/zh_TW/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
12
qml/main.qml
12
qml/main.qml
@@ -148,10 +148,14 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
id: contextDrawer
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
modal: !root.wideScreen || !enabled
|
||||
onEnabledChanged: drawerOpen = enabled && !modal
|
||||
onModalChanged: drawerOpen = !modal
|
||||
onModalChanged: {
|
||||
if (Config.autoRoomInfoDrawer) {
|
||||
drawerOpen = !modal
|
||||
dim = false
|
||||
}
|
||||
}
|
||||
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 && (root.wideScreen || pageStack.currentIndex > 0)
|
||||
}
|
||||
@@ -309,10 +313,10 @@ Kirigami.ApplicationWindow {
|
||||
Connections {
|
||||
target: root.roomPage
|
||||
function onSwitchRoomUp() {
|
||||
roomList.goToNextRoom();
|
||||
roomList.goToPreviousRoom();
|
||||
}
|
||||
function onSwitchRoomDown() {
|
||||
roomList.goToPreviousRoom();
|
||||
roomList.goToNextRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ add_executable(neochat
|
||||
chatboxhelper.cpp
|
||||
commandmodel.cpp
|
||||
webshortcutmodel.cpp
|
||||
spellcheckhighlighter.cpp
|
||||
blurhash.cpp
|
||||
blurhashimageprovider.cpp
|
||||
joinrulesevent.cpp
|
||||
collapsestateproxymodel.cpp
|
||||
../res.qrc
|
||||
)
|
||||
|
||||
@@ -62,7 +62,8 @@ if(NOT ANDROID)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat PRIVATE ../res_desktop.qrc)
|
||||
target_sources(neochat PRIVATE ../res_desktop.qrc runner.cpp)
|
||||
target_compile_definitions(neochat PRIVATE -DHAVE_RUNNER)
|
||||
else()
|
||||
target_sources(neochat PRIVATE ../res_android.qrc)
|
||||
endif()
|
||||
@@ -125,7 +126,7 @@ if(ANDROID)
|
||||
)
|
||||
else()
|
||||
target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::KIOWidgets)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
|
||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||
endif()
|
||||
|
||||
if(TARGET KF5::DBusAddons)
|
||||
@@ -138,3 +139,8 @@ if (TARGET KF5::KIOWidgets)
|
||||
endif()
|
||||
|
||||
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -106,20 +106,7 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
CustomEmojiModel *cem)
|
||||
{
|
||||
QString rawText = text;
|
||||
auto stringList = text.split(QStringLiteral("```"));
|
||||
QString cleanedText;
|
||||
const auto count = stringList.count();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i % 2 == 0) {
|
||||
if (i + 1 != count) {
|
||||
cleanedText += stringList[i].toHtmlEscaped() + QStringLiteral("```");
|
||||
} else {
|
||||
cleanedText += stringList[i].toHtmlEscaped();
|
||||
}
|
||||
} else {
|
||||
cleanedText += stringList[i] + QStringLiteral("```");
|
||||
}
|
||||
}
|
||||
QString cleanedText = text;
|
||||
|
||||
auto preprocess = [cem](const QString &it) -> QString {
|
||||
if (cem == nullptr) {
|
||||
|
||||
@@ -204,6 +204,12 @@ void ChatDocumentHandler::replaceAutoComplete(const QString &word)
|
||||
}
|
||||
|
||||
cursor.insertHtml(word);
|
||||
|
||||
// Add space after autocomplete if not already there
|
||||
if (!cursor.block().text().endsWith(QStringLiteral(" "))) {
|
||||
cursor.insertText(QStringLiteral(" "));
|
||||
}
|
||||
|
||||
m_lastState = cursor.block().text();
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
|
||||
84
src/collapsestateproxymodel.cpp
Normal file
84
src/collapsestateproxymodel.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "collapsestateproxymodel.h"
|
||||
#include "messageeventmodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
|
||||
!= QLatin1String("state") // If this is not a state, show it
|
||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
||||
!= QLatin1String("state") // If this is the first state in a block, show it. TODO hidden events?
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool() // If it's a new day, show it
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
|
||||
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0),
|
||||
MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
|
||||
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
|
||||
}
|
||||
|
||||
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == AggregateDisplayRole) {
|
||||
return aggregateEventToString(mapToSource(index).row());
|
||||
}
|
||||
return sourceModel()->data(mapToSource(index), role);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
|
||||
{
|
||||
auto roles = sourceModel()->roleNames();
|
||||
roles[AggregateDisplayRole] = "aggregateDisplay";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||
{
|
||||
QStringList parts;
|
||||
for (int i = sourceRow; i >= 0; i--) {
|
||||
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
|
||||
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole) != QLatin1String("state") // If it's not a state event
|
||||
|| (i > 0
|
||||
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
|
||||
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
|
||||
|| (i > 0
|
||||
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
|
||||
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!parts.isEmpty()) {
|
||||
QStringList chunks;
|
||||
while (!parts.isEmpty()) {
|
||||
chunks += QString();
|
||||
int count = 1;
|
||||
auto part = parts.takeFirst();
|
||||
chunks.last() += part;
|
||||
while (!parts.isEmpty() && parts.first() == part) {
|
||||
parts.removeFirst();
|
||||
count++;
|
||||
}
|
||||
if (count > 1) {
|
||||
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
|
||||
}
|
||||
}
|
||||
QString text = chunks.takeFirst();
|
||||
|
||||
if (chunks.size() > 0) {
|
||||
while (chunks.size() > 1) {
|
||||
text += i18nc("[action 1], [action 2 and action 3]", ", ");
|
||||
text += chunks.takeFirst();
|
||||
}
|
||||
text += i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||
text += chunks.takeFirst();
|
||||
}
|
||||
return text;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
23
src/collapsestateproxymodel.h
Normal file
23
src/collapsestateproxymodel.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPair>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
|
||||
class CollapseStateProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Roles {
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
||||
};
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||
};
|
||||
@@ -70,19 +70,21 @@ Controller::Controller(QObject *parent)
|
||||
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);
|
||||
m_trayIcon = new TrayIcon(this);
|
||||
m_trayIcon->show();
|
||||
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||
}
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this, trayIcon]() {
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this]() {
|
||||
if (NeoChatConfig::self()->systemTray()) {
|
||||
trayIcon->show();
|
||||
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
m_trayIcon = new TrayIcon(this);
|
||||
m_trayIcon->show();
|
||||
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
} else {
|
||||
trayIcon->hide();
|
||||
disconnect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
delete m_trayIcon;
|
||||
m_trayIcon = nullptr;
|
||||
}
|
||||
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ class QKeySequences;
|
||||
|
||||
class NeoChatRoom;
|
||||
class NeoChatUser;
|
||||
class TrayIcon;
|
||||
class QQuickWindow;
|
||||
|
||||
namespace QKeychain
|
||||
@@ -100,6 +101,7 @@ private:
|
||||
|
||||
QPointer<Connection> m_connection;
|
||||
bool m_busy = false;
|
||||
TrayIcon *m_trayIcon = nullptr;
|
||||
|
||||
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
|
||||
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const AccountSettings &account);
|
||||
|
||||
29
src/main.cpp
29
src/main.cpp
@@ -6,10 +6,12 @@
|
||||
#include <QFontDatabase>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkProxy>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlNetworkAccessManagerFactory>
|
||||
#include <QQuickStyle>
|
||||
#include <QQuickWindow>
|
||||
|
||||
@@ -44,6 +46,7 @@
|
||||
#include "chatboxhelper.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "clipboard.h"
|
||||
#include "collapsestateproxymodel.h"
|
||||
#include "commandmodel.h"
|
||||
#include "controller.h"
|
||||
#include "csapi/joining.h"
|
||||
@@ -60,12 +63,12 @@
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "networkaccessmanager.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "publicroomlistmodel.h"
|
||||
#include "roomlistmodel.h"
|
||||
#include "roommanager.h"
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
#include "spellcheckhighlighter.h"
|
||||
#include "userdirectorylistmodel.h"
|
||||
#include "userlistmodel.h"
|
||||
#include "webshortcutmodel.h"
|
||||
@@ -74,8 +77,21 @@
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
||||
{
|
||||
QNetworkAccessManager *create(QObject *) override
|
||||
{
|
||||
return NetworkAccessManager::instance();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef HAVE_WINDOWSYSTEM
|
||||
static void raiseWindow(QWindow *window)
|
||||
{
|
||||
@@ -131,12 +147,11 @@ int main(int argc, char *argv[])
|
||||
QStringLiteral(NEOCHAT_VERSION_STRING),
|
||||
i18n("Matrix client"),
|
||||
KAboutLicense::GPL_V3,
|
||||
i18n("© 2018-2020 Black Hat, 2020-2021 KDE Community"));
|
||||
i18n("© 2018-2020 Black Hat, 2020-2022 KDE Community"));
|
||||
about.addAuthor(i18n("Black Hat"), QString(), QStringLiteral("bhat@encom.eu.org"));
|
||||
about.addAuthor(i18n("Carl Schwan"), QString(), QStringLiteral("carl@carlschwan.eu"));
|
||||
about.addAuthor(i18n("Tobias Fella"), QString(), QStringLiteral("fella@posteo.de"));
|
||||
about.setOrganizationDomain("kde.org");
|
||||
about.setBugAddress("https://invent.kde.org/network/neochat/issues");
|
||||
|
||||
about.addComponent(QStringLiteral("libQuotient"),
|
||||
i18n("A Qt5 library to write cross-platform clients for Matrix"),
|
||||
@@ -181,12 +196,12 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||
qmlRegisterType<SpellcheckHighlighter>("org.kde.neochat", 1, 0, "SpellcheckHighlighter");
|
||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
||||
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
|
||||
@@ -219,6 +234,7 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||
KLocalizedString::setApplicationDomain("neochat");
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
|
||||
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
||||
@@ -243,6 +259,11 @@ int main(int argc, char *argv[])
|
||||
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
|
||||
}
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
Runner runner;
|
||||
QDBusConnection::sessionBus().registerObject("/RoomRunner", &runner, QDBusConnection::ExportScriptableContents);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
KDBusService service(KDBusService::Unique);
|
||||
service.connect(&service,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include <connection.h>
|
||||
#include <csapi/rooms.h>
|
||||
#include <events/reactionevent.h>
|
||||
#include <events/redactionevent.h>
|
||||
#include <events/roomavatarevent.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "stickerevent.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlEngine> // for qmlRegisterType()
|
||||
#include <QTimeZone>
|
||||
|
||||
@@ -40,7 +42,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[FileMimetypeIcon] = "fileMimetypeIcon";
|
||||
roles[AnnotationRole] = "annotation";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[IsReplyRole] = "isReply";
|
||||
roles[ReplyRole] = "reply";
|
||||
roles[ReplyIdRole] = "replyId";
|
||||
roles[UserMarkerRole] = "userMarker";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
@@ -49,6 +53,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[SourceRole] = "source";
|
||||
roles[MimeTypeRole] = "mimeType";
|
||||
roles[FormattedBodyRole] = "formattedBody";
|
||||
roles[AuthorIdRole] = "authorId";
|
||||
roles[MediaUrlRole] = "mediaUrl";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -60,20 +66,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
|
||||
qRegisterMetaType<FileTransferInfo>();
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (!m_currentRoom) {
|
||||
return;
|
||||
}
|
||||
m_currentRoom->getPreviousContent(50);
|
||||
connect(this, &QAbstractListModel::rowsInserted, this, [this]() {
|
||||
if (m_currentRoom->readMarkerEventId().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
|
||||
if (it == m_currentRoom->historyEdge()) {
|
||||
m_currentRoom->getPreviousContent(50);
|
||||
}
|
||||
});
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyRole});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +88,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (room) {
|
||||
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
|
||||
room->setDisplayed();
|
||||
if (m_currentRoom->timelineSize() < 10) {
|
||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||
room->getPreviousContent(50);
|
||||
}
|
||||
lastReadEventId = room->readMarkerEventId();
|
||||
@@ -638,19 +632,35 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return variantList;
|
||||
}
|
||||
|
||||
if (role == IsReplyRole) {
|
||||
return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
|
||||
}
|
||||
|
||||
if (role == ReplyIdRole) {
|
||||
return evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
|
||||
}
|
||||
|
||||
if (role == ReplyRole) {
|
||||
const QString &replyEventId = evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
|
||||
if (replyEventId.isEmpty()) {
|
||||
return {};
|
||||
};
|
||||
const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
|
||||
if (replyIt == m_currentRoom->historyEdge()) {
|
||||
const RoomEvent *replyPtr = replyIt != m_currentRoom->historyEdge() ? &**replyIt : nullptr;
|
||||
if (!replyPtr) {
|
||||
for (const auto &e : m_extraEvents) {
|
||||
if (e->id() == replyEventId) {
|
||||
replyPtr = e.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
};
|
||||
const auto &replyEvt = **replyIt;
|
||||
}
|
||||
|
||||
QString type;
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&replyEvt)) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
type = "emote";
|
||||
@@ -675,29 +685,29 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
type = "message";
|
||||
}
|
||||
|
||||
} else if (is<const StickerEvent>(replyEvt)) {
|
||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
||||
type = "sticker";
|
||||
} else {
|
||||
type = "other";
|
||||
}
|
||||
|
||||
QVariant content;
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&replyEvt)) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
||||
// Cannot use e.contentJson() here because some
|
||||
// EventContent classes inject values into the copy of the
|
||||
// content JSON stored in EventContent::Base
|
||||
content = e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
|
||||
};
|
||||
|
||||
if (auto e = eventCast<const StickerEvent>(&replyEvt)) {
|
||||
if (auto e = eventCast<const StickerEvent>(replyPtr)) {
|
||||
content = QVariant::fromValue(e->image().originalJson);
|
||||
}
|
||||
|
||||
return QVariantMap{{"eventId", replyEventId},
|
||||
{"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)},
|
||||
{"display", m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
|
||||
{"content", content},
|
||||
{"type", type},
|
||||
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
|
||||
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyPtr->senderId())), m_currentRoom, evt)}};
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
@@ -759,6 +769,26 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
return res;
|
||||
}
|
||||
if (role == AuthorIdRole) {
|
||||
return evt.senderId();
|
||||
}
|
||||
|
||||
if (role == MediaUrlRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (!e->hasFileContent()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (e->content()->originalJson.contains(QStringLiteral("file")) && e->content()->originalJson["file"].toObject().contains(QStringLiteral("url"))) {
|
||||
return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["file"]["url"].toString());
|
||||
}
|
||||
if (e->content()->originalJson.contains(QStringLiteral("url"))) {
|
||||
return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["url"].toString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return m_currentRoom->urlToDownload(evt.id());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -785,22 +815,29 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
|
||||
for (auto it = timelineBottom; it != limit; ++it) {
|
||||
auto evt = it->event();
|
||||
auto e = eventCast<const RoomMessageEvent>(evt);
|
||||
if (!e) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// check if the current message's sender's id is same as the user's id
|
||||
if ((*it)->senderId() == m_currentRoom->localUser()->id()) {
|
||||
auto content = (*it)->contentJson();
|
||||
|
||||
if (content.contains("m.relates_to")) {
|
||||
// the message has been edited once
|
||||
// so we have to return the id of the related' message instead
|
||||
targetMessage.insert("event_id", content["m.relates_to"].toObject()["event_id"].toString());
|
||||
targetMessage.insert("body", content["formatted_body"].toString());
|
||||
return targetMessage;
|
||||
}
|
||||
if (e->msgtype() != MessageEventType::Unknown) {
|
||||
QString eventId;
|
||||
if (content.contains("m.new_content")) {
|
||||
// The message has been edited so we have to return the id of the original message instead of the replacement
|
||||
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
||||
} else {
|
||||
// For any message that isn't an edit return the id of the current message
|
||||
eventId = (*it)->id();
|
||||
}
|
||||
targetMessage.insert("event_id", eventId);
|
||||
targetMessage.insert("formattedBody", content["formatted_body"].toString());
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
||||
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
|
||||
|
||||
if (e->msgtype() == MessageEventType::Text) {
|
||||
targetMessage.insert("event_id", (*it)->id());
|
||||
targetMessage.insert("body", content["body"].toString());
|
||||
return targetMessage;
|
||||
}
|
||||
}
|
||||
@@ -823,23 +860,19 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
|
||||
|
||||
auto content = (*it)->contentJson();
|
||||
|
||||
if (content.contains("m.relates_to")) {
|
||||
auto relatedContent = content["m.relates_to"].toObject();
|
||||
|
||||
if (!relatedContent.contains("m.in_reply_to")) {
|
||||
// the message has been edited once
|
||||
// so we have to return the id of the related' message instead
|
||||
replyResponse.insert("event_id", relatedContent["event_id"].toString());
|
||||
replyResponse.insert("event", content["m.formatted_body"].toString());
|
||||
replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId())));
|
||||
replyResponse.insert("at", -it->index());
|
||||
return replyResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (e->msgtype() != MessageEventType::Unknown) {
|
||||
replyResponse.insert("event_id", (*it)->id());
|
||||
replyResponse.insert("event", content["body"].toString());
|
||||
QString eventId;
|
||||
if (content.contains("m.new_content")) {
|
||||
// The message has been edited so we have to return the id of the original message instead of the replacement
|
||||
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
||||
} else {
|
||||
// For any message that isn't an edit return the id of the current message
|
||||
eventId = (*it)->id();
|
||||
}
|
||||
replyResponse.insert("event_id", eventId);
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
||||
replyResponse.insert("message", idx.data(Qt::UserRole + 2));
|
||||
replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId())));
|
||||
replyResponse.insert("at", -it->index());
|
||||
return replyResponse;
|
||||
@@ -847,3 +880,13 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
|
||||
}
|
||||
return replyResponse;
|
||||
}
|
||||
|
||||
void MessageEventModel::loadReply(const QModelIndex &index)
|
||||
{
|
||||
auto job = m_currentRoom->connection()->callApi<GetOneRoomEventJob>(m_currentRoom->id(), data(index, ReplyIdRole).toString());
|
||||
QPersistentModelIndex persistentIndex(index);
|
||||
connect(job, &BaseJob::success, this, [this, job, persistentIndex] {
|
||||
m_extraEvents.push_back(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData()));
|
||||
Q_EMIT dataChanged(persistentIndex, persistentIndex, {ReplyRole});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ public:
|
||||
MimeTypeRole,
|
||||
FileMimetypeIcon,
|
||||
|
||||
IsReplyRole,
|
||||
ReplyRole,
|
||||
ReplyIdRole,
|
||||
|
||||
ShowAuthorRole,
|
||||
ShowSectionRole,
|
||||
@@ -42,9 +44,11 @@ public:
|
||||
|
||||
IsEditedRole,
|
||||
SourceRole,
|
||||
|
||||
MediaUrlRole,
|
||||
// For debugging
|
||||
EventResolvedTypeRole,
|
||||
AuthorIdRole,
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
@@ -64,6 +68,7 @@ public:
|
||||
Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const;
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
|
||||
Q_INVOKABLE void loadReply(const QModelIndex &row);
|
||||
|
||||
private Q_SLOTS:
|
||||
int refreshEvent(const QString &eventId);
|
||||
@@ -88,6 +93,8 @@ private:
|
||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
|
||||
std::vector<event_ptr_tt<RoomEvent>> m_extraEvents;
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void fancyEffectsReasonFound(const QString &fancyEffect);
|
||||
|
||||
@@ -14,6 +14,7 @@ Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[id]=NeoChat
|
||||
Name[it]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
@@ -28,6 +29,7 @@ Name[sk]=NeoChat
|
||||
Name[sl]=NeoChat
|
||||
Name[sv]=NeoChat
|
||||
Name[ta]=நியோச்சாட்
|
||||
Name[tr]=NeoChat
|
||||
Name[uk]=NeoChat
|
||||
Name[x-test]=xxNeoChatxx
|
||||
Name[zh_CN]=NeoChat
|
||||
@@ -37,6 +39,7 @@ Comment[ar]=عميل لماتركس، ميفاق الاتصال اللامركز
|
||||
Comment[az]=Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu
|
||||
Comment[ca]=Un client per a Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[ca@valencia]=Un client per a Matrix, el protocol de comunicacions descentralitzat
|
||||
Comment[cs]=Klient pro decentralizovaný komunikační protokol matrix
|
||||
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
|
||||
Comment[en_GB]=A client for matrix, the decentralised communication protocol
|
||||
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
|
||||
@@ -45,6 +48,7 @@ Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
|
||||
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
|
||||
Comment[hu]=Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz
|
||||
Comment[ia]=Un cliente per Matrix, le protocollo de communication decentralisate
|
||||
Comment[id]=Sebuah klien untuk matrix, protokol komunikasi terdecentralisasi
|
||||
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
|
||||
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
|
||||
Comment[lt]=Matrix decentralizuoto bendravimo protokolo kliento programa
|
||||
@@ -58,6 +62,7 @@ Comment[ro]=Client pentru Matrix, protocolul de comunicare descentralizată
|
||||
Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol
|
||||
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
|
||||
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
|
||||
Comment[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci
|
||||
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
|
||||
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
|
||||
Comment[zh_CN]=分布式通讯协议 Matrix 的客户端
|
||||
@@ -78,6 +83,7 @@ Name[fi]=Uusi viesti
|
||||
Name[fr]=Nouveau message
|
||||
Name[hu]=Új üzenet
|
||||
Name[ia]=Nove message
|
||||
Name[id]=Pesan baru
|
||||
Name[it]=Nuovo messaggio
|
||||
Name[ko]=새 메시지
|
||||
Name[lt]=Nauja žinutė
|
||||
@@ -92,6 +98,7 @@ Name[sk]=Nová správa
|
||||
Name[sl]=Novo sporočilo
|
||||
Name[sv]=Nytt meddelande
|
||||
Name[ta]=புதிய செய்தி
|
||||
Name[tr]=Yeni ileti
|
||||
Name[uk]=Нове повідомлення
|
||||
Name[x-test]=xxNew messagexx
|
||||
Name[zh_CN]=新消息
|
||||
@@ -108,6 +115,7 @@ Comment[fi]=Saapui uusi viesti
|
||||
Comment[fr]=Il y a un nouveau message
|
||||
Comment[hu]=Új üzenet érkezett
|
||||
Comment[ia]=Il ha un nove message
|
||||
Comment[id]=Ada pesan baru
|
||||
Comment[it]=È presente un nuovo messaggio
|
||||
Comment[ko]=새 메시지가 있음
|
||||
Comment[lt]=Yra nauja žinutė
|
||||
@@ -122,6 +130,7 @@ Comment[sk]=Je nová správa
|
||||
Comment[sl]=Prišlo je novo sporočilo
|
||||
Comment[sv]=Det finns ett nytt meddelande
|
||||
Comment[ta]=ஒரு புதிய செய்தி உள்ளது
|
||||
Comment[tr]=Yeni bir ileti var
|
||||
Comment[uk]=Надійшло нове повідомлення
|
||||
Comment[x-test]=xxThere is a new messagexx
|
||||
Comment[zh_CN]=有新消息
|
||||
@@ -133,11 +142,14 @@ Name[ar]=دعوة جديدة
|
||||
Name[az]=Yeni dəvət
|
||||
Name[ca]=Invitació nova
|
||||
Name[ca@valencia]=Invitació nova
|
||||
Name[cs]=Nová pozvánka
|
||||
Name[de]=Neue Einladung
|
||||
Name[en_GB]=New Invitation
|
||||
Name[es]=Nueva invitación
|
||||
Name[fi]=Uusi kutsu
|
||||
Name[fr]=Nouvelle invitation
|
||||
Name[ia]=Nove invitation
|
||||
Name[id]=Undangan Baru
|
||||
Name[it]=Nuovo invito
|
||||
Name[ko]=새 초대장
|
||||
Name[nl]=Nieuwe uitnodiging
|
||||
@@ -147,6 +159,8 @@ Name[pt]=Novo Convite
|
||||
Name[pt_BR]=Novo convite
|
||||
Name[sl]=Novo povabilo
|
||||
Name[sv]=Ny inbjudan
|
||||
Name[ta]=புதிய அழைப்பிதழ்
|
||||
Name[tr]=Yeni Davet
|
||||
Name[uk]=Нове запрошення
|
||||
Name[x-test]=xxNew Invitationxx
|
||||
Comment=There is a new invitation to a room
|
||||
@@ -154,11 +168,14 @@ Comment[ar]=توجد دعوة جديدة
|
||||
Comment[az]=Otağa bir yeni dəvət var
|
||||
Comment[ca]=Hi ha una invitació nova a una sala
|
||||
Comment[ca@valencia]=Hi ha una invitació nova a una sala
|
||||
Comment[cs]=Máte novou pozvánku do místnosti
|
||||
Comment[de]=Es gibt eine neue Einladung zu einem Raum
|
||||
Comment[en_GB]=There is a new invitation to a room
|
||||
Comment[es]=Hay una nueva invitación a una sala
|
||||
Comment[fi]=Uusi kutsu huoneeseen
|
||||
Comment[fr]=Il y a une nouvelle invitation dans un salon.
|
||||
Comment[ia]=Il ha un nove invitation a un sala
|
||||
Comment[id]=Ada undangan baru ke sebuah ruangan
|
||||
Comment[it]=È presente un nuovo invito a una stanza
|
||||
Comment[ko]=새로운 대화방 초대장을 받음
|
||||
Comment[nl]=Er is een nieuwe uitnodiging naar een room
|
||||
@@ -168,6 +185,8 @@ Comment[pt]=Existe um novo convite para uma sala
|
||||
Comment[pt_BR]=Existe um novo convite para uma sala
|
||||
Comment[sl]=Tam je novo povabilo v sobo
|
||||
Comment[sv]=Det finns en ny inbjudan till ett rum
|
||||
Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது
|
||||
Comment[tr]=Bir odaya yeni bir davetiye var
|
||||
Comment[uk]=У кімнаті нове запрошення
|
||||
Comment[x-test]=xxThere is a new invitation to a roomxx
|
||||
Action=Popup
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
<entry name="RoomDrawerWidth" type="int">
|
||||
<default>-1</default>
|
||||
</entry>
|
||||
<entry name="TypingNotifications" type="bool">
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="AutoRoomInfoDrawer" type="bool">
|
||||
<label>Automatic Hide/Unhide Room Information</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Timeline">
|
||||
<entry name="ShowAvatarInTimeline" type="bool">
|
||||
|
||||
@@ -83,7 +83,11 @@ void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||
|
||||
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
|
||||
setHasFileUploading(true);
|
||||
#ifdef QUOTIENT_07
|
||||
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
|
||||
#else
|
||||
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
|
||||
#endif
|
||||
if (id == txnId) {
|
||||
setFileUploadingProgress(0);
|
||||
setHasFileUploading(false);
|
||||
@@ -252,6 +256,35 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
return messageEvents().rbegin()->get()->originTimestamp();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::subtitleText()
|
||||
{
|
||||
QString subtitle = this->lastEventToString().size() == 0 ? this->topic() : this->lastEventToString();
|
||||
|
||||
subtitle
|
||||
// replace blockquote, i.e. '> text'
|
||||
.replace(QRegularExpression("(\r\n\t|\n|\r\t|)> "), " ")
|
||||
// replace headings, i.e. "# text"
|
||||
.replace(QRegularExpression("(\r\n\t|\n|\r\t|)\\#{1,6} "), " ")
|
||||
// replace newlines
|
||||
.replace(QRegularExpression("(\r\n\t|\n|\r\t)"), " ")
|
||||
// replace '**text**' and '__text__'
|
||||
.replace(QRegularExpression("(\\*\\*|__)(?=\\S)([^\\r]*\\S)\\1"), "\\2")
|
||||
// replace '*text*' and '_text_'
|
||||
.replace(QRegularExpression("(\\*|_)(?=\\S)([^\\r]*\\S)\\1"), "\\2")
|
||||
// replace '~~text~~'
|
||||
.replace(QRegularExpression("~~(.*)~~"), "\\1")
|
||||
// replace '~text~'
|
||||
.replace(QRegularExpression("~(.*)~"), "\\1")
|
||||
// replace '<del>text</del>'
|
||||
.replace(QRegularExpression("<del>(.*)</del>"), "\\1")
|
||||
// replace '```code```'
|
||||
.replace(QRegularExpression("```([^```]+)```"), "\\1")
|
||||
// replace '`code`'
|
||||
.replace(QRegularExpression("`([^`]+)`"), "\\1");
|
||||
|
||||
return subtitle.size() > 0 ? subtitle : QStringLiteral(" ");
|
||||
}
|
||||
|
||||
int NeoChatRoom::savedTopVisibleIndex() const
|
||||
{
|
||||
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||
@@ -311,7 +344,11 @@ QVariantMap NeoChatRoom::getUser(const QString &userID) const
|
||||
|
||||
QUrl NeoChatRoom::urlToMxcUrl(const QUrl &mxcUrl)
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return connection()->makeMediaUrl(mxcUrl);
|
||||
#else
|
||||
return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl);
|
||||
#endif
|
||||
}
|
||||
|
||||
QString NeoChatRoom::avatarMediaId() const
|
||||
@@ -396,6 +433,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
[this](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == MembershipType::Leave) {
|
||||
auto displayName = e.prevContent()->displayName;
|
||||
#ifdef QUOTIENT_07
|
||||
if (displayName) {
|
||||
subjectName = sanitized(*displayName).toHtmlEscaped();
|
||||
#else
|
||||
if (displayName.isEmpty()) {
|
||||
subjectName = sanitized(displayName).toHtmlEscaped();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case MembershipType::Invite:
|
||||
@@ -493,6 +542,15 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"]["prev_content"].toObject().isEmpty()) {
|
||||
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"].toString());
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"]["prev_content"]["name"].toString());
|
||||
}
|
||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"].toString());
|
||||
}
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
},
|
||||
@@ -556,6 +614,7 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
|
||||
result.replace(QRegularExpression("(<br />)*$"), "");
|
||||
result.replace("<p>", "");
|
||||
result.replace("</p>", "");
|
||||
result.replace("\n", "<br>");
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -596,7 +655,6 @@ void NeoChatRoom::postMessage(const QString &rawText, const QString &text, Messa
|
||||
|
||||
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);
|
||||
@@ -642,7 +700,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>" + html
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
@@ -652,11 +710,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRichText) {
|
||||
Room::postHtmlMessage(text, html, type);
|
||||
} else {
|
||||
Room::postMessage(text, type);
|
||||
}
|
||||
Room::postHtmlMessage(text, html, type);
|
||||
}
|
||||
|
||||
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
|
||||
@@ -781,3 +835,8 @@ QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::clearInvitationNotification()
|
||||
{
|
||||
NotificationsManager::instance().clearInvitationNotification(id());
|
||||
}
|
||||
|
||||
@@ -66,6 +66,12 @@ public:
|
||||
/// \see lastEvent
|
||||
[[nodiscard]] QDateTime lastActiveTime();
|
||||
|
||||
/// Get subtitle text for room
|
||||
///
|
||||
/// Fetches last event and removes markdown formatting
|
||||
/// \see lastEventToString
|
||||
[[nodiscard]] QString subtitleText();
|
||||
|
||||
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
|
||||
|
||||
[[nodiscard]] QString joinRule() const;
|
||||
@@ -121,6 +127,7 @@ public:
|
||||
|
||||
Q_INVOKABLE QString htmlSafeName() const;
|
||||
Q_INVOKABLE QString htmlSafeDisplayName() const;
|
||||
Q_INVOKABLE void clearInvitationNotification();
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
|
||||
|
||||
@@ -32,11 +32,11 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
||||
}
|
||||
|
||||
void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
const QString &roomName,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId)
|
||||
const QString &replyEventId,
|
||||
bool canReply)
|
||||
{
|
||||
if (!NeoChatConfig::self()->showNotifications()) {
|
||||
return;
|
||||
@@ -46,10 +46,10 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
img.convertFromImage(icon);
|
||||
KNotification *notification = new KNotification("message");
|
||||
|
||||
if (sender == roomName) {
|
||||
if (sender == room->displayName()) {
|
||||
notification->setTitle(sender);
|
||||
} else {
|
||||
notification->setTitle(i18n("%1 (%2)", sender, roomName));
|
||||
notification->setTitle(i18n("%1 (%2)", sender, room->displayName()));
|
||||
}
|
||||
|
||||
notification->setText(text.toHtmlEscaped());
|
||||
@@ -57,19 +57,24 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
|
||||
notification->setDefaultAction(i18n("Open NeoChat in this room"));
|
||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||
#if defined(HAVE_WINDOWSYSTEM) && KNOTIFICATIONS_VERSION >= QT_VERSION_CHECK(5, 90, 0)
|
||||
KWindowSystem::setCurrentXdgActivationToken(notification->xdgActivationToken());
|
||||
#endif
|
||||
RoomManager::instance().enterRoom(room);
|
||||
Q_EMIT Controller::instance().showWindow();
|
||||
});
|
||||
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, room->preprocessText(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
});
|
||||
if (canReply) {
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, room->preprocessText(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
});
|
||||
notification->setReplyAction(std::move(replyAction));
|
||||
}
|
||||
|
||||
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
|
||||
|
||||
notification->setReplyAction(std::move(replyAction));
|
||||
|
||||
notification->sendEvent();
|
||||
|
||||
@@ -87,24 +92,38 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
|
||||
notification->setText(i18n("%1 invited you to a room", sender));
|
||||
notification->setTitle(title);
|
||||
notification->setPixmap(img);
|
||||
notification->setFlags(KNotification::Persistent);
|
||||
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||
connect(notification, &KNotification::defaultActivated, this, [=]() {
|
||||
#if defined(HAVE_WINDOWSYSTEM) && KNOTIFICATIONS_VERSION >= QT_VERSION_CHECK(5, 90, 0)
|
||||
KWindowSystem::setCurrentXdgActivationToken(notification->xdgActivationToken());
|
||||
#endif
|
||||
notification->close();
|
||||
RoomManager::instance().enterRoom(room);
|
||||
Q_EMIT Controller::instance().showWindow();
|
||||
});
|
||||
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
|
||||
connect(notification, &KNotification::action1Activated, this, [room]() {
|
||||
connect(notification, &KNotification::action1Activated, this, [this, room, notification]() {
|
||||
room->acceptInvitation();
|
||||
notification->close();
|
||||
});
|
||||
connect(notification, &KNotification::action2Activated, this, [room]() {
|
||||
connect(notification, &KNotification::action2Activated, this, [this, room, notification]() {
|
||||
RoomManager::instance().leaveRoom(room);
|
||||
notification->close();
|
||||
});
|
||||
connect(notification, &KNotification::closed, this, [this, room]() {
|
||||
m_invitations.remove(room->id());
|
||||
});
|
||||
|
||||
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
|
||||
|
||||
notification->sendEvent();
|
||||
m_notifications.insert(room->id(), notification);
|
||||
m_invitations.insert(room->id(), notification);
|
||||
}
|
||||
|
||||
void NotificationsManager::clearInvitationNotification(const QString &roomId)
|
||||
{
|
||||
if (m_invitations.contains(roomId)) {
|
||||
m_invitations[roomId]->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,14 @@ public:
|
||||
static NotificationsManager &instance();
|
||||
|
||||
Q_INVOKABLE void
|
||||
postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon, const QString &replyEventId);
|
||||
postNotification(NeoChatRoom *room, const QString &sender, const QString &text, const QImage &icon, const QString &replyEventId, bool canReply);
|
||||
void postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon);
|
||||
|
||||
void clearInvitationNotification(const QString &roomId);
|
||||
|
||||
private:
|
||||
NotificationsManager(QObject *parent = nullptr);
|
||||
|
||||
QMultiMap<QString, KNotification *> m_notifications;
|
||||
QHash<QString, QPointer<KNotification>> m_invitations;
|
||||
};
|
||||
|
||||
67
src/plasma-runner-neochat.desktop
Normal file
67
src/plasma-runner-neochat.desktop
Normal file
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
[Desktop Entry]
|
||||
Name=NeoChat
|
||||
Name[ar]=نيوتشات
|
||||
Name[az]=NeoChat
|
||||
Name[ca]=NeoChat
|
||||
Name[ca@valencia]=NeoChat
|
||||
Name[cs]=NeoChat
|
||||
Name[de]=NeoChat
|
||||
Name[en_GB]=NeoChat
|
||||
Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[id]=NeoChat
|
||||
Name[it]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
Name[nl]=NeoChat
|
||||
Name[nn]=NeoChat
|
||||
Name[pa]=ਨਿਓ-ਚੈਟ
|
||||
Name[pl]=NeoChat
|
||||
Name[pt]=NeoChat
|
||||
Name[pt_BR]=NeoChat
|
||||
Name[ro]=NeoChat
|
||||
Name[sk]=NeoChat
|
||||
Name[sl]=NeoChat
|
||||
Name[sv]=NeoChat
|
||||
Name[ta]=நியோச்சாட்
|
||||
Name[tr]=NeoChat
|
||||
Name[uk]=NeoChat
|
||||
Name[x-test]=xxNeoChatxx
|
||||
Name[zh_CN]=NeoChat
|
||||
Comment=Find rooms in NeoChat
|
||||
Comment[ar]=اعثر على غرف في نيوتشات
|
||||
Comment[az]=NeoChat-da otaqları tapın
|
||||
Comment[ca]=Cerca sales en el NeoChat
|
||||
Comment[ca@valencia]=Busca sales en NeoChat
|
||||
Comment[en_GB]=Find rooms in NeoChat
|
||||
Comment[es]=Buscar salas en NeoChat
|
||||
Comment[fi]=Etsi huoneita NeoChatissä
|
||||
Comment[fr]=Trouver des salons dans NeoChat
|
||||
Comment[ia]=Trova salas in NeoChat
|
||||
Comment[id]=Cari ruangan di NeoChat
|
||||
Comment[it]=Trova stanze in NeoChat
|
||||
Comment[ko]=NeoChat에서 대화방 찾기
|
||||
Comment[nl]=Rooms zoeken in NeoChat
|
||||
Comment[pl]=Znajdź pokoje w NeoChat
|
||||
Comment[pt]=Procurar salas no NeoChat
|
||||
Comment[pt_BR]=Encontrar salas no NeoChat
|
||||
Comment[sl]=Najdi sobe v NeoChatu
|
||||
Comment[sv]=Sök efter rum i NeoChat
|
||||
Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும்
|
||||
Comment[tr]=NeoChat'te odalar bulun
|
||||
Comment[uk]=Пошук кімнат у NeoChat
|
||||
Comment[x-test]=xxFind rooms in NeoChatxx
|
||||
X-KDE-ServiceTypes=Plasma/Runner
|
||||
Type=Service
|
||||
Icon=org.kde.neochat
|
||||
X-Plasma-API=DBus
|
||||
X-Plasma-DBusRunner-Service=org.kde.neochat
|
||||
X-Plasma-DBusRunner-Path=/RoomRunner
|
||||
X-Plasma-Request-Actions-Once=true
|
||||
X-Plasma-Runner-Min-Letter-Count=3
|
||||
@@ -157,10 +157,6 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
return displayName;
|
||||
}
|
||||
|
||||
if (!room.aliases.isEmpty()) {
|
||||
displayName = room.aliases.front();
|
||||
}
|
||||
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QGuiApplication>
|
||||
#include <utility>
|
||||
|
||||
#include "csapi/notifications.h"
|
||||
@@ -171,7 +172,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refresh(room, {LastEventRole});
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
||||
connect(room, &Room::highlightCountChanged, this, [this, room] {
|
||||
@@ -225,7 +226,9 @@ void RoomListModel::handleNotifications()
|
||||
}
|
||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
||||
auto room = m_connection->room(notification["room_id"].toString());
|
||||
if (room) {
|
||||
|
||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||
|
||||
@@ -236,11 +239,11 @@ void RoomListModel::handleNotifications()
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
room->displayName(),
|
||||
sender->displayname(room),
|
||||
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
||||
avatar_image,
|
||||
notification["event"].toObject()["event_id"].toString());
|
||||
notification["event"].toObject()["event_id"].toString(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -329,7 +332,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
NeoChatRoom *room = m_rooms.at(index.row());
|
||||
if (role == NameRole) {
|
||||
return !room->name().isEmpty() ? room->htmlSafeName() : room->htmlSafeDisplayName();
|
||||
return !room->name().isEmpty() ? room->name() : room->displayName();
|
||||
}
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
@@ -396,6 +399,15 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == CategoryVisibleRole) {
|
||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
return room->subtitleText();
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
}
|
||||
if (role == IdRole) {
|
||||
return room->id();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
@@ -426,6 +438,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
roles[JoinStateRole] = "joinState";
|
||||
roles[CurrentRoomRole] = "currentRoom";
|
||||
roles[CategoryVisibleRole] = "categoryVisible";
|
||||
roles[SubtitleTextRole] = "subtitleText";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ public:
|
||||
JoinStateRole,
|
||||
CurrentRoomRole,
|
||||
CategoryVisibleRole,
|
||||
SubtitleTextRole,
|
||||
AvatarImageRole,
|
||||
IdRole,
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
|
||||
94
src/runner.cpp
Normal file
94
src/runner.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QDBusMetaType>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roomlistmodel.h"
|
||||
#include "roommanager.h"
|
||||
#include "runner.h"
|
||||
|
||||
RemoteImage Runner::serializeImage(const QImage &image)
|
||||
{
|
||||
QImage convertedImage = image.convertToFormat(QImage::Format_RGBA8888);
|
||||
RemoteImage remoteImage{
|
||||
convertedImage.width(),
|
||||
convertedImage.height(),
|
||||
convertedImage.bytesPerLine(),
|
||||
true, // hasAlpha
|
||||
8, // bitsPerSample
|
||||
4, // channels
|
||||
QByteArray(reinterpret_cast<const char *>(convertedImage.constBits()), convertedImage.sizeInBytes()),
|
||||
};
|
||||
return remoteImage;
|
||||
}
|
||||
|
||||
Runner::Runner()
|
||||
: QObject()
|
||||
{
|
||||
qDBusRegisterMetaType<RemoteMatch>();
|
||||
qDBusRegisterMetaType<RemoteMatches>();
|
||||
qDBusRegisterMetaType<RemoteAction>();
|
||||
qDBusRegisterMetaType<RemoteActions>();
|
||||
qDBusRegisterMetaType<RemoteImage>();
|
||||
|
||||
m_model.setSourceModel(&m_sourceModel);
|
||||
|
||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &Runner::activeConnectionChanged);
|
||||
}
|
||||
|
||||
void Runner::activeConnectionChanged()
|
||||
{
|
||||
m_sourceModel.setConnection(Controller::instance().activeConnection());
|
||||
}
|
||||
|
||||
RemoteActions Runner::Actions()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
RemoteMatches Runner::Match(const QString &searchTerm)
|
||||
{
|
||||
m_model.setFilterText(searchTerm);
|
||||
|
||||
RemoteMatches matches;
|
||||
|
||||
for (int i = 0; i < m_model.rowCount(); ++i) {
|
||||
RemoteMatch match;
|
||||
|
||||
const QString name = m_model.data(m_model.index(i, 0), RoomListModel::DisplayNameRole).toString();
|
||||
|
||||
match.iconName = QStringLiteral("org.kde.neochat");
|
||||
match.id = m_model.data(m_model.index(i, 0), RoomListModel::IdRole).toString();
|
||||
match.text = name;
|
||||
match.relevance = 1;
|
||||
const RemoteImage remoteImage = serializeImage(m_model.data(m_model.index(i, 0), RoomListModel::AvatarImageRole).value<QImage>());
|
||||
match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage));
|
||||
match.properties.insert(QStringLiteral("subtext"), m_model.data(m_model.index(i, 0), RoomListModel::TopicRole).toString());
|
||||
|
||||
if (name.compare(searchTerm, Qt::CaseInsensitive) == 0) {
|
||||
match.type = ExactMatch;
|
||||
} else {
|
||||
match.type = CompletionMatch;
|
||||
}
|
||||
|
||||
matches << match;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
void Runner::Run(const QString &id, const QString &actionId)
|
||||
{
|
||||
Q_UNUSED(actionId);
|
||||
|
||||
NeoChatRoom *room = qobject_cast<NeoChatRoom *>(Controller::instance().activeConnection()->room(id));
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
RoomManager::instance().enterRoom(room);
|
||||
Q_EMIT Controller::instance().showWindow();
|
||||
}
|
||||
169
src/runner.h
Normal file
169
src/runner.h
Normal file
@@ -0,0 +1,169 @@
|
||||
// SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDBusContext>
|
||||
#include <QObject>
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "roomlistmodel.h"
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
|
||||
// Copied from KRunner/QueryMatch
|
||||
enum MatchType {
|
||||
NoMatch = 0, /**< Null match */
|
||||
CompletionMatch = 10, /**< Possible completion for the data of the query */
|
||||
PossibleMatch = 30, /**< Something that may match the query */
|
||||
InformationalMatch = 50, /**< A purely informational, non-runnable match,
|
||||
such as the answer to a question or calculation.
|
||||
The data of the match will be converted to a string
|
||||
and set in the search field */
|
||||
HelperMatch = 70, /**< A match that represents an action not directly related
|
||||
to activating the given search term, such as a search
|
||||
in an external tool or a command learning trigger. Helper
|
||||
matches tend to be generic to the query and should not
|
||||
be autoactivated just because the user hits "Enter"
|
||||
while typing. They must be explicitly selected to
|
||||
be activated, but unlike InformationalMatch cause
|
||||
an action to be triggered. */
|
||||
ExactMatch = 100, /**< An exact match to the query */
|
||||
};
|
||||
|
||||
struct RemoteMatch {
|
||||
// sssuda{sv}
|
||||
QString id;
|
||||
QString text;
|
||||
QString iconName;
|
||||
MatchType type = MatchType::NoMatch;
|
||||
qreal relevance = 0;
|
||||
QVariantMap properties;
|
||||
};
|
||||
|
||||
typedef QList<RemoteMatch> RemoteMatches;
|
||||
|
||||
struct RemoteAction {
|
||||
QString id;
|
||||
QString text;
|
||||
QString iconName;
|
||||
};
|
||||
|
||||
typedef QList<RemoteAction> RemoteActions;
|
||||
|
||||
struct RemoteImage {
|
||||
// iiibiiay (matching notification spec image-data attribute)
|
||||
int width;
|
||||
int height;
|
||||
int rowStride;
|
||||
bool hasAlpha;
|
||||
int bitsPerSample;
|
||||
int channels;
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteMatch &match)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << match.id;
|
||||
argument << match.text;
|
||||
argument << match.iconName;
|
||||
argument << match.type;
|
||||
argument << match.relevance;
|
||||
argument << match.properties;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteMatch &match)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> match.id;
|
||||
argument >> match.text;
|
||||
argument >> match.iconName;
|
||||
uint type;
|
||||
argument >> type;
|
||||
match.type = static_cast<MatchType>(type);
|
||||
argument >> match.relevance;
|
||||
argument >> match.properties;
|
||||
argument.endStructure();
|
||||
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteAction &action)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << action.id;
|
||||
argument << action.text;
|
||||
argument << action.iconName;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteAction &action)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> action.id;
|
||||
argument >> action.text;
|
||||
argument >> action.iconName;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteImage &image)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << image.width;
|
||||
argument << image.height;
|
||||
argument << image.rowStride;
|
||||
argument << image.hasAlpha;
|
||||
argument << image.bitsPerSample;
|
||||
argument << image.channels;
|
||||
argument << image.data;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteImage &image)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> image.width;
|
||||
argument >> image.height;
|
||||
argument >> image.rowStride;
|
||||
argument >> image.hasAlpha;
|
||||
argument >> image.bitsPerSample;
|
||||
argument >> image.channels;
|
||||
argument >> image.data;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(RemoteMatch)
|
||||
Q_DECLARE_METATYPE(RemoteMatches)
|
||||
Q_DECLARE_METATYPE(RemoteAction)
|
||||
Q_DECLARE_METATYPE(RemoteActions)
|
||||
Q_DECLARE_METATYPE(RemoteImage)
|
||||
|
||||
class Runner : public QObject, protected QDBusContext
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.kde.krunner1")
|
||||
public:
|
||||
Runner();
|
||||
|
||||
Q_SCRIPTABLE RemoteActions Actions();
|
||||
Q_SCRIPTABLE RemoteMatches Match(const QString &searchTerm);
|
||||
Q_SCRIPTABLE void Run(const QString &id, const QString &actionId);
|
||||
|
||||
private:
|
||||
RemoteImage serializeImage(const QImage &image);
|
||||
void activeConnectionChanged();
|
||||
|
||||
SortFilterRoomListModel m_model;
|
||||
RoomListModel m_sourceModel;
|
||||
};
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
|
||||
// SPDX-FileCopyrightText: 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "spellcheckhighlighter.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QTextBoundaryFinder>
|
||||
|
||||
// Cache of previously-determined languages (when using AutoDetectLanguage)
|
||||
// There is one such cache per block (paragraph)
|
||||
class LanguageCache : public QTextBlockUserData
|
||||
{
|
||||
public:
|
||||
// Key: QPair<start, length>
|
||||
// Value: language name
|
||||
QMap<QPair<int, int>, QString> languages;
|
||||
|
||||
// Remove all cached language information after @p pos
|
||||
void invalidate(int pos)
|
||||
{
|
||||
QMutableMapIterator<QPair<int, int>, QString> it(languages);
|
||||
it.toBack();
|
||||
while (it.hasPrevious()) {
|
||||
it.previous();
|
||||
if (it.key().first + it.key().second >= pos) {
|
||||
it.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString languageAtPos(int pos) const
|
||||
{
|
||||
// The data structure isn't really great for such lookups...
|
||||
QMapIterator<QPair<int, int>, QString> it(languages);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
|
||||
return it.value();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
};
|
||||
|
||||
QVector<QStringRef> split(QTextBoundaryFinder::BoundaryType boundary, const QString &text, int reasonMask = 0)
|
||||
{
|
||||
QVector<QStringRef> parts;
|
||||
QTextBoundaryFinder boundaryFinder(boundary, text);
|
||||
|
||||
while (boundaryFinder.position() < text.length()) {
|
||||
const int start = boundaryFinder.position();
|
||||
|
||||
// Advance until we find a break that matches the mask or are at the end
|
||||
for (;;) {
|
||||
if (boundaryFinder.toNextBoundary() == -1) {
|
||||
boundaryFinder.toEnd();
|
||||
break;
|
||||
}
|
||||
if (!reasonMask || boundaryFinder.boundaryReasons() & reasonMask) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto length = boundaryFinder.position() - start;
|
||||
|
||||
if (length < 1) {
|
||||
continue;
|
||||
}
|
||||
parts << QStringRef{&text, start, length};
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
SpellcheckHighlighter::SpellcheckHighlighter(QObject *parent)
|
||||
: QSyntaxHighlighter(parent)
|
||||
#ifndef Q_OS_ANDROID
|
||||
, mSpellchecker{new Sonnet::Speller()}
|
||||
, mLanguageGuesser
|
||||
{
|
||||
new Sonnet::GuessLanguage()
|
||||
}
|
||||
#endif
|
||||
, m_document(nullptr), m_cursorPosition(-1), m_selectionStart(-1), m_selectionEnd(-1)
|
||||
{
|
||||
// Danger red from our color scheme
|
||||
mErrorFormat.setForeground(QColor(0xED, 0x15, 0x15));
|
||||
mErrorFormat.setUnderlineColor(QColor(0xED, 0x15, 0x15));
|
||||
mErrorFormat.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
||||
mQuoteFormat.setForeground(QColor(0x7F, 0x8C, 0x8D));
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (!mSpellchecker->isValid()) {
|
||||
qWarning() << "Spellchecker is invalid";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::autodetectLanguage(const QString &sentence)
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
const auto lang = mLanguageGuesser->identify(sentence, mSpellchecker->availableLanguages());
|
||||
if (lang.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
mSpellchecker->setLanguage(lang);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool isSpellcheckable(const QStringRef &token)
|
||||
{
|
||||
if (token.isNull() || token.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!token.at(0).isLetter() || token.at(0).isUpper() || token.startsWith(QStringLiteral("http"))) {
|
||||
return false;
|
||||
}
|
||||
// part of a slash command
|
||||
if (token.contains("rainbowme") || token.contains("lenny") || token.contains("tableflip") || token.contains("unflip")) {
|
||||
return false;
|
||||
}
|
||||
// TODO ignore urls and uppercase?
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
// Avoid spellchecking quotes
|
||||
if (text.isEmpty() || text.at(0) == QLatin1Char('>')) {
|
||||
setFormat(0, text.length(), mQuoteFormat);
|
||||
return;
|
||||
}
|
||||
// Don't spell check certain commands
|
||||
if (text.startsWith("/join") || text.startsWith("/part") || text.startsWith("/invite")) {
|
||||
setFormat(0, text.length(), QTextCharFormat{});
|
||||
return;
|
||||
}
|
||||
#ifndef Q_OS_ANDROID
|
||||
for (const auto &sentenceRef : split(QTextBoundaryFinder::Sentence, text)) {
|
||||
// Avoid spellchecking quotes
|
||||
if (sentenceRef.isEmpty() || sentenceRef.at(0) == QLatin1Char('>')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto sentence = QString::fromRawData(sentenceRef.data(), sentenceRef.length());
|
||||
|
||||
autodetectLanguage(sentence);
|
||||
|
||||
const int offset = sentenceRef.position();
|
||||
for (const auto &wordRef : split(QTextBoundaryFinder::Word, sentence)) {
|
||||
// Avoid spellchecking words in progress
|
||||
// FIXME this will also prevent spellchecking a single word on a line.
|
||||
if (offset + wordRef.position() + wordRef.length() >= text.length()) {
|
||||
continue;
|
||||
}
|
||||
if (isSpellcheckable(wordRef)) {
|
||||
const auto word = QString::fromRawData(wordRef.data(), wordRef.length());
|
||||
const auto format = mSpellchecker->isMisspelled(word) ? mErrorFormat : QTextCharFormat{};
|
||||
setFormat(offset + wordRef.position(), wordRef.length(), format);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList SpellcheckHighlighter::suggestions(int mousePosition, int max)
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
QTextCursor cursorAtMouse(textDocument());
|
||||
cursorAtMouse.setPosition(mousePosition);
|
||||
|
||||
// Check if the user clicked a selected word
|
||||
/* clang-format off */
|
||||
const bool selectedWordClicked = cursor.hasSelection()
|
||||
&& mousePosition >= cursor.selectionStart()
|
||||
&& mousePosition <= cursor.selectionEnd();
|
||||
/* clang-format on */
|
||||
|
||||
// Get the word under the (mouse-)cursor and see if it is misspelled.
|
||||
// Don't include apostrophes at the start/end of the word in the selection.
|
||||
QTextCursor wordSelectCursor(cursorAtMouse);
|
||||
wordSelectCursor.clearSelection();
|
||||
wordSelectCursor.select(QTextCursor::WordUnderCursor);
|
||||
m_selectedWord = wordSelectCursor.selectedText();
|
||||
|
||||
// Clear the selection again, we re-select it below (without the apostrophes).
|
||||
wordSelectCursor.setPosition(wordSelectCursor.position() - m_selectedWord.size());
|
||||
if (m_selectedWord.startsWith(QLatin1Char('\'')) || m_selectedWord.startsWith(QLatin1Char('\"'))) {
|
||||
m_selectedWord = m_selectedWord.right(m_selectedWord.size() - 1);
|
||||
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
|
||||
}
|
||||
if (m_selectedWord.endsWith(QLatin1Char('\'')) || m_selectedWord.endsWith(QLatin1Char('\"'))) {
|
||||
m_selectedWord.chop(1);
|
||||
}
|
||||
|
||||
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selectedWord.size());
|
||||
|
||||
int endSelection = wordSelectCursor.selectionEnd();
|
||||
Q_EMIT wordUnderMouseChanged();
|
||||
|
||||
bool isMouseCursorInsideWord = true;
|
||||
if ((mousePosition < wordSelectCursor.selectionStart() || mousePosition >= wordSelectCursor.selectionEnd()) //
|
||||
&& (m_selectedWord.length() > 1)) {
|
||||
isMouseCursorInsideWord = false;
|
||||
}
|
||||
|
||||
wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selectedWord.size());
|
||||
|
||||
m_wordIsMisspelled = isMouseCursorInsideWord && !m_selectedWord.isEmpty() && mSpellchecker->isMisspelled(m_selectedWord);
|
||||
Q_EMIT wordIsMisspelledChanged();
|
||||
|
||||
if (!m_wordIsMisspelled || selectedWordClicked) {
|
||||
return QStringList{};
|
||||
}
|
||||
|
||||
if (!selectedWordClicked) {
|
||||
Q_EMIT changeCursorPosition(wordSelectCursor.selectionStart(), endSelection);
|
||||
}
|
||||
|
||||
LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
|
||||
if (cache) {
|
||||
const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
|
||||
if (!cachedLanguage.isEmpty()) {
|
||||
mSpellchecker->setLanguage(cachedLanguage);
|
||||
}
|
||||
}
|
||||
QStringList suggestions = mSpellchecker->suggest(m_selectedWord);
|
||||
if (max >= 0 && suggestions.count() > max) {
|
||||
suggestions = suggestions.mid(0, max);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
#else
|
||||
return QStringList();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::addWordToDictionary(const QString &word)
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
mSpellchecker->addToPersonal(word);
|
||||
rehighlight();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::ignoreWord(const QString &word)
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
mSpellchecker->addToSession(word);
|
||||
rehighlight();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::replaceWord(const QString &replacement)
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
textCursor().insertText(replacement);
|
||||
#endif
|
||||
}
|
||||
|
||||
QQuickTextDocument *SpellcheckHighlighter::quickDocument() const
|
||||
{
|
||||
return m_document;
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::setQuickDocument(QQuickTextDocument *document)
|
||||
{
|
||||
if (document == m_document) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_document) {
|
||||
m_document->textDocument()->disconnect(this);
|
||||
}
|
||||
m_document = document;
|
||||
setDocument(document->textDocument());
|
||||
Q_EMIT documentChanged();
|
||||
}
|
||||
|
||||
int SpellcheckHighlighter::cursorPosition() const
|
||||
{
|
||||
return m_cursorPosition;
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::setCursorPosition(int position)
|
||||
{
|
||||
if (position == m_cursorPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_cursorPosition = position;
|
||||
Q_EMIT cursorPositionChanged();
|
||||
}
|
||||
|
||||
int SpellcheckHighlighter::selectionStart() const
|
||||
{
|
||||
return m_selectionStart;
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::setSelectionStart(int position)
|
||||
{
|
||||
if (position == m_selectionStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionStart = position;
|
||||
Q_EMIT selectionStartChanged();
|
||||
}
|
||||
|
||||
int SpellcheckHighlighter::selectionEnd() const
|
||||
{
|
||||
return m_selectionEnd;
|
||||
}
|
||||
|
||||
void SpellcheckHighlighter::setSelectionEnd(int position)
|
||||
{
|
||||
if (position == m_selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionEnd = position;
|
||||
Q_EMIT selectionEndChanged();
|
||||
}
|
||||
|
||||
QTextCursor SpellcheckHighlighter::textCursor() const
|
||||
{
|
||||
QTextDocument *doc = textDocument();
|
||||
if (!doc) {
|
||||
return QTextCursor();
|
||||
}
|
||||
|
||||
QTextCursor cursor(doc);
|
||||
if (m_selectionStart != m_selectionEnd) {
|
||||
cursor.setPosition(m_selectionStart);
|
||||
cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor);
|
||||
} else {
|
||||
cursor.setPosition(m_cursorPosition);
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
QTextDocument *SpellcheckHighlighter::textDocument() const
|
||||
{
|
||||
if (!m_document) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_document->textDocument();
|
||||
}
|
||||
|
||||
bool SpellcheckHighlighter::wordIsMisspelled() const
|
||||
{
|
||||
return m_wordIsMisspelled;
|
||||
}
|
||||
|
||||
QString SpellcheckHighlighter::wordUnderMouse() const
|
||||
{
|
||||
return m_selectedWord;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
|
||||
// SPDX-FileCopyrightText: 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickTextDocument>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextDocument>
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <Sonnet/GuessLanguage>
|
||||
#include <Sonnet/Speller>
|
||||
#endif
|
||||
|
||||
class SpellcheckHighlighter : public QSyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QQuickTextDocument *document READ quickDocument WRITE setQuickDocument NOTIFY documentChanged)
|
||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
||||
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
|
||||
Q_PROPERTY(bool wordIsMisspelled READ wordIsMisspelled NOTIFY wordIsMisspelledChanged)
|
||||
Q_PROPERTY(QString wordUnderMouse READ wordUnderMouse NOTIFY wordUnderMouseChanged)
|
||||
|
||||
public:
|
||||
SpellcheckHighlighter(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QStringList suggestions(int position, int max = 5);
|
||||
Q_INVOKABLE void ignoreWord(const QString &word);
|
||||
Q_INVOKABLE void addWordToDictionary(const QString &word);
|
||||
Q_INVOKABLE void replaceWord(const QString &word);
|
||||
|
||||
[[nodiscard]] QQuickTextDocument *quickDocument() const;
|
||||
void setQuickDocument(QQuickTextDocument *document);
|
||||
|
||||
[[nodiscard]] int cursorPosition() const;
|
||||
void setCursorPosition(int position);
|
||||
|
||||
[[nodiscard]] int selectionStart() const;
|
||||
void setSelectionStart(int position);
|
||||
|
||||
[[nodiscard]] int selectionEnd() const;
|
||||
void setSelectionEnd(int position);
|
||||
|
||||
[[nodiscard]] bool wordIsMisspelled() const;
|
||||
[[nodiscard]] QString wordUnderMouse() const;
|
||||
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void documentChanged();
|
||||
void cursorPositionChanged();
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void wordIsMisspelledChanged();
|
||||
void wordUnderMouseChanged();
|
||||
void changeCursorPosition(int start, int end);
|
||||
|
||||
private:
|
||||
[[nodiscard]] QTextCursor textCursor() const;
|
||||
[[nodiscard]] QTextDocument *textDocument() const;
|
||||
|
||||
void autodetectLanguage(const QString &sentence);
|
||||
QTextCharFormat mErrorFormat;
|
||||
QTextCharFormat mQuoteFormat;
|
||||
#ifndef Q_OS_ANDROID
|
||||
QScopedPointer<Sonnet::Speller> mSpellchecker;
|
||||
QScopedPointer<Sonnet::GuessLanguage> mLanguageGuesser;
|
||||
#endif
|
||||
QString m_selectedWord;
|
||||
QQuickTextDocument *m_document;
|
||||
int m_cursorPosition;
|
||||
int m_selectionStart;
|
||||
int m_selectionEnd;
|
||||
|
||||
int m_autoCompleteBeginPosition = -1;
|
||||
int m_autoCompleteEndPosition = -1;
|
||||
int m_wordIsMisspelled = false;
|
||||
};
|
||||
@@ -23,5 +23,9 @@ const EventContent::ImageContent &StickerEvent::image() const
|
||||
|
||||
QUrl StickerEvent::url() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return m_imageContent.url();
|
||||
#else
|
||||
return m_imageContent.url;
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user