Compare commits

..

1 Commits

154 changed files with 9515 additions and 26529 deletions

View File

@@ -31,7 +31,8 @@ Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: BSD-2-Clause
Files: src/neochatconfig.kcfg
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <fella@posteo.de>
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>
Copyright: 2020-2021 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/neochat.notifyrc

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "22.09")
set(PROJECT_VERSION "22.06")
set(KF5_MIN_VERSION "5.91.0")
set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.2")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -25,14 +25,12 @@ include(FeatureSummary)
include(ECMSetupVersion)
include(KDEInstallDirs)
include(ECMFindQmlModule)
include(KDEClangFormat)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
@@ -139,12 +137,11 @@ add_subdirectory(src)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
if (NOT ANDROID)
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})
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)
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
endif()
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

View File

@@ -21,7 +21,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.lib_name" android:value="neochat"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>

View File

@@ -9,6 +9,6 @@ install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
${CMAKE_INSTALL_SYSCONFDIR}/fonts/conf.d/
)

View File

@@ -405,9 +405,7 @@ ToolBar {
}
currentRoom.markAllMessagesAsRead();
inputField.clear();
inputField.text = Qt.binding(function() {
return currentRoom ? currentRoom.cachedInput : "";
});
inputField.text = currentRoom ? currentRoom.cachedInput : "";
messageSent()
}

View File

@@ -3,12 +3,10 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
Popup {
ApplicationWindow {
id: root
property alias source: image.source
@@ -16,293 +14,57 @@ Popup {
property string blurhash: ""
property int imageWidth: -1
property int imageHeight: -1
property var modelData
parent: Overlay.overlay
closePolicy: Popup.CloseOnEscape
width: parent.width
height: parent.height
modal: true
padding: 0
background: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
ColumnLayout {
anchors.fill: parent
spacing: Kirigami.Units.largeSpacing
title: i18n("Image View - %1", filename)
Control {
Layout.fillWidth: true
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
color: Kirigami.Theme.backgroundColor
Kirigami.Avatar {
id: avatar
background: AbstractButton {
onClicked: root.destroy()
}
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
BusyIndicator {
visible: image.status !== Image.Ready && root.blurhash === ""
anchors.centerIn: parent
running: visible
}
name: modelData.author.name ?? modelData.author.displayName
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
color: modelData.author.color
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
AnimatedImage {
id: image
anchors.centerIn: parent
Label {
id: nameLabel
width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width)
height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height)
text: modelData.author.displayName
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
}
Label {
id: timeLabel
fillMode: Image.PreserveAspectFit
text: time.toLocaleString(Qt.locale(), Locale.ShortFormat)
}
}
Label {
id: imageLabel
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
text: modelData.display
font.weight: Font.Bold
elide: Text.ElideRight
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom in")
Accessible.name: text
icon.name: "zoom-in"
display: AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor + 0.25
if (image.scaleFactor > 3) {
image.scaleFactor = 3
}
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom out")
Accessible.name: text
icon.name: "zoom-out"
display: AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor - 0.25
if (image.scaleFactor < 0.25) {
image.scaleFactor = 0.25
}
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate left")
Accessible.name: text
icon.name: "image-rotate-left-symbolic"
display: AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle - 90
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate right")
Accessible.name: text
icon.name: "image-rotate-right-symbolic"
display: AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle + 90
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Save as")
Accessible.name: text
icon.name: "document-save"
display: AbstractButton.IconOnly
onClicked: {
var dialog = saveAsDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Close")
Accessible.name: text
icon.name: "dialog-close"
display: AbstractButton.IconOnly
onClicked: {
root.close()
}
ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
}
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
}
Kirigami.Separator {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 1
}
}
BusyIndicator {
Layout.fillWidth: true
visible: image.status !== Image.Ready && root.blurhash === ""
running: visible
}
// Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area.
Item {
id: imageContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
clip: true
Image {
id: image
property var scaleFactor: 1
property int rotationAngle: 0
property var rotationInsensitiveWidth: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2)
property var rotationInsensitiveHeight: Math.min(root.imageHeight !== -1 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2)
anchors.centerIn: parent
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
fillMode: Image.PreserveAspectFit
clip: true
Behavior on width {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on height {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Image {
anchors.centerIn: parent
width: image.width
height: image.height
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
visible: root.blurhash !== "" && parent.status !== Image.Ready
}
transform: [
Rotation {
origin.x: image.width / 2
origin.y: image.height / 2
angle: image.rotationAngle
Behavior on angle {
RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
},
Scale {
origin.x: image.width / 2
origin.y: image.height / 2
xScale: image.scaleFactor
yScale: image.scaleFactor
Behavior on xScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on yScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
}
]
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author,
message: modelData.message,
eventId: modelData.eventId,
source: modelData.source,
file: root.parent,
mimeType: modelData.mimeType,
progressInfo: modelData.progressInfo,
plainMessage: modelData.message,
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
root.close()
}
}
Image {
anchors.centerIn: parent
width: image.width
height: image.height
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
visible: root.blurhash !== "" && parent.status !== Image.Ready
}
}
Component {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
}
}
}
Button {
anchors.top: parent.top
anchors.right: parent.right
onClosed: {
image.scaleFactor = 1
image.rotationAngle = 0
text: i18n("Close")
icon.name: "dialog-close"
display: AbstractButton.IconOnly
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
onClicked: root.destroy()
}
}

View File

@@ -5,18 +5,24 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
Kirigami.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
property var showContinueButton: false
property var showBackButton: false
text: i18n("Synchronizing with your homeserver…")
icon.name: "cloud-download"
property string title: i18n("Loading…")
anchors.centerIn: parent
explanation: i18n("Please wait. This might take a little while.")
QQC2.Label {
text: i18n("Please wait. This might take a little while.")
}
QQC2.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: false
}
}

View File

@@ -14,29 +14,24 @@ import NeoChat.Component 1.0
LoginStep {
id: login
showContinueButton: LoginHelper.homeserverReachable
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Welcome to NeoChat!")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
QQC2.Label {
text: i18n("To get started, enter your matrix ID:")
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if(acceptableInput) {
LoginHelper.matrixId = text
} else {
LoginHelper.matrixId = ""
}
}
@@ -66,6 +61,5 @@ LoginStep {
}
}
enabled: LoginHelper.homeserverReachable
iconName: "go-next"
}
}

View File

