Compare commits

..

66 Commits

Author SHA1 Message Date
Bhushan Shah
be116e1ba7 GIT_SILENT: add changelog entries for 22.04 2022-04-23 18:25:50 +05:30
Bhushan Shah
3396f831d4 GIT_SILENT: bump version to 22.04 2022-04-23 17:04:13 +05:30
l10n daemon script
a0f6170539 GIT_SILENT Sync po/docbooks with svn 2022-04-23 01:49:18 +00:00
l10n daemon script
731c6f924c GIT_SILENT Sync po/docbooks with svn 2022-04-22 01:53:07 +00:00
l10n daemon script
3011c3d885 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-22 01:44:58 +00:00
Nicolas Fella
709b2c8fd9 Use undeprecated install dirs
Using kde-dev-scripts/kf5/cmakelists_install_vars.pl
2022-04-21 20:58:00 +02:00
l10n daemon script
0cfa87e23d GIT_SILENT Sync po/docbooks with svn 2022-04-21 01:48:36 +00:00
l10n daemon script
538ed7dd02 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-21 01:42:02 +00:00
l10n daemon script
180a754e67 GIT_SILENT Sync po/docbooks with svn 2022-04-19 01:51:51 +00:00
l10n daemon script
81ba5f6ee5 GIT_SILENT Sync po/docbooks with svn 2022-04-17 01:57:04 +00:00
l10n daemon script
a15b406cff SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-16 01:44:16 +00:00
Marcus Harrison
d0bc8f3d05 Fix mis-aligned user messages
In compact mode with userMessagesOnRight, the user
avatar disappeared and their messages left space
on the right for an avatar that wasn't displayed
anymore.
2022-04-14 14:38:02 +02:00
l10n daemon script
c83f4b4f75 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-10 01:45:29 +00:00
Tobias Fella
0f5425e030 Require passing tests on CI 2022-04-09 21:33:52 +02:00
Tobias Fella
f381cc4623 Close WelcomePage after account is loaded 2022-04-09 19:47:36 +02:00
Tobias Fella
decd528079 Disable busyindicator 2022-04-09 19:47:17 +02:00
Tobias Fella
0c5bd57976 Fix REUSE check on CI
The CI installs files to _include and _build in the source directory, which breaks
the REUSE check
2022-04-09 15:19:35 +00:00
Tobias Fella
7362b90c42 Don't try to load more messages than there are in the timeline
The function call from qml is removed because it is redundant
2022-04-08 18:44:30 +00:00
Tobias Fella
aef6d6fc85 More typing notification improvements 2022-04-08 20:37:17 +02:00
Tobias Fella
432e209b16 Try fixing stuck read notifications 2022-04-08 20:33:41 +02:00
l10n daemon script
a72cac5ea3 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-08 01:44:45 +00:00
Tobias Fella
b9152dc93c Add ki18n_install 2022-04-07 17:25:16 +02:00
l10n daemon script
e5791970da SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-07 01:43:10 +00:00
Carl Schwan
c157625645 Fix link 2022-04-06 14:04:21 +00:00
Nicolas Fella
026c7660bc Add Windows CI 2022-04-06 12:01:47 +02:00
Nicolas Fella
be10e66974 Fix condition to build runner 2022-04-06 12:01:47 +02:00
l10n daemon script
024fb1a97a SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-04 01:46:19 +00:00
l10n daemon script
e4c8b6b676 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-03 01:54:27 +00:00
l10n daemon script
863508b629 GIT_SILENT made messages (after extraction) 2022-04-03 00:48:36 +00:00
l10n daemon script
ef5550bafd SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-04-02 01:41:06 +00:00
Nicolas Fella
1cc8d915bc Add rooms runner
This allows to search for and open rooms in KRunner
2022-04-01 10:56:19 +00:00
Snehit Sah
9a5f2e4938 Show subtitle text without markdown
Create new role in RoomListModel to send back cleaned subtitle text
2022-03-31 17:39:34 +00:00
l10n daemon script
a747d44cac SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-03-14 01:43:37 +00:00
Tobias Fella
334c13b36c Set preferredWidth and preferredHeight of images 2022-03-11 15:09:57 +01:00
Tobias Fella
aac96da2e2 Revert "Show RoomList when cached state is loaded"
This reverts commit db5f328539.
2022-03-08 21:10:38 +01:00
Tobias Fella
12f3f72a67 Lower typing notification timeouts 2022-03-08 15:00:00 +01:00
Tobias Fella
62f6cfbf9a Force RoomListDelegate to use plaintext
Text.AutoText isn't robust enough to handle this
2022-03-08 14:45:33 +01:00
l10n daemon script
c59e3db1dd SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-03-05 01:43:56 +00:00
Tobias Fella
9252e0e65e Disable the BusyIndicator
For some reason having the busyindicator running increases the time
required to load the state cache by several orders of magnitude
2022-03-01 12:18:07 +00:00
Carl Schwan
80ee5e9356 Apply 1 suggestion(s) to 1 file(s) 2022-03-01 00:29:07 +00:00
Tobias Fella
be802a28c2 Make invitation notifications persistent 2022-03-01 00:29:07 +00:00
Tobias Fella
b2a8430fa2 Don't apply autocompletion when autocomplete list is empty
Fixes sending messages like ':)'
2022-03-01 00:26:28 +00:00
Tobias Fella
db5f328539 Show RoomList when cached state is loaded
This should somewhat speed up the loading since we don't need to wait
until the first sync is done.

It's still slow though since loading the cache is slow
2022-03-01 00:29:48 +01:00
l10n daemon script
9ac1fbd99b GIT_SILENT made messages (after extraction) 2022-02-27 00:46:56 +00:00
Tobias Fella
022951a9df Add nicer delegate message for widget events 2022-02-25 20:49:57 +00:00
Tobias Fella
47a0d30e57 Fix quitting without tray icon
Setting KSNI status to Passive doesn't *disable* the tray icon, it just
moves it to the overflow menu. This causes the application to not quit
when closing the app even when disabling the tray icon
2022-02-25 20:19:12 +00:00
Tobias Fella
faeb1964bd Prepare Image & Video loading for E2EE
Changes the urls to make sure they are decrypted, while making sure that
it is backwards compatible to libQuotient 0.6
2022-02-25 21:15:46 +01:00
Tobias Fella
db8b2fd64b Aggregate similar state events 2022-02-25 20:10:07 +00:00
Tobias Fella
37c7fe380b Don't load backlog until read marker
This is bad if there are a lot of unread messages
2022-02-25 12:29:03 +01:00
l10n daemon script
537a1e44b1 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2022-02-25 01:50:55 +00:00
Tobias Fella
dc9d574b58 Fix login regex 2022-02-23 22:49:58 +01:00
Jose Flores
7c807e6a25 Modifies regex check for valid matrix server to accept ip addresses. 2022-02-22 22:54:43 +00:00
Jose Flores
f74c6a41ae Wraps the checkbox text for messages on the right to wrap on mobile devices (PinePhone). 2022-02-22 03:47:20 +00:00
Jose Flores
7d5a8c87a1 Wraps the quick edit checkbox using workaround 2022-02-22 03:47:20 +00:00
Sandro Knauß
ca719b835e The component of QtCoro5 is called Core and not Coro ;) 2022-02-21 16:31:24 +00:00
l10n daemon script
9b5ad3a3a0 GIT_SILENT made messages (after extraction) 2022-02-21 00:43:44 +00:00
Jose Flores
fdfbbb1b04 Uses the formatted message to enable clickable links for mobile. 2022-02-19 14:30:16 +00:00
Tobias Fella
dd91cb91d0 Load replied-to message when it isn't in the timeline already 2022-02-18 16:11:51 +01:00
Tobias Fella
290b2249c4 Port away from CMake deprecation 2022-02-14 22:41:42 +01:00
Jose Flores
8b8e521c56 Fix issue with clear image button. Will only be visible if the user has an avatar (local or saved) 2022-02-13 22:46:51 +00:00
Tobias Fella
cba88e1af7 Allow disabling notification inline reply
Is temporarily required for encrypted rooms
2022-02-12 22:33:10 +01:00
Tobias Fella
1661d34d7c Use Quotient's NetworkAccessManager in QML
Will be required for showing encrypted images
2022-02-12 22:23:59 +01:00
Tobias Fella
dc3b1a3c87 Remove unneeded parameter 2022-02-12 22:09:38 +01:00
Tobias Fella
f55dc19d95 Make user colors update when colortheme changes 2022-02-11 02:06:46 +01:00
Vitaly Zaitsev
6014c15b4f SingleMainWindow is a part of XDG SPEC version 1.5 and bogus on 1.0.
Signed-off-by: Vitaly Zaitsev <vitaly@easycoding.org>
2022-02-09 17:30:19 +01:00
l10n daemon script
a5f835b1eb GIT_SILENT made messages (after extraction) 2022-02-09 00:47:40 +00:00
73 changed files with 70340 additions and 153 deletions

View File

@@ -5,3 +5,4 @@ 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/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

View File

@@ -23,3 +23,6 @@ Dependencies:
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'
Options:
require-passing-tests-on: [ 'Linux', 'Windows' ]

View File

@@ -7,7 +7,7 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "22.02")
set(PROJECT_VERSION "22.04")
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)
@@ -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})

View File

@@ -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

View File

@@ -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) {
currentRoom.sendTypingNotification(true)
}
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;

View File

@@ -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

View File

@@ -23,5 +23,6 @@ Kirigami.PlaceholderMessage {
QQC2.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: false
}
}

View File

@@ -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]+)?$/
}
}
}

View File

@@ -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

View File

@@ -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()
}
}
}

View File

@@ -22,11 +22,20 @@ QQC2.ItemDelegate {
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))
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

View File

@@ -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;

View File

@@ -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()
}
}
}

View File

@@ -13,7 +13,7 @@ Kirigami.Page {
anchors.centerIn: parent
text: i18n("Loading…")
QQC2.BusyIndicator {
running: loadingIndicator.visible
running: false
Layout.alignment: Qt.AlignHCenter
}
}