@@ -25,7 +25,6 @@ LoginStep {
onTriggered: {
LoginHelper.login();
}
iconName: "go-next"
}
Connections {

View File

@@ -26,23 +26,15 @@ LoginStep {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
}
RowLayout {
QQC2.Button {
text: i18nc("@action:button", "Back")
onClicked: {
module.source = "qrc:/imports/NeoChat/Component/Login/Login.qml"
}
}
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
}
}

View File

@@ -14,8 +14,6 @@ TimelineContainer {
innerObject: TextEdit {
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
color: Kirigami.Theme.disabledTextColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true

View File

@@ -42,7 +42,9 @@ DelegateChooser {
DelegateChoice {
roleValue: "sticker"
delegate: ImageDelegate {}
delegate: ImageDelegate {
cardBackground: false
}
}
DelegateChoice {

View File

@@ -53,7 +53,6 @@ TimelineContainer {
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: openSavedFile()
}
@@ -71,7 +70,6 @@ TimelineContainer {
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: currentRoom.cancelFileTransfer(eventId)
}
},

View File

@@ -6,8 +6,6 @@ import QtQuick.Controls 2.15
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
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
@@ -52,7 +50,6 @@ TimelineContainer {
ToolTip.text: model.display
ToolTip.visible: hoverHandler.hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
@@ -92,21 +89,16 @@ TimelineContainer {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onTapped: {
img.ToolTip.hide()
fullScreenImage.open()
fullScreenImage.createObject(parent, {
filename: eventId,
source: model.mediaUrl,
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h
}).showFullScreen();
}
}
FullScreenImage {
id: fullScreenImage
filename: eventId
source: mediaUrl
blurhash: model.content.info["xyz.amorgan.blurhash"]
imageWidth: content.info.w
imageHeight: content.info.h
modelData: model
}
function downloadAndOpen() {
if (downloaded) {
openSavedFile()

View File

@@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
RowLayout {
id: row
property var links: model.display.match(/(\bhttps?:\/\/[^\s\<\>\"\']*[^\s\<\>\"\'])/g)
// don't show previews for room links or user mentions
.filter(link => !link.includes("https://matrix.to"))
// remove ending fullstops and commas
.map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link)
LinkPreviewer {
id: lp
url: links[0]
}
visible: lp.loaded && lp.title
Rectangle {
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
visible: lp.loaded && lp.title
color: Kirigami.Theme.highlightColor
}
Image {
visible: lp.imageSource
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
Layout.maximumWidth: Kirigami.Units.gridUnit * 5
source: lp.imageSource.replace("mxc://", "image://mxc/")
fillMode: Image.PreserveAspectFit
}
ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
Layout.fillWidth: true
level: 4
wrapMode: Text.Wrap
textFormat: Text.RichText
text: "<style>
a {
text-decoration: none;
}
</style>
<a href=\"" + links[0] + "\">" + lp.title.replace("&ndash;", "—") + "</a>"
visible: lp.loaded
onLinkActivated: RoomManager.openResource(link)
}
Label {
text: lp.description
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
Layout.fillWidth: true
wrapMode: Text.Wrap
visible: lp.loaded && lp.description
}
}
}

View File

@@ -14,26 +14,14 @@ TimelineContainer {
id: messageDelegate
property bool isEmote: false
onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument))
onOpenContextMenu: openMessageContext(model, parent.selectedText, Controller.plainText(label.textDocument))
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: ColumnLayout {
Layout.maximumWidth: messageDelegate.contentMaxWidth
RichLabel {
id: label
isEmote: messageDelegate.isEmote
}
Loader {
id: linkPreviewLoader
Layout.fillWidth: true
height: active ? item.implicitHeight : 0
active: !currentRoom.usesEncryption && model.display && model.display.includes("http")
visible: active
sourceComponent: LinkPreviewDelegate {
anchors.verticalCenter: parent.verticalCenter
}
}
innerObject: RichLabel {
id: label
isEmote: messageDelegate.isEmote
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
}
}

View File

@@ -15,7 +15,7 @@ MouseArea {
id: replyButton
Layout.fillWidth: true
implicitHeight: replyName.implicitHeight + (loader.item ? loader.item.height : 0) + Kirigami.Units.largeSpacing
implicitWidth: Math.min(contentMaxWidth, Math.max((loader.item ? loader.item.width : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
implicitWidth: Math.min(contentMaxWidth, Math.max((loader.item ? loader.item.width + Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
Component.onCompleted: {
parent.Layout.fillWidth = true;
parent.Layout.preferredWidth = Qt.binding(function() { return implicitWidth; })

View File

@@ -15,31 +15,14 @@ TextEdit {
readonly property var hasSpoiler: /data-mx-spoiler/g
property bool isEmote: false
/* Turn all links which aren't already in <a> tags into <a> hyperlinks */
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g
property string textMessage: model.display.includes("http")
? model.display.replace(linkRegex, function() {
if (arguments[1]) {
return arguments[0];
} else {
var l = arguments[2];
if ([".", ","].includes(l[l.length-1])) {
var link = l.substring(0, l.length-1);
var leftover = l[l.length-1];
return "<a href=\"" + link + "\">" + link + "</a>" + leftover;
}
return "<a href=\"" + l + "\">" + l + "</a>";
}
})
: model.display
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
Layout.fillWidth: true
persistentSelection: true
Layout.fillWidth: Config.compactLayout
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
text: "<style>
table {
@@ -73,8 +56,6 @@ a{
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true

View File

@@ -18,17 +18,6 @@ QQC2.ItemDelegate {
property bool isEmote: false
property bool cardBackground: true
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
property bool isTemporaryHighlighted: false
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
signal openContextMenu
@@ -53,16 +42,9 @@ QQC2.ItemDelegate {
topPadding: 0
bottomPadding: 0
topInset: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing))
background: Rectangle {
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : 0)
background: null
property Item hoverComponent
@@ -121,20 +103,15 @@ QQC2.ItemDelegate {
Kirigami.Avatar {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0
height: width
padding: Kirigami.Units.smallSpacing
topInset: Kirigami.Units.smallSpacing
bottomInset: Kirigami.Units.smallSpacing
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
sourceSize.width: width
sourceSize.height: width
anchors {
top: sectionDelegate.bottom
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : 0
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
leftMargin: Kirigami.Units.largeSpacing
}
visible: model.showAuthor &&
@@ -163,13 +140,13 @@ QQC2.ItemDelegate {
id: bubble
topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Config.compactLayout ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
hoverEnabled: true
anchors {
top: avatar.top
leftMargin: Kirigami.Units.smallSpacing
leftMargin: Kirigami.Units.largeSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
@@ -205,17 +182,23 @@ QQC2.ItemDelegate {
contentItem: ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
RowLayout {
spacing: 0
Item {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor && !isEmote
Layout.fillWidth: true
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.preferredWidth: nameLabel.implicitWidth + timeLabel.implicitWidth + Kirigami.Units.largeSpacing * 2
Layout.maximumWidth: contentMaxWidth
implicitHeight: visible ? nameLabel.implicitHeight : 0
QQC2.Label {
id: nameLabel
topInset: 0
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
visible: model.showAuthor && !isEmote
width: Math.min(contentMaxWidth - timeLabel.width, implicitWidth)
text: visible ? author.displayName : ""
textFormat: Text.PlainText
@@ -238,12 +221,14 @@ QQC2.ItemDelegate {
}
QQC2.Label {
id: timeLabel
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
anchors.left: nameLabel.right
visible: model.showAuthor && !isEmote
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
@@ -255,6 +240,9 @@ QQC2.ItemDelegate {
active: model.reply !== undefined
source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml'
visible: active
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
Layout.leftMargin: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing
Connections {
target: replyLoader.item
@@ -266,14 +254,19 @@ QQC2.ItemDelegate {
}
background: Item {
Rectangle {
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
anchors.fill: parent
}
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
color: {
if (model.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (timelineContainer.isHighlighted) {
} else if (model.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
@@ -281,13 +274,9 @@ QQC2.ItemDelegate {
}
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: timelineContainer.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
Behavior on color {
ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
}
}

View File

@@ -12,4 +12,3 @@ EncryptedDelegate 1.0 EncryptedDelegate.qml
EventDelegate 1.0 EventDelegate.qml
MessageDelegate 1.0 MessageDelegate.qml
ReadMarkerDelegate 1.0 ReadMarkerDelegate.qml
LinkPreviewDelegate 1.0 LinkPreviewDelegate.qml

View File

@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: emojiItem
property string emoji
property string description
QQC2.Label {
id: emojiLabel
x: 0
y: 0
width: parent.width
height: parent.height * 0.75
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: emojiItem.emoji
font.family: "emoji"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4
}
QQC2.Label {
x: 0
y: parent.height * 0.75
width: parent.width
height: parent.height * 0.25
text: emojiItem.description
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Row {
id: emojiRow
property alias model: repeater.model
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
id: repeater
delegate: EmojiItem {
emoji: modelData.emoji
description: modelData.description
width: emojiRow.height
height: width
}
}
}

View File

@@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: emojiSas
required property var model
signal accept()
signal reject()
visible: dialog.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Confirm the emoji below are displayed on both devices, in the same order.")
}
EmojiRow {
anchors.horizontalCenter: parent.horizontalCenter
height: Kirigami.Units.gridUnit * 4
model: emojiSas.model.slice(0, 4)
}
EmojiRow {
anchors.horizontalCenter: parent.horizontalCenter
height: Kirigami.Units.gridUnit * 4
model: emojiSas.model.slice(4, 7)
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They match")
icon.name: "dialog-ok"
onClicked: emojiSas.accept()
}
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They don't match")
icon.name: "dialog-cancel"
onClicked: emojiSas.reject()
}
}
}

View File

@@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: dialog
title: i18n("Session Verification")
required property var session
Item {
anchors.fill: parent
VerificationCanceled {
visible: dialog.session.state === KeyVerificationSession.CANCELED
anchors.centerIn: parent
reason: dialog.session.error
}
EmojiSas {
anchors.centerIn: parent
visible: dialog.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
model: dialog.session.sasEmojis
onReject: dialog.session.cancelVerification(KeyVerificationSession.MISMATCHED_SAS)
onAccept: dialog.session.sendMac()
}
Message {
visible: dialog.session.state === KeyVerificationSession.WAITINGFORREADY
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Waiting for device to accept verification.")
}
Message {
visible: dialog.session.state === KeyVerificationSession.INCOMING
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Incoming key verification request from device **%1**", dialog.session.remoteDeviceId)
}
Message {
visible: dialog.session.state === KeyVerificationSession.WAITINGFORMAC
anchors.centerIn: parent
icon: "security-medium-symbolic"
text: i18n("Waiting for other party to verify.")
}
Kirigami.BasicListItem {
id: emojiVerification
text: "Emoji Verification"
visible: dialog.session.state === KeyVerificationSession.READY
subtitle: i18n("Compare a set of emoji on both devices")
onClicked: {
dialog.session.sendStartSas()
}
}
Message {
visible: dialog.session.state === KeyVerificationSession.DONE
anchors.centerIn: parent
text: i18n("Successfully verified device **%1**", dialog.session.remoteDeviceId)
icon: "security-high"
}
}
footer: QQC2.ToolBar {
visible: dialog.session.state === KeyVerificationSession.INCOMING
QQC2.DialogButtonBox {
anchors.fill: parent
Item { Layout.fillWidth: true }
QQC2.Button {
text: i18n("Accept")
icon.name: "dialog-ok"
onClicked: dialog.session.sendReady()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
QQC2.Button {
text: i18n("Decline")
icon.name: "dialog-cancel"
onClicked: dialog.session.cancelVerification("m.user", "Declined")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.CancelRole
}
}
}
}

View File

@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Column {
id: message
required property string icon
required property string text
anchors.centerIn: parent
Kirigami.Icon {
width: Kirigami.Units.iconSizes.enormous
height: width
anchors.horizontalCenter: parent.horizontalCenter
source: message.icon
}
QQC2.Label {
text: message.text
textFormat: Text.MarkdownText
}
}

View File

@@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQml 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
Message {
id: verificationCanceled
required property int reason
anchors.centerIn: parent
icon: "security-low"
text: {
switch(verificationCanceled.reason) {
case KeyVerificationSession.NONE:
return i18n("The session verification was canceled for unknown reason.");
case KeyVerificationSession.TIMEOUT:
return i18n("The session verification timed out.");
case KeyVerificationSession.REMOTE_TIMEOUT:
return i18n("The session verification timed out for remote party.");
case KeyVerificationSession.USER:
return i18n("You canceled the session verification.");
case KeyVerificationSession.REMOTE_USER:
return i18n("The remote party canceled the session verification.");
case KeyVerificationSession.UNEXPECTED_MESSAGE:
return i18n("The session verification was canceled because we received an unexpected message.");
case KeyVerificationSession.REMOTE_UNEXPECTED_MESSAGE:
return i18n("The remote party canceled the session verification because it received an unexpected message.");
case KeyVerificationSession.UNKNOWN_TRANSACTION:
return i18n("The session verification was canceled because it received a message for an unknown session.");
case KeyVerificationSession.REMOTE_UNKNOWN_TRANSACTION:
return i18n("The remote party canceled the session verification because it received a message for an unknown session.");
case KeyVerificationSession.UNKNOWN_METHOD:
return i18n("The session verification was canceled because NeoChat is unable to handle this verification method.");
case KeyVerificationSession.REMOTE_UNKNOWN_METHOD:
return i18n("The remote party canceled the session verification because it is unable to handle this verification method.");
case KeyVerificationSession.KEY_MISMATCH:
return i18n("The session verification was canceled because the keys are incorrect.");
case KeyVerificationSession.REMOTE_KEY_MISMATCH:
return i18n("The remote party canceled the session verification because the keys are incorrect.");
case KeyVerificationSession.USER_MISMATCH:
return i18n("The session verification was canceled because it verifies an unexpected user.");
case KeyVerificationSession.REMOTE_USER_MISMATCH:
return i18n("The remote party canceled the session verification because it verifies an unexpected user.");
case KeyVerificationSession.INVALID_MESSAGE:
return i18n("The session verification was canceled because we received an invalid message.");
case KeyVerificationSession.REMOTE_INVALID_MESSAGE:
return i18n("The remote party canceled the session verification because it received an invalid message.");
case KeyVerificationSession.SESSION_ACCEPTED:
return i18n("The session was accepted on a different device"); //TODO this should not be visible
case KeyVerificationSession.REMOTE_SESSION_ACCEPTED:
return i18n("The session was accepted on a different device"); //TODO neither should this
case KeyVerificationSession.MISMATCHED_COMMITMENT:
return i18n("The session verification was canceled because of a mismatched key.");
case KeyVerificationSession.REMOTE_MISMATCHED_COMMITMENT:
return i18n("The remote party canceled the session verification because of a mismatched key.");
case KeyVerificationSession.MISMATCHED_SAS:
return i18n("The session verification was canceled because the keys do not match.");
case KeyVerificationSession.REMOTE_MISMATCHED_SAS:
return i18n("The remote party canceled the session verification because the keys do not match.");
default:
return i18n("The session verification was canceled due to an unknown error.");
}
}
}

View File

@@ -1,7 +0,0 @@
module NeoChat.Dialog.KeyVerification
KeyVerificationDialog 1.0 KeyVerificationDialog.qml
Message 1.0 Message.qml
VerificationCanceled 1.0 VerificationCanceled.qml
EmojiItem 1.0 EmojiItem.qml
EmojiRow 1.0 EmojiRow.qml
EmojiSas 1.0 EmojiSas.qml

View File

@@ -14,8 +14,6 @@ import NeoChat.Component 1.0
Kirigami.OverlaySheet {
id: root
signal closed()
property var room
property var user
@@ -54,7 +52,7 @@ Kirigami.OverlaySheet {
onClicked: {
if (avatarMediaId) {
fullScreenImage.createObject(parent, {filename: displayName, source: room.urlToMxcUrl(avatarUrl)}).showFullScreen()
fullScreenImage.createObject(parent, {"filename": displayName, "source": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
}
}
}
@@ -166,11 +164,5 @@ Kirigami.OverlaySheet {
FullScreenImage {}
}
}
onSheetOpenChanged: {
if (!sheetOpen) {
closed()
}
}
}

View File

@@ -7,4 +7,3 @@ OpenFileDialog 1.0 OpenFileDialog.qml
ImageClipboardDialog 1.0 ImageClipboardDialog.qml
StartChatDialog 1.0 StartChatDialog.qml
EmojiDialog 1.0 EmojiDialog.qml
KeyVerificationDialog 1.0 KeyVerificationDialog.qml

View File

@@ -45,7 +45,7 @@ Labs.MenuBar {
Labs.MenuItem {
text: i18nc("menu", "New Private Chat…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
}
Labs.MenuItem {
text: i18nc("menu", "New Group…")
@@ -58,7 +58,7 @@ Labs.MenuBar {
}
Labs.MenuItem {
text: i18nc("menu", "Browse Chats…")
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
}
}
EditMenu {

View File

@@ -24,7 +24,7 @@ Loader {
Menu {
MenuItem {
id: newWindow
text: i18n("Open in New Window")
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
@@ -49,7 +49,7 @@ Loader {
}
MenuItem {
text: i18nc("@action:inmenu", "Copy Address to Clipboard")
text: i18nc("@action:inmenu", "Copy address to clipboard")
onTriggered: if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
@@ -57,53 +57,8 @@ Loader {
}
}
Menu {
title: i18n("Notification State")
MenuItem {
text: i18n("Follow Global Setting")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Default
}
}
MenuItem {
text: i18nc("As in 'notify for all messages'","All")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.All
}
}
MenuItem {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'","@Mentions and Keywords")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
MenuItem {
text: i18nc("As in 'do not notify for any messages'","Off")
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Mute
}
}
}
MenuItem {
text: i18n("Room Settings")
text: i18n("Room settings")
onTriggered: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}

View File

@@ -14,8 +14,6 @@ import NeoChat.Menu 1.0
MessageDelegateContextMenu {
id: root
signal closeFullscreen
required property var file
required property var progressInfo
required property string mimeType
@@ -53,7 +51,6 @@ MessageDelegateContextMenu {
icon.name: "mail-replied-symbolic"
onTriggered: {
chatBoxHelper.replyToMessage(eventId, message, author);
root.closeFullscreen()
}
},
Kirigami.Action {
@@ -63,18 +60,8 @@ MessageDelegateContextMenu {
icon.color: "red"
onTriggered: {
currentRoom.redactEvent(eventId);
root.closeFullscreen()
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: author.id !== currentRoom.localUser.id
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/imports/NeoChat/Menu/Timeline/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
@@ -85,7 +72,6 @@ MessageDelegateContextMenu {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
root.closeFullscreen()
}
}
]

View File

@@ -48,15 +48,6 @@ Loader {
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.plainMessage : loadRoot.selectedText)
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: author.id !== currentRoom.localUser.id
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/imports/NeoChat/Menu/Timeline/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"

View File

@@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
Kirigami.Page {
id: reportSheet
property var room
property string eventId
title: i18n("Report Message")
QQC2.TextArea {
id: reason
placeholderText: i18n("Reason for reporting this message")
anchors.fill: parent
wrapMode: TextEdit.Wrap
}
footer: QQC2.ToolBar {
QQC2.DialogButtonBox {
anchors.fill: parent
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
reportSheet.room.reportEvent(eventId, reason.text)
reportSheet.closeDialog()
}
}
QQC2.Button {
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: reportSheet.closeDialog()
}
}
}
}

View File

@@ -2,4 +2,3 @@ module NeoChat.Menu.Timeline
MessageDelegateContextMenu 1.0 MessageDelegateContextMenu.qml
FileDelegateContextMenu 1.0 FileDelegateContextMenu.qml
MessageSourceSheet 1.0 MessageSourceSheet.qml
ReportSheet 1.0 ReportSheet.qml

View File

@@ -3,11 +3,18 @@
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.12 as Kirigami
Kirigami.Page {
Kirigami.LoadingPlaceholder {
title: i18n("Loading…")
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
text: i18n("Loading…")
QQC2.BusyIndicator {
running: false
Layout.alignment: Qt.AlignHCenter
}
}
}

View File

@@ -42,17 +42,27 @@ Kirigami.ScrollablePage {
}
}
Connections {
target: SpaceHierarchyCache
function onSpaceHierarchyChanged() {
if (spaceList.activeSpaceId !== '') {
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(spaceList.activeSpaceId, false);
}
}
}
header: QQC2.Control {
contentItem: QQC2.RoundButton {
id: homeButton
flat: true
padding: Kirigami.Units.gridUnit / 2
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
text: i18nc('@action:button', 'Show All Rooms')
display: QQC2.AbstractButton.IconOnly
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
sortFilterRoomListModel.activeSpaceRooms = [];
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
@@ -72,10 +82,13 @@ Kirigami.ScrollablePage {
implicitHeight: ListView.view.headerItem.implicitHeight
contentItem: Kirigami.Avatar {
id: del
actions.main: Kirigami.Action {
id: enterSpaceAction
onTriggered: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(id, true);
}
}
@@ -355,7 +368,7 @@ Kirigami.ScrollablePage {
}
function createRoomListContextMenu() {
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
configButton.visible = true
configButton.down = true
menu.closed.connect(function() {

View File

@@ -8,7 +8,7 @@ import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1 as Platform
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
@@ -105,17 +105,17 @@ Kirigami.ScrollablePage {
function onShowMessage(messageType, message) {
page.header.contentItem.text = message;
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
page.header.visible = true;
page.header.contentItem.visible = true;
}
}
header: QQC2.Control {
height: visible ? implicitHeight : 0
visible: false
visible: contentItem.visible
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
showCloseButton: true
visible: true
visible: false
}
}
@@ -144,10 +144,15 @@ Kirigami.ScrollablePage {
}
}
Kirigami.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
visible: loading
text: i18n("Loading…")
QQC2.BusyIndicator {
running: loadingIndicator.visible
Layout.alignment: Qt.AlignHCenter
}
}
focus: true
@@ -177,15 +182,6 @@ Kirigami.ScrollablePage {
}
}
Connections {
target: currentRoom
function onPositiveMessage(message) {
page.header.contentItem.text = message;
page.header.contentItem.type = Kirigami.MessageType.Positive;
page.header.visible = true;
}
}
// hover actions on a delegate, activated in TimelineContainer.qml
Connections {
target: page.flickable
@@ -207,7 +203,7 @@ Kirigami.ScrollablePage {
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool isLoaded: page.width * page.height > 10
spacing: 0
spacing: Config.compactLayout ? 1 : Kirigami.Units.smallSpacing
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
@@ -430,9 +426,7 @@ Kirigami.ScrollablePage {
headerPositioning: ListView.OverlayHeader
function goToEvent(eventID) {
const index = eventToIndex(eventID)
messageListView.positionViewAtIndex(index, ListView.Center)
itemAtIndex(index).isTemporaryHighlighted = true
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
Item {
@@ -458,9 +452,8 @@ Kirigami.ScrollablePage {
}
property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0)
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 + (delegate.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 : 0 ): 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (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
@@ -476,23 +469,10 @@ Kirigami.ScrollablePage {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
Kirigami.Icon {
source: "security-high"
width: height
height: parent.height
visible: hoverActions.event.verified
HoverHandler {
id: hover
}
QQC2.ToolTip.text: i18n("This message was sent from a verified device")
QQC2.ToolTip.visible: hover.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
QQC2.ToolTip.text: i18n("React")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open();
EmojiDialog {
@@ -506,7 +486,6 @@ Kirigami.ScrollablePage {
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
visible: hoverActions.showEdit
icon.name: "document-edit"
onClicked: {
@@ -519,7 +498,6 @@ Kirigami.ScrollablePage {
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "mail-replied-symbolic"
onClicked: {
chatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
@@ -593,7 +571,7 @@ Kirigami.ScrollablePage {
function warning(title, message) {
page.header.contentItem.text = `${title}<br />${message}`;
page.header.contentItem.type = Kirigami.MessageType.Warning;
page.header.visible = true;
page.header.contentItem.visible = true;
}
function showUserDetail(user) {

View File

@@ -5,7 +5,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
@@ -45,81 +45,10 @@ Kirigami.ScrollablePage {
}
ColumnLayout {
Item {
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
Layout.alignment: Qt.AlignHCenter
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
id: swapper
states: [
State {
when: !LoginHelper.homeserverReachable
name: "idle"
PropertyChanges {
target: icon
opacity: 1
}
PropertyChanges {
target: avi
opacity: 0
}
},
State {
when: LoginHelper.homeserverReachable
name: "showAvi"
PropertyChanges {
target: icon
opacity: 0
}
PropertyChanges {
target: avi
opacity: 1
}
}
]
transitions: [
Transition {
to: "showAvi"
SequentialAnimation {
NumberAnimation { target: icon; properties: "opacity";}
NumberAnimation { target: avi; properties: "opacity";}
}
},
Transition {
from: "showAvi"
SequentialAnimation {
NumberAnimation { target: avi; properties: "opacity";}
NumberAnimation { target: icon; properties: "opacity";}
}
}
]
Kirigami.Icon {
id: icon
source: "org.kde.neochat"
anchors.fill: parent
implicitWidth: height
}
ColumnLayout {
id: avi
opacity: 0
anchors.fill: parent
Kirigami.Avatar {
source: LoginHelper.loginAvatar
name: LoginHelper.loginName
Layout.fillHeight: true
implicitWidth: height
Layout.alignment: Qt.AlignHCenter
}
Controls.Label {
text: LoginHelper.loginName
font.pointSize: 24
Layout.alignment: Qt.AlignHCenter
}
}
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
Controls.Label {
Layout.fillWidth: true
@@ -137,7 +66,6 @@ Kirigami.ScrollablePage {
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
@@ -150,26 +78,16 @@ Kirigami.ScrollablePage {
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
icon.name: "go-back"
}
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
opacity: welcomePage.currentStep.showContinueButton ? 1 : 0
Behavior on opacity { NumberAnimation {} }
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
Kirigami.LoadingPlaceholder {
icon.name: "online"
opacity: LoginHelper.testing ? 1 : 0
text: i18n("Connecting to your homeserver...")
Behavior on opacity { NumberAnimation {} }
Layout.alignment: Qt.AlignHCenter
}
Connections {
target: currentStep

View File

@@ -18,7 +18,7 @@ Kirigami.OverlayDrawer {
id: roomDrawer
readonly property var room: RoomManager.currentRoom
width: actualWidth
width: modal ? undefined : actualWidth
readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
@@ -77,70 +77,48 @@ Kirigami.OverlayDrawer {
sourceComponent: ColumnLayout {
id: columnLayout
property alias userSearchText: userListSearchField.text
property alias highlightedUser: userListView.currentIndex
spacing: 0
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
topPadding: Kirigami.Units.smallSpacing / 2;
bottomPadding: Kirigami.Units.smallSpacing / 2;
rightPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing
RowLayout {
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
spacing: 0
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
level: 1
}
ToolButton {
id: inviteButton
Layout.alignment: Qt.AlignRight
icon.name: "list-add-user"
text: i18n("Invite user to room")
display: AbstractButton.IconOnly
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {room: room})
roomDrawer.close();
}
ToolTip {
text: inviteButton.text
}
}
ToolButton {
id: favouriteButton
Item {
// HACK otherwise rating item is not right aligned
Layout.fillWidth: true
}
ToolButton {
Layout.alignment: Qt.AlignRight
icon.name: room && room.isFavourite ? "rating" : "rating-unrated"
checkable: true
checked: room && room.isFavourite
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
display: AbstractButton.IconOnly
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
ToolTip {
text: favouriteButton.text
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
}
}
ToolButton {
id: settingsButton
Layout.alignment: Qt.AlignRight
icon.name: 'settings-configure'
text: i18n("Room settings")
display: AbstractButton.IconOnly
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
ToolTip {
text: settingsButton.text
text: i18n("Room settings")
}
}
}
@@ -149,11 +127,14 @@ Kirigami.OverlayDrawer {
ColumnLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Heading {
text: i18n("Room information")
level: 3
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
@@ -170,9 +151,10 @@ Kirigami.OverlayDrawer {
spacing: 0
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
type: Kirigami.Heading.Type.Primary
font.bold: true
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
textFormat: Text.PlainText
@@ -183,8 +165,6 @@ Kirigami.OverlayDrawer {
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
readOnly: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
@@ -194,13 +174,11 @@ Kirigami.OverlayDrawer {
TextEdit {
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(https:\/\/[^ ]*)/
readonly property var replaceLinks: /\(https:\/\/[^ ]*\)/
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
@@ -214,23 +192,16 @@ Kirigami.OverlayDrawer {
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
Label {
Layout.alignment: Qt.AlignRight
text: room ? i18np("%1 Member", "%1 Members", room.joinedCount) : i18n("No Member Count")
}
}
Control {
Layout.fillWidth: true
// Note need to set padding individually to guarantee it will always work
// see note - https://doc.qt.io/qt-6/qml-qtquick-controls2-control.html#padding-prop
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
Pane {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
@@ -238,7 +209,6 @@ Kirigami.OverlayDrawer {
}
contentItem: Kirigami.SearchField {
id: userListSearchField
onAccepted: sortedMessageEventModel.filterString = text;
}
}
@@ -253,6 +223,8 @@ Kirigami.OverlayDrawer {
ListView {
id: userListView
clip: true
headerPositioning: ListView.OverlayHeader
boundsBehavior: Flickable.DragOverBounds
activeFocusOnTab: true
model: KSortFilterProxyModel {
@@ -267,53 +239,58 @@ Kirigami.OverlayDrawer {
filterCaseSensitivity: Qt.CaseInsensitive
}
delegate: Kirigami.BasicListItem {
id: userListItem
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
z: 1
label: name
onClicked: {
const popup = userDetailDialog.createObject(ApplicationWindow.overlay, {room: room, user: user, displayName: name, avatarMediaId: avatar})
popup.closed.connect(function() {
userListItem.highlighted = false
})
if (roomDrawer.modal) {
roomDrawer.close()
contentItem: RowLayout {
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
Layout.preferredHeight: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
visible: Config.showAvatarInRoomDrawer
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
source: avatar ? ("image://mxc/" + avatar) : ""
name: name
}
popup.open()
}
leading: Kirigami.Avatar {
implicitWidth: height
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
source: avatar ? ("image://mxc/" + avatar) : ""
name: name
}
Label {
Layout.fillWidth: true
trailing: Label {
visible: perm != UserType.Member
text: name
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
default:
return "";
Label {
visible: perm != UserType.Member
text: {
if (perm == UserType.Owner) {
return i18n("Owner")
}
if (perm == UserType.Admin) {
return i18n("Admin")
}
if (perm == UserType.Moderator) {
return i18n("Mod")
}
if (perm == UserType.Muted) {
return i18n("Muted")
}
return ""
}
color: Kirigami.Theme.disabledTextColor
font.pixelSize: 12
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
action: Kirigami.Action {
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open()
}
}
}
@@ -324,7 +301,6 @@ Kirigami.OverlayDrawer {
onRoomChanged: {
if (loader.active) {
loader.item.userSearchText = ""
loader.item.highlightedUser = -1
}
if (room == null) {
close()

View File

@@ -3,12 +3,12 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
id: root
property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
@@ -30,16 +30,6 @@ Kirigami.CategorizedSettings {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Notifications")
icon.name: "notifications"
page: Qt.resolvedUrl("PushNotification.qml")
initialProperties: {
return {
room: root.room
}
}
}
]
}

View File

@@ -21,7 +21,7 @@ Kirigami.ScrollablePage {
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
title: i18n("General")
title: i18n('General')
ColumnLayout {
Kirigami.FormLayout {

View File

@@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
property var room
title: i18nc('@title:window', 'Notifications')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.RadioButton {
text: i18n("Follow global setting")
Kirigami.FormData.label: i18n("Room notifications setting:")
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Default
}
}
QQC2.RadioButton {
text: i18nc("As in 'notify for all messages'","All")
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.All
}
}
QQC2.RadioButton {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'","@Mentions and Keywords")
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
QQC2.RadioButton {
text: i18nc("As in 'do not notify for any messages'","Off")
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Mute
}
}
}
}
}

View File

@@ -16,13 +16,13 @@ Kirigami.ScrollablePage {
property var room
title: i18n("Security")
title: i18n('Security')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
RadioButton {
CheckBox {
text: i18nc("@option:check", "Private (invite only)")
Kirigami.FormData.label: i18nc("@option:check", "Access:")
checked: room.joinRule === "invite"
@@ -32,7 +32,7 @@ Kirigami.ScrollablePage {
text: i18n("Only invited people can join.")
font: Kirigami.Theme.smallFont
}
RadioButton {
CheckBox {
text: i18nc("@option:check", "Space members")
checked: room.joinRule === "restricted"
enabled: false
@@ -41,7 +41,7 @@ Kirigami.ScrollablePage {
text: i18n("Anyone in a space can find and join.")
font: Kirigami.Theme.smallFont
}
RadioButton {
CheckBox {
text: i18nc("@option:check", "Public")
checked: room.joinRule === "public"
enabled: false

View File

@@ -7,6 +7,6 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.AboutPage {
title: i18nc("@title:window", "About NeoChat")
title: i18nc('@title:window', 'About NeoChat')
aboutData: Controller.aboutData
}

View File

@@ -47,7 +47,7 @@ Kirigami.ScrollablePage {
onTriggered: pageSettingStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
connection: model.connection
}, {
title: i18n("Account editor")
title: i18n('Account editor')
});
}
}

View File

@@ -12,7 +12,7 @@ import org.kde.neochat 1.0
import NeoChat.Settings 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "Appearance")
title: i18nc('@title:window', 'Appearance')
ColumnLayout {
RowLayout {
Layout.alignment: Qt.AlignCenter
@@ -177,7 +177,7 @@ Kirigami.ScrollablePage {
Kirigami.FormLayout {
Layout.maximumWidth: parent.width
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Show Avatar:")
Kirigami.FormData.label: "Show Avatar:"
text: i18n("In Chat")
checked: Config.showAvatarInTimeline
onToggled: {
@@ -237,7 +237,6 @@ Kirigami.ScrollablePage {
HoverHandler { id: sliderHover }
QQC2.ToolTip.visible: sliderHover.hovered && !enabled
QQC2.ToolTip.text: i18n("Only enabled if the transparent chat page is enabled.")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
text: Math.round(Config.transparency * 100) + "%"

View File

@@ -5,7 +5,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
@@ -19,9 +19,13 @@ Kirigami.ScrollablePage {
anchors.fill: parent
Kirigami.LoadingPlaceholder {
Kirigami.PlaceholderMessage {
visible: parent.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
text: i18n("Loading…")
Controls.BusyIndicator {
running: parent.visible
}
}
delegate: Kirigami.BasicListItem {
@@ -41,17 +45,6 @@ Kirigami.ScrollablePage {
}
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
visible: Controller.encryptionSupported
action: Kirigami.Action {
text: i18n("Verify device")
iconName: "security-low-symbolic"
onTriggered: {
devices.connection.startKeyVerificationSession(model.id)
}
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
action: Kirigami.Action {

View File

@@ -16,7 +16,7 @@ import NeoChat.Component 1.0 as Components
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "Custom Emojis")
title: i18nc('@title:window', 'Custom Emojis')
ListView {
anchors.fill: parent

View File

@@ -11,7 +11,7 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18nc("@title:window", "General")
title: i18nc('@title:window', 'General')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
@@ -46,7 +46,6 @@ Kirigami.ScrollablePage {
onToggled: {
Config.showNotifications = checked
Config.save()
NotificationsManager.globalNotificationsEnabled = checked
}
}
QQC2.CheckBox {

View File

@@ -3,6 +3,7 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
@@ -24,7 +25,7 @@ Kirigami.CategorizedSettings {
page: Qt.resolvedUrl("AccountsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Custom Emojis")
text: i18n("Custom Emoji")
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},

View File

@@ -47,7 +47,7 @@ Kirigami.Page {
dialog.close();
}
}
title: i18nc("@window:title", "Spellchecking")
title: i18nc('@window:title', 'Spellchecking')
QQC2.Dialog {
id: applyDialog

View File

@@ -25,7 +25,6 @@
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name>
<name xml:lang="it">NeoChat</name>
<name xml:lang="ka">NeoChat</name>
<name xml:lang="ko">NeoChat</name>
<name xml:lang="nl">NeoChat</name>
<name xml:lang="nn">NeoChat</name>
@@ -57,7 +56,6 @@
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldings­protokollen</summary>
@@ -176,7 +174,6 @@
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="ka">KDE-ის საზოგადოება</developer_name>
<developer_name xml:lang="ko">KDE 커뮤니티</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
@@ -209,9 +206,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="22.09" date="2022-09-27">
<url>https://www.plasma-mobile.org/2022/09/27/plasma-mobile-gear-22-09/</url>
</release>
<release version="22.06" date="2022-06-24">
<url>https://www.plasma-mobile.org/2022/06/28/plasma-mobile-gear-22-06/</url>
<description>

View File

@@ -18,7 +18,6 @@ Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[nl]=NeoChat
@@ -52,7 +51,6 @@ GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programą
GenericName[nl]=Matrix-client
@@ -85,7 +83,6 @@ 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[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[nl]=Client voor het Matrix-protocol

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Page 1.0
import NeoChat.Panel 1.0
import NeoChat.Dialog.KeyVerification 1.0
Kirigami.ApplicationWindow {
id: root
@@ -52,7 +51,7 @@ Kirigami.ApplicationWindow {
Timer {
id: saveWindowGeometryTimer
interval: 1000
onTriggered: Controller.saveWindowGeometry()
onTriggered: Controller.saveWindowGeometry(root)
}
Connections {
@@ -60,7 +59,7 @@ Kirigami.ApplicationWindow {
enabled: false // Disable on startup to avoid writing wrong values if the window is hidden
target: root
function onClosing() { Controller.saveWindowGeometry(); }
function onClosing() { Controller.saveWindowGeometry(root); }
function onWidthChanged() { saveWindowGeometryTimer.restart(); }
function onHeightChanged() { saveWindowGeometryTimer.restart(); }
function onXChanged() { saveWindowGeometryTimer.restart(); }
@@ -107,7 +106,7 @@ Kirigami.ApplicationWindow {
function onOpenRoomInNewWindow(room) {
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
secondayWiroomWindowndow.width = root.width - pageStack.get(0).width;
secondayWindow.width = root.width - pageStack.get(0).width;
secondayWindow.show();
}
@@ -140,6 +139,13 @@ Kirigami.ApplicationWindow {
}
}
function showWindow() {
root.show()
root.raise()
root.requestActivate()
Controller.raiseWindow(root)
}
contextDrawer: RoomDrawer {
id: contextDrawer
modal: !root.wideScreen || !enabled
@@ -229,13 +235,13 @@ Kirigami.ApplicationWindow {
Kirigami.Action {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Start a Chat")
icon.name: "irc-join-channel"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {connection: Controller.activeConnection})
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
},
Kirigami.Action {
@@ -347,6 +353,16 @@ Kirigami.ApplicationWindow {
showPassiveNotification(i18n("%1: %2", error, detail));
}
function onShowWindow(token = null) {
root.showWindow()
if (token && KWindowSystem) {
KWindowSystem.setCurrentXdgActivationToken(basicNotification.xdgActivationToken)
KWindowSystem.activateWindow(root)
} else {
root.raise()
}
}
function onUserConsentRequired(url) {
consentSheet.url = url
consentSheet.open()
@@ -368,23 +384,11 @@ Kirigami.ApplicationWindow {
}
}
Component {
id: keyVerificationDialogComponent
KeyVerificationDialog { }
}
Connections {
target: Controller.activeConnection
function onDirectChatAvailable(directChat) {
RoomManager.enterRoom(Controller.activeConnection.room(directChat.id));
}
function onNewKeyVerificationSession(session) {
applicationWindow().pageStack.pushDialogLayer(keyVerificationDialogComponent, {
session: session,
}, {
title: i18nc("@title:window", "Session Verification")
});
}
}
Kirigami.OverlaySheet {
@@ -449,15 +453,14 @@ Kirigami.ApplicationWindow {
property Item hoverLinkIndicator: QQC2.Control {
parent: overlay.parent
property string text
opacity: linkText.text.length > 0 ? 1 : 0
property alias text: linkText.text
opacity: text.length > 0 ? 1 : 0
z: 20
x: 0
y: parent.height - implicitHeight
contentItem: QQC2.Label {
id: linkText
text: parent.text.startsWith("https://matrix.to/") ? "" : parent.text
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {

10
res.qrc
View File

@@ -15,7 +15,6 @@
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/RoomSettings/General.qml</file>
<file>imports/NeoChat/RoomSettings/Security.qml</file>
<file>imports/NeoChat/RoomSettings/PushNotification.qml</file>
<file>imports/NeoChat/RoomSettings/Categories.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/FullScreenImage.qml</file>
@@ -40,7 +39,6 @@
<file>imports/NeoChat/Component/Timeline/SectionDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/VideoDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ReactionDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/LinkPreviewDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file>
@@ -64,13 +62,6 @@
<file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file>
<file>imports/NeoChat/Dialog/EmojiDialog.qml</file>
<file>imports/NeoChat/Dialog/OpenFileDialog.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/KeyVerificationDialog.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/Message.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/EmojiItem.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/EmojiRow.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/EmojiSas.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/VerificationCanceled.qml</file>
<file>imports/NeoChat/Dialog/KeyVerification/qmldir</file>
<file>imports/NeoChat/Menu/qmldir</file>
<file>imports/NeoChat/Menu/GlobalMenu.qml</file>
<file>imports/NeoChat/Menu/EditMenu.qml</file>
@@ -78,7 +69,6 @@
<file>imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml</file>
<file>imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml</file>
<file>imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml</file>
<file>imports/NeoChat/Menu/Timeline/ReportSheet.qml</file>
<file>imports/NeoChat/Menu/RoomListContextMenu.qml</file>
<file>qtquickcontrols2.conf</file>
<file>imports/NeoChat/Component/glowdot.png</file>

View File

@@ -38,8 +38,6 @@ add_library(neochat STATIC
joinrulesevent.cpp
collapsestateproxymodel.cpp
urlhelper.cpp
windowcontroller.cpp
linkpreviewer.cpp
)
add_executable(neochat-app
@@ -140,20 +138,13 @@ if(ANDROID)
"org.kde.neochat"
"preferences-system-users"
"preferences-desktop-theme-global"
"notifications"
"zoom-in"
"zoom-out"
"image-rotate-left-symbolic"
"image-rotate-right-symbolic"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PUBLIC KF5::DBusAddons)
@@ -164,7 +155,7 @@ if (TARGET KF5::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(TARGETS neochat-app ${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)

View File

@@ -2,13 +2,17 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "actionshandler.h"
#include "controller.h"
#include <csapi/joining.h>
#include <events/roommessageevent.h>
#include <KLocalizedString>
#include <QDebug>
#include <QStringBuilder>
#include "controller.h"
#include "customemojimodel.h"
#include "neochatroom.h"
#include "roommanager.h"
ActionsHandler::ActionsHandler(QObject *parent)

View File

@@ -5,12 +5,11 @@
#include <QObject>
namespace Quotient
{
class Connection;
}
#include "connection.h"
#include "neochatroom.h"
using namespace Quotient;
class NeoChatRoom;
class CustomEmojiModel;
/// \brief Handles user interactions with NeoChat (joining room, creating room,
@@ -20,7 +19,7 @@ class ActionsHandler : public QObject
Q_OBJECT
/// \brief The connection that will handle sending the message.
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/// \brief The connection that will handle sending the message.
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
@@ -35,8 +34,8 @@ public:
explicit ActionsHandler(QObject *parent = nullptr);
~ActionsHandler();
[[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
[[nodiscard]] Connection *connection() const;
void setConnection(Connection *connection);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -69,6 +68,6 @@ public Q_SLOTS:
void postEdit(const QString &text);
private:
Quotient::Connection *m_connection = nullptr;
Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
};

View File

@@ -3,10 +3,6 @@
#include "blurhash.h"
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
namespace

View File

@@ -3,7 +3,11 @@
#pragma once
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels);

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "chatboxhelper.h"
#include <QDebug>
ChatBoxHelper::ChatBoxHelper(QObject *parent)
: QObject(parent)

View File

@@ -159,7 +159,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo(bool isAutocompleting)
};
}
if (autoCompletePrefix.startsWith("/") && text.trimmed().length() <= 1) {
if (autoCompletePrefix.startsWith("/")) {
return QVariantMap{
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::Command},

View File

@@ -3,11 +3,13 @@
#pragma once
#include <QFont>
#include <QObject>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QUrl>
class QTextDocument;
class QQuickTextDocument;
class NeoChatRoom;
class Controller;

View File

@@ -8,7 +8,6 @@
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
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)

Some files were not shown because too many files have changed in this diff Show More