View File

@@ -236,13 +236,9 @@ 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

View File

@@ -41,6 +41,8 @@ Kirigami.ScrollablePage {
if(pageStack.lastItem == page) {
pageStack.pop()
}
} else if (page.currentRoom.isInvite) {
page.currentRoom.clearInvitationNotification();
}
}
}
@@ -239,6 +241,11 @@ Kirigami.ScrollablePage {
}
}
CollapseStateProxyModel {
id: collapseStateProxyModel
sourceModel: sortedMessageEventModel
}
ListView {
id: messageListView
visible: !invitation.visible
@@ -251,7 +258,7 @@ Kirigami.ScrollablePage {
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
model: !isLoaded ? undefined : sortedMessageEventModel
model: !isLoaded ? undefined : collapseStateProxyModel
MessageEventModel {
id: messageEventModel
@@ -400,12 +407,6 @@ Kirigami.ScrollablePage {
}
Component.onCompleted: {
if (currentRoom) {
if (currentRoom.timelineSize < 20) {
currentRoom.getPreviousContent(50);
}
}
positionViewAtBeginning();
}
@@ -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,

View File

@@ -37,6 +37,13 @@ Kirigami.ScrollablePage {
}
}
Connections {
target: Controller
function onInitiated() {
pageStack.layers.pop();
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"

View File

@@ -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 = ""

View File

@@ -175,6 +175,7 @@ Kirigami.ScrollablePage {
}
}
Kirigami.FormLayout {
Layout.maximumWidth: parent.width
QQC2.CheckBox {
Kirigami.FormData.label: "Show Avatar:"
text: i18n("In Chat")

View File

@@ -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: {

View File

@@ -73,7 +73,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 usant 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 +82,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>
@@ -98,7 +99,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 xats encriptats i els xats 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 +108,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>
@@ -123,7 +125,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 +134,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>
@@ -194,15 +197,21 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<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>

View File

@@ -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
@@ -46,6 +48,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ą
@@ -76,6 +79,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

2020
po/ar/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2151
po/az/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2201
po/ca/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2015
po/ca@valencia/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1998
po/cs/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2101
po/da/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2165
po/de/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2166
po/en_GB/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2197
po/es/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2187
po/eu/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2156
po/fi/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2185
po/fr/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2193
po/hu/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2092
po/ia/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2005
po/id/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2183
po/it/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1990
po/ja/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2080
po/ko/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2195
po/nl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2001
po/nn/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2149
po/pa/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2168
po/pl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2005
po/pt/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2167
po/pt_BR/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2179
po/sk/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2014
po/sl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2194
po/sv/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2100
po/ta/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2007
po/tok/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2049
po/tr/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2216
po/uk/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1998
po/x-test/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1994
po/zh_CN/neochat.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ add_executable(neochat
blurhash.cpp
blurhashimageprovider.cpp
joinrulesevent.cpp
collapsestateproxymodel.cpp
../res.qrc
)
@@ -62,7 +63,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 +127,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 +140,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()

View 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 {};
}
}

View 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;
};

View File

@@ -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());
});

View File

@@ -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);

View File

@@ -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,6 +63,7 @@
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "networkaccessmanager.h"
#include "notificationsmanager.h"
#include "publicroomlistmodel.h"
#include "roomlistmodel.h"
@@ -74,8 +78,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)
{
@@ -187,6 +204,7 @@ int main(int argc, char *argv[])
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 +237,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 +262,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,

View File

@@ -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 {};
}
@@ -847,3 +877,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});
});
}

View File

@@ -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);

View File

@@ -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
@@ -37,6 +38,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 +47,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
@@ -78,6 +81,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ė
@@ -108,6 +112,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ė
@@ -133,11 +138,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
@@ -154,11 +162,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

View File

@@ -252,6 +252,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 +340,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 +429,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 +538,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());
},
@@ -781,3 +835,8 @@ QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user)
}
}
}
void NeoChatRoom::clearInvitationNotification()
{
NotificationsManager::instance().clearInvitationNotification(id());
}

View File

@@ -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

View File

@@ -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());
@@ -61,15 +61,17 @@ void NotificationsManager::postNotification(NeoChatRoom *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 +89,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();
}
}

View File

@@ -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;
};

View File

@@ -0,0 +1,61 @@
# 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[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[it]=Trova stanze in NeoChat
Comment[ko]=NeoChat에서 대화방 찾기
Comment[nl]=Rooms zoeken in 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[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

View File

@@ -171,7 +171,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] {
@@ -236,11 +236,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 +329,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 +396,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 +435,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[CategoryVisibleRole] = "categoryVisible";
roles[SubtitleTextRole] = "subtitleText";
return roles;
}

View File

@@ -49,6 +49,9 @@ public:
JoinStateRole,
CurrentRoomRole,
CategoryVisibleRole,
SubtitleTextRole,
AvatarImageRole,
IdRole,
};
Q_ENUM(EventRoles)

94
src/runner.cpp Normal file
View 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
View 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;
};