Compare commits

..

1 Commits

Author SHA1 Message Date
Tobias Fella
c1a36de9a2 Port to bottom navbar 2021-10-14 21:46:33 +02:00
77 changed files with 1375 additions and 1624 deletions

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@ neochat.kdev4
compile_commands.json
.cache/
.vscode/
kate.project.ctags.*

View File

@@ -1,7 +0,0 @@
# SPDX-FileCopyrightText: none
# SPDX-License-Identifier: CC0-1.0
include:
- https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-reuse.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml

View File

@@ -1,25 +0,0 @@
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/sonnet': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'

View File

@@ -7,10 +7,9 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "21.12")
set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.2")
set(KF5_MIN_VERSION "5.86.0")
set(QT_MIN_VERSION "5.15.0")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -39,7 +38,7 @@ endif()
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
cmake_policy(SET CMP0063 OLD)
ecm_setup_version(${PROJECT_VERSION}
ecm_setup_version(1.2.80
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
@@ -115,10 +114,6 @@ find_package(QCoro REQUIRED)
qcoro_enable_coroutines()
if(ANDROID)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()
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)

View File

@@ -1,11 +0,0 @@
Copyright (c) <year> <owner>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -6,8 +6,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.neochat"
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:versionName="0.0.1"
android:versionCode="1604412458"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"

View File

@@ -1,96 +0,0 @@
/*
SPDX-FileCopyrightText: 2018-2020 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-FileCopyrightText: 2020 Gabriel Souza Franco <gabrielfrancosouza@gmail.com>
SPDX-License-Identifier: BSD-3-Clause
*/
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.4'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.application'
apply from: '../version.gradle'
def timestamp = (int)(new Date().getTime()/1000)
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
defaultConfig {
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
}
packagingOptions {
exclude 'lib/*/*RemoteObjects*'
exclude 'lib/*/*StateMachine*'
exclude 'lib/*/*_imageformats_qico_*'
exclude 'lib/*/*_imageformats_qicns_*'
exclude 'lib/*/*_imageformats_qtga_*'
exclude 'lib/*/*_imageformats_qtiff_*'
exclude 'lib/*/*_qmltooling_*'
}
aaptOptions {
// different syntax than above
// see https://android.googlesource.com/platform/frameworks/base/+/refs/heads/pie-release/tools/aapt2/util/Files.h#90
ignoreAssetsPattern '!<dir>ECM:!<dir>aclocal:!<dir>doc:!<dir>gtk-doc:!<dir>iso-codes:!<dir>man:!<dir>mime:!<dir>pkgconfig:!<dir>qlogging-categories5:!<file>iso_15924.mo:!<file>iso_3166-2.mo:!<file>iso_3166-3.mo:!<file>iso_4217.mo:!<file>iso_639-2.mo:!<file>iso_639-3.mo:!<file>iso_639-5.mo:!<file>kcodecs5_qt.qm:!<file>kde5_xml_mimetypes.qm'
}
}

View File

@@ -1,6 +0,0 @@
// SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: BSD-3-Clause
ext {
projectVersionFull = "@NEOCHAT_VERSION@"
}

View File

@@ -9,7 +9,8 @@ import QtQuick.Templates 2.15 as T
import Qt.labs.platform 1.1 as Platform
import QtQuick.Window 2.15
import org.kde.kirigami 2.18 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
ToolBar {
@@ -68,7 +69,7 @@ ToolBar {
font: inputField.font
}
TextArea {
T.TextArea {
id: inputField
focus: true
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
@@ -100,9 +101,16 @@ ToolBar {
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption
palette: Kirigami.Theme.palette
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.SpellChecking.enabled: true
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding)
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding)
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
@@ -111,6 +119,65 @@ ToolBar {
selectByMouse: !Kirigami.Settings.tabletMode
cursorDelegate: Loader {
visible: inputField.activeFocus && !inputField.readOnly && inputField.selectionStart === inputField.selectionEnd
active: visible
sourceComponent: CursorDelegate { target: inputField }
}
CursorHandle {
id: selectionStartHandle
target: inputField
}
CursorHandle {
id: selectionEndHandle
target: inputField
isSelectionEnd: true
}
TapHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
acceptedButtons: Qt.LeftButton | Qt.RightButton
// unfortunately, taphandler's pressed event only triggers when the press is lifted
// we need to use the longpress signal since it triggers when the button is first pressed
longPressThreshold: 0
onLongPressed: TextFieldContextMenu.targetClick(point, inputField, spellcheckhighlighter, inputField.positionAt(point.position.x, point.position.y));
}
onPressAndHold: {
if (!Kirigami.Settings.tabletMode) {
return;
}
forceActiveFocus();
cursorPosition = positionAt(event.x, event.y);
selectWord();
}
onFocusChanged: {
if (focus) {
MobileTextActionsToolBar.controlRoot = inputField;
}
}
Label {
id: placeholder
x: inputField.leftPadding
y: inputField.topPadding
width: inputField.width - (inputField.leftPadding + inputField.rightPadding)
height: inputField.height - (inputField.topPadding + inputField.bottomPadding)
text: inputField.placeholderText
font: inputField.font
color: Kirigami.Theme.disabledTextColor
horizontalAlignment: inputField.horizontalAlignment
verticalAlignment: inputField.verticalAlignment
visible: !inputField.length && !inputField.preeditText && (!inputField.activeFocus || inputField.horizontalAlignment !== Qt.AlignHCenter)
wrapMode: Text.Wrap
}
ChatDocumentHandler {
id: documentHandler
document: inputField.textDocument
@@ -120,6 +187,18 @@ ToolBar {
room: currentRoom ?? null
}
SpellcheckHighlighter {
id: spellcheckhighlighter
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
onChangeCursorPosition: {
inputField.cursorPosition = start;
inputField.moveCursorSelection(end, TextEdit.SelectCharacters);
}
}
Timer {
id: timeoutTimer
repeat: false
@@ -160,6 +239,9 @@ ToolBar {
}
Keys.onPressed: {
// trigger if context menu button is pressed
TextFieldContextMenu.targetKeyPressed(event, inputField)
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
@@ -243,7 +325,10 @@ ToolBar {
chatBar.complete();
}
onPressed: MobileTextActionsToolBar.shouldBeVisible = true;
onTextChanged: {
MobileTextActionsToolBar.shouldBeVisible = false;
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text

View File

@@ -86,7 +86,6 @@ Popup {
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
}
labelItem.textFormat: Text.PlainText
text: modelData.displayName
onClicked: completeTriggered();
}

View File

@@ -0,0 +1,65 @@
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Templates 2.15
import org.kde.kirigami 2.14 as Kirigami
Item {
id: root
property alias target: root.parent
Rectangle {
id: cursorLine
property real previousX: 0
property real previousY: 0
parent: target
implicitWidth: target.cursorRectangle.width
implicitHeight: target.cursorRectangle.height
x: Math.floor(target.cursorRectangle.x)
y: Math.floor(target.cursorRectangle.y)
color: target.color
SequentialAnimation {
id: blinkAnimation
running: root.visible && Qt.styleHints.cursorFlashTime != 0 && target.selectionStart === target.selectionEnd
PropertyAction {
target: cursorLine
property: "opacity"
value: 1
}
PauseAnimation {
duration: Qt.styleHints.cursorFlashTime/2
}
SequentialAnimation {
loops: Animation.Infinite
OpacityAnimator {
target: cursorLine
from: 1
to: 0
duration: Qt.styleHints.cursorFlashTime/2
easing.type: Easing.OutCubic
}
OpacityAnimator {
target: cursorLine
from: 0
to: 1
duration: Qt.styleHints.cursorFlashTime/2
easing.type: Easing.OutCubic
}
}
}
}
Connections {
target: root.target
function onCursorPositionChanged() {
blinkAnimation.restart()
}
}
}

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Templates 2.15
import org.kde.kirigami 2.14 as Kirigami
Loader {
id: root
property Item target
property bool isSelectionEnd: false
visible: Kirigami.Settings.tabletMode && target.activeFocus && (isSelectionEnd ? target.selectionStart !== target.selectionEnd : true)
active: visible
sourceComponent: Kirigami.ShadowedRectangle {
id: handle
property real selectionStartX: Math.floor(Qt.inputMethod.anchorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
property real selectionStartY: Math.floor(Qt.inputMethod.anchorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
property real selectionEndX: Math.floor(Qt.inputMethod.cursorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
property real selectionEndY: Math.floor(Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
property real pointyBitVerticalOffset: Math.abs(pointyBit.y*2)
parent: Overlay.overlay
x: isSelectionEnd ? selectionEndX : selectionStartX
y: isSelectionEnd ? selectionEndY : selectionStartY
// HACK: make it appear above most popups that show up in the
// overlay in case any of them use TextField or TextArea
z: 999
//opacity: target.activeFocus ? 1 : 0
implicitHeight: {
let h = Kirigami.Units.gridUnit
return h - (h % 2 == 0 ? 1 : 0)
}
implicitWidth: implicitHeight
radius: width/2
color: target.selectionColor
shadow {
color: Qt.rgba(0,0,0,0.2)
size: 3
yOffset: 1
}
Rectangle {
id: pointyBit
x: (parent.width - width)/2
y: -height/4 + 0.2 // magic number to get it to line up with the edge of the circle
implicitHeight: parent.implicitHeight/2
implicitWidth: implicitHeight
antialiasing: true
rotation: 45
color: parent.color
}
Kirigami.ShadowedRectangle {
id: inner
visible: target.selectionStart !== target.selectionEnd && (handle.y < selectionStartY || handle.y < selectionEndY)
anchors.fill: parent
anchors.margins: Kirigami.Units.smallBorder
color: target.selectedTextColor
radius: height/2
Rectangle {
id: innerPointyBit
x: (parent.width - width)/2
y: -height/4 + 0.8 // magic number to get it to line up with the edge of the circle
implicitHeight: pointyBit.implicitHeight
implicitWidth: implicitHeight
antialiasing: true
rotation: 45
color: parent.color
}
}
MouseArea {
enabled: handle.visible
anchors.fill: parent
// preventStealing: true
onPositionChanged: {
let pos = mapToItem(root.target, mouse.x, mouse.y);
pos = root.target.positionAt(pos.x, pos.y - handle.height - handle.pointyBitVerticalOffset);
if (target.selectionStart !== target.selectionEnd) {
if (!isSelectionEnd) {
root.target.select(Math.min(pos, root.target.selectionEnd - 1), root.target.selectionEnd);
} else {
root.target.select(root.target.selectionStart, Math.max(pos, root.target.selectionStart + 1));
}
} else {
root.target.cursorPosition = pos;
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
/*
SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
pragma Singleton
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.5 as Kirigami
Popup {
id: root
property Item controlRoot
parent: controlRoot ? controlRoot.Window.contentItem : undefined
modal: false
focus: false
closePolicy: Popup.NoAutoClose
property bool shouldBeVisible: false
x: {
if (!controlRoot || !controlRoot.Window.contentItem) {
return 0;
}
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, controlRoot.positionToRectangle(controlRoot.selectionStart).x, 0).x - root.width/2), controlRoot.Window.contentItem.width - root.width);
}
y: {
if (!controlRoot || !controlRoot.Window.contentItem) {
return 0;
}
var desiredY = controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionStart).y).y - root.height;
if (desiredY >= 0) {
return Math.min(desiredY, controlRoot.Window.contentItem.height - root.height);
} else {
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionEnd).y + Math.round(Kirigami.Units.gridUnit*1.5)).y), controlRoot.Window.contentItem.height - root.height);
}
}
visible: controlRoot ? shouldBeVisible && Qt.platform.os !== "android" && Kirigami.Settings.tabletMode && (controlRoot.selectedText.length > 0 || controlRoot.canPaste) : false
width: contentItem.implicitWidth + leftPadding + rightPadding
contentItem: RowLayout {
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-cut"
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
onClicked: {
controlRoot.cut();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-copy"
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
onClicked: {
controlRoot.copy();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-paste"
visible: controlRoot && controlRoot.canPaste
onClicked: {
controlRoot.paste();
}
}
}
}

View File

@@ -0,0 +1,259 @@
/*
SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com>
SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
pragma Singleton
import QtQuick 2.6
import QtQml 2.2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.5 as Kirigami
Menu {
id: contextMenu
property Item target
property bool deselectWhenMenuClosed: true
property int restoredCursorPosition: 0
property int restoredSelectionStart
property int restoredSelectionEnd
property bool persistentSelectionSetting
property var spellcheckhighlighter: null
property var suggestions: ([])
Component.onCompleted: persistentSelectionSetting = persistentSelectionSetting // break binding
property var runOnMenuClose
parent: Overlay.overlay
function storeCursorAndSelection() {
contextMenu.restoredCursorPosition = target.cursorPosition;
contextMenu.restoredSelectionStart = target.selectionStart;
contextMenu.restoredSelectionEnd = target.selectionEnd;
}
// target is pressed with mouse
function targetClick(handlerPoint, newTarget, spellcheckhighlighter, mousePosition) {
if (handlerPoint.pressedButtons === Qt.RightButton) { // only accept just right click
if (contextMenu.visible) {
deselectWhenMenuClosed = false; // don't deselect text if menu closed by right click on textfield
dismiss();
} else {
contextMenu.target = newTarget;
contextMenu.target.persistentSelection = true; // persist selection when menu is opened
contextMenu.spellcheckhighlighter = spellcheckhighlighter
contextMenu.suggestions = spellcheckhighlighter.suggestions(mousePosition);
storeCursorAndSelection();
popup(contextMenu.target);
// slightly locate context menu away from mouse so no item is selected when menu is opened
x += 1
y += 1
}
} else {
dismiss();
}
}
// context menu keyboard key
function targetKeyPressed(event, newTarget) {
if (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Menu) {
contextMenu.target = newTarget;
target.persistentSelection = true; // persist selection when menu is opened
storeCursorAndSelection();
popup(contextMenu.target);
}
}
readonly property bool targetIsPassword: target !== null && (target.echoMode === TextInput.PasswordEchoOnEdit || target.echoMode === TextInput.Password)
onAboutToShow: {
if (Overlay.overlay) {
let tempZ = 0
for (let i in Overlay.overlay.visibleChildren) {
tempZ = Math.max(tempZ, Overlay.overlay.visibleChildren[i].z)
}
z = tempZ + 1
}
}
// deal with whether or not text should be deselected
onClosed: {
// restore text field's original persistent selection setting
target.persistentSelection = persistentSelectionSetting
// deselect text field text if menu is closed not because of a right click on the text field
if (deselectWhenMenuClosed) {
target.deselect();
}
deselectWhenMenuClosed = true;
// restore cursor position
target.forceActiveFocus();
target.cursorPosition = restoredCursorPosition;
target.select(restoredSelectionStart, restoredSelectionEnd);
// run action
runOnMenuClose();
}
onOpened: {
runOnMenuClose = function() {};
}
Instantiator {
active: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
model: suggestions
delegate: MenuItem {
text: modelData
onClicked: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {
spellcheckhighlighter.replaceWord(modelData);
};
}
}
onObjectAdded: contextMenu.insertItem(0, object)
onObjectRemoved: contextMenu.removeItem(0)
}
MenuItem {
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled && suggestions.length === 0
action: Action {
text: spellcheckhighlighter ? i18nc("@action:inmenu", "No suggestions for %1", spellcheckhighlighter.wordUnderMouse) : ""
enabled: false
}
}
MenuSeparator {
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
}
MenuItem {
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
action: Action {
text: i18n("Add to dictionary")
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {
spellcheckhighlighter.addWordToDictionary(spellcheckhighlighter.wordUnderMouse)
};
}
}
}
MenuItem {
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
action: Action {
text: i18n("Ignore")
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {
spellcheckhighlighter.ignoreWord(spellcheckhighlighter.wordUnderMouse)
};
}
}
}
MenuSeparator {
visible: target !== null && !target.readOnly && spellcheckhighlighter !== null && spellcheckhighlighter.wordIsMisspelled
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-undo-symbolic"
text: i18nc("@action:inmenu", "Undo")
shortcut: StandardKey.Undo
}
enabled: target !== null && target.canUndo
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.undo()};
}
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-redo-symbolic"
text: i18nc("@action:inmenu", "Redo")
shortcut: StandardKey.Redo
}
enabled: target !== null && target.canRedo
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.redo()};
}
}
MenuSeparator {
visible: target !== null && !target.readOnly
}
MenuItem {
visible: target !== null && !target.readOnly && !targetIsPassword
action: Action {
icon.name: "edit-cut-symbolic"
text: i18nc("@action:inmenu", "Cut")
shortcut: StandardKey.Cut
}
enabled: target !== null && target.selectedText
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.cut()}
}
}
MenuItem {
action: Action {
icon.name: "edit-copy-symbolic"
text: i18nc("@action:inmenu", "Copy")
shortcut: StandardKey.Copy
}
enabled: target !== null && target.selectedText
visible: !targetIsPassword
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.copy()}
}
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-paste-symbolic"
text: i18nc("@action:inmenu", "Paste")
shortcut: StandardKey.Paste
}
enabled: target !== null && target.canPaste
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.paste()};
}
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-delete-symbolic"
text: i18nc("@action:inmenu", "Delete")
shortcut: StandardKey.Delete
}
enabled: target !== null && target.selectedText
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.remove(target.selectionStart, target.selectionEnd)};
}
}
MenuSeparator {
visible: !targetIsPassword
}
MenuItem {
action: Action {
icon.name: "edit-select-all-symbolic"
text: i18nc("@action:inmenu", "Select All")
shortcut: StandardKey.SelectAll
}
visible: !targetIsPassword
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = function() {target.selectAll()};
}
}
}

View File

@@ -5,3 +5,7 @@ ReplyPane 1.0 ReplyPane.qml
AttachmentPane 1.0 AttachmentPane.qml
CompletionMenu 1.0 CompletionMenu.qml
EmojiPickerPane 1.0 EmojiPickerPane.qml
singleton TextFieldContextMenu 1.0 TextFieldContextMenu.qml
CursorDelegate 1.0 CursorDelegate.qml
CursorHandle 1.0 CursorHandle.qml
singleton MobileTextActionsToolBar 1.0 MobileTextActionsToolBar.qml

View File

@@ -16,6 +16,7 @@ ApplicationWindow {
property int imageHeight: -1
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("Image View - %1", filename)

View File

@@ -79,7 +79,6 @@ MouseArea {
id: replyText
textMessage: reply.display
textFormat: Text.RichText
hasContextMenu: false
width: Math.min(implicitWidth, bubbleMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}

View File

@@ -13,8 +13,6 @@ import NeoChat.Dialog 1.0
RowLayout {
id: row
height: label.contentHeight
Kirigami.Avatar {
id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
@@ -38,7 +36,6 @@ RowLayout {
}
Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.preferredHeight: icon.height

View File

@@ -17,17 +17,8 @@ TextEdit {
property bool isEmote: false
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
property bool hasContextMenu: true
signal requestOpenMessageContext()
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
Layout.fillWidth: Config.compactLayout
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
text: "<style>
table {
width:100%;
@@ -62,6 +53,8 @@ a{
wrapMode: Text.Wrap
textFormat: Text.RichText
Layout.fillWidth: true
onLinkActivated: RoomManager.openResource(link)
onHoveredLinkChanged: if (hoveredLink.length > 0) {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
@@ -77,16 +70,4 @@ a{
enabled: !parent.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(model, parent.selectedText)
enabled: hasContextMenu
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: requestOpenMessageContext()
enabled: hasContextMenu
}
}

View File

@@ -90,7 +90,6 @@ Loader {
id: typingLabel
elide: Text.ElideRight
text: root.labelText
textFormat: Text.PlainText
}
}

View File

@@ -14,7 +14,9 @@ Kirigami.OverlaySheet {
parent: applicationWindow().overlay
title: i18n("Create a Room")
header: Kirigami.Heading {
text: i18n("Create a Room")
}
contentItem: Kirigami.FormLayout {
TextField {

View File

@@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
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
import NeoChat.Component 1.0
Kirigami.OverlaySheet {
id: root
property var room
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
readonly property bool canChangeName: room.canSendState("m.room.name")
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
parent: applicationWindow().overlay
header: Kirigami.Heading {
text: i18nc("%1 is the room name", "Room Settings - %1", room.displayName)
elide: Text.ElideRight
}
contentItem: ColumnLayout {
RowLayout {
Layout.fillWidth: true
spacing: 16
Kirigami.Avatar {
Layout.preferredWidth: 72
Layout.preferredHeight: 72
Layout.alignment: Qt.AlignTop
name: room.name
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
MouseArea {
anchors.fill: parent
enabled: canChangeAvatar
onClicked: {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
room.changeAvatar(path)
})
fileDialog.open()
}
}
}
Kirigami.FormLayout {
Layout.fillWidth: true
TextField {
id: roomNameField
text: room.name
Kirigami.FormData.label: i18n("Room Name:")
enabled: canChangeName
}
TextArea {
id: roomTopicField
Layout.fillWidth: true
text: room.topic
Kirigami.FormData.label: i18n("Room topic:")
enabled: canChangeTopic
}
Button {
Layout.alignment: Qt.AlignRight
visible: canChangeName || canChangeTopic
text: i18n("Save")
highlighted: true
onClicked: {
if (room.name != roomNameField.text) {
room.setName(roomNameField.text)
}
if (room.topic != roomTopicField.text) {
room.setTopic(roomTopicField.text)
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: canonicalAliasComboBox.visible || altAlias.visible
}
ComboBox {
id: canonicalAliasComboBox
visible: room.aliases && room.aliases.length
Kirigami.FormData.label: i18n("Canonical Alias:")
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
enabled: canChangeCanonicalAlias
model: room.aliases
currentIndex: room.aliases.indexOf(room.canonicalAlias)
onCurrentIndexChanged: {
if (room.canonicalAlias != room.aliases[currentIndex]) {
room.setCanonicalAlias(room.aliases[currentIndex])
}
}
}
RowLayout {
id: altAlias
Kirigami.FormData.label: i18n("Other Aliases:")
Layout.fillWidth: true
visible: room.altAliases && room.altAliases.length
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: room.altAliases
delegate: RowLayout {
Layout.maximumWidth: parent.width
Label {
text: modelData
}
ToolButton {
icon.name: ""
onClicked: room.removeLocalAlias(modelData)
}
}
}
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: next.visible || prev.visible
}
Control {
id: next
Layout.fillWidth: true
visible: room.predecessorId && room.connection.room(room.predecessorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room continues another conversation.")
actions: Kirigami.Action {
text: i18n("See older messages...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
root.close()
}
}
}
}
Control {
id: prev
Layout.fillWidth: true
visible: room.successorId && room.connection.room(room.successorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room has been replaced.")
actions: Kirigami.Action {
text: i18n("See new room...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
root.close()
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
}
}

View File

@@ -27,7 +27,13 @@ Kirigami.OverlaySheet {
rightPadding: 0
topPadding: 0
title: i18nc("@title:menu Account detail dialog", "Account detail")
header: Kirigami.Heading {
id: heading
text: i18nc("@title:menu Account detail dialog", "Account detail")
elide: Text.ElideRight
QQC2.ToolTip.visible: truncated && hovered
QQC2.ToolTip.text: text
}
contentItem: ColumnLayout {
spacing: 0

View File

@@ -1,4 +1,5 @@
module NeoChat.Dialog
RoomSettingsDialog 1.0 RoomSettingsDialog.qml
UserDetailDialog 1.0 UserDetailDialog.qml
LoginDialog 1.0 LoginDialog.qml
CreateRoomDialog 1.0 CreateRoomDialog.qml

View File

@@ -28,7 +28,7 @@ Labs.MenuBar {
text: i18nc("menu", "Preferences…")
shortcut: StandardKey.Preferences
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
}
Labs.MenuItem {
text: i18nc("menu", "Quit NeoChat")

View File

@@ -5,12 +5,9 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Page 1.0
/**
* Context menu when clicking on a room in the room list
*/
@@ -19,15 +16,11 @@ Menu {
property var room
MenuItem {
id: newWindow
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
MenuSeparator {
visible: newWindow.visible
}
MenuSeparator {}
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
@@ -47,18 +40,6 @@ Menu {
onTriggered: room.markAllMessagesAsRead()
}
MenuItem {
text: i18nc("@action:inmenu", "Copy address to clipboard")
onTriggered: {
if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
Clipboard.saveText(room.canonicalAlias)
}
}
}
MenuSeparator {}
MenuItem {

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
/**
* Context menu when clicking on a room in the room list
*/
Menu {
id: root
property var selectedText
Repeater {
model: WebShortcutModel {
selectedText: root.selectedText
}
delegate: MenuItem {
text: model.display
icon.name: model.decoration
}
}
MenuSeparator {}
onClosed: destroy()
}

View File

@@ -65,12 +65,7 @@ MessageDelegateContextMenu {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: root.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
messageSourceSheet.createObject(root, {'sourceText': root.source}).open();
}
}
]

View File

@@ -43,18 +43,13 @@ Loader {
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.message : loadRoot.selectedText)
onTriggered: Clipboard.saveText(message)
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: {
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml', {
sourceText: loadRoot.source
}, {
title: i18n("Message Source"),
width: Kirigami.Units.gridUnit * 25
});
messageSourceSheet.createObject(page, {'sourceText': loadRoot.source}).open();
}
}
]

View File

@@ -7,38 +7,25 @@ import QtQuick.Controls 2.15
import org.kde.syntaxhighlighting 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
Kirigami.OverlaySheet {
property string sourceText
topPadding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
header: Kirigami.Heading {
text: i18n("Message Source")
}
title: i18n("Message Source")
TextArea {
id: sourceTextArea
text: sourceText
readOnly: true
wrapMode: Text.WordWrap
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
TextArea {
id: sourceTextArea
text: sourceText
readOnly: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
}
SyntaxHighlighter {
textEdit: sourceTextArea
definition: "JSON"
repository: Repository
}
SyntaxHighlighter {
textEdit: sourceTextArea
repository: Repository
definition: "JSON"
}
}
}

View File

@@ -42,7 +42,7 @@ Kirigami.Page {
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
ListView {
clip: true
model: AccountRegistry
model: AccountListModel { }
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
@@ -50,10 +50,10 @@ Kirigami.Page {
anchors.top: parent.top
anchors.bottom: parent.bottom
text: model.connection.localUser.displayName
text: model.user.displayName
labelItem.textFormat: Text.PlainText
subtitle: model.connection.localUserId
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
subtitle: model.user.id
icon: model.user.avatarMediaId ? ("image://mxc/" + model.user.avatarMediaId) : "im-user"
onClicked: {
Controller.activeConnection = model.connection
@@ -133,7 +133,9 @@ Kirigami.Page {
property var connection
title: i18n("Edit Account")
header: Kirigami.Heading {
text: i18n("Edit Account")
}
Kirigami.FormLayout {
RowLayout {

View File

@@ -82,7 +82,9 @@ Kirigami.Page {
property var index
title: i18n("Remove device")
header: Kirigami.Heading {
text: i18n("Remove device")
}
Kirigami.FormLayout {
Controls.TextField {
id: passwordField
@@ -105,7 +107,9 @@ Kirigami.Page {
property int index
property string name
title: i18n("Edit device")
header: Kirigami.Heading {
text: i18n("Edit device")
}
Kirigami.FormLayout {
Controls.TextField {
id: nameField

View File

@@ -1,20 +1,13 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.12 as Kirigami
Kirigami.Page {
title: i18n("Loading")
Kirigami.PlaceholderMessage {
id: loadingIndicator
QQC2.BusyIndicator {
anchors.centerIn: parent
text: i18n("Loading")
QQC2.BusyIndicator {
running: loadingIndicator.visible
Layout.alignment: Qt.AlignHCenter
}
}
}

View File

@@ -46,6 +46,11 @@ Kirigami.ScrollablePage {
}
}
actions.main: Kirigami.Action {
text: i18n("New room")
icon.name: "list-add"
}
function goToNextRoom() {
do {
listView.incrementCurrentIndex();
@@ -70,8 +75,7 @@ Kirigami.ScrollablePage {
Component {
id: searchField
Kirigami.SearchField {
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillHeight: true
Layout.fillWidth: true
onTextChanged: sortFilterRoomListModel.filterText = text
@@ -271,6 +275,7 @@ Kirigami.ScrollablePage {
id: configButton
visible: roomListItem.hovered || Kirigami.Settings.isMobile
Accessible.name: i18n("Configure room")
flat: true
action: Kirigami.Action {
id: optionAction
@@ -303,14 +308,16 @@ Kirigami.ScrollablePage {
spacing: 0
Repeater {
id: accountList
model: AccountRegistry
model: AccountListModel {
id: accountListModel
}
delegate: Kirigami.BasicListItem {
checkable: true
checked: Controller.activeConnection && Controller.activeConnection.localUserId === model.connection.localUserId
checked: Controller.activeConnection && Controller.activeConnection.localUser.id === model.user.id
onClicked: Controller.activeConnection = model.connection
Layout.fillWidth: true
Layout.fillHeight: true
text: model.connection.localUserId
text: model.user.id
}
}
}

View File

@@ -28,8 +28,6 @@ Kirigami.ScrollablePage {
title: currentRoom.htmlSafeDisplayName
KeyNavigation.left: pageStack.get(0)
Connections {
target: RoomManager
function onCurrentRoomChanged() {
@@ -231,12 +229,14 @@ Kirigami.ScrollablePage {
ListView {
id: messageListView
pixelAligned: true
visible: !invitation.visible
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool isLoaded: page.width * page.height > 10
spacing: Config.compactLayout ? 1 : Kirigami.Units.smallSpacing
reuseItems: true
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
@@ -355,7 +355,6 @@ Kirigami.ScrollablePage {
leftPadding: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
topPadding: 0
bottomPadding: 0
height: contentItem.height
contentItem: StateDelegate { }
implicitWidth: messageListView.width - Kirigami.Units.largeSpacing
}
@@ -373,8 +372,18 @@ Kirigami.ScrollablePage {
innerObject: TextDelegate {
isEmote: true
Layout.fillWidth: Config.compactLayout
Layout.maximumWidth: emoteContainer.bubbleMaxWidth
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
}
}
}
}
@@ -389,8 +398,18 @@ Kirigami.ScrollablePage {
hoverComponent: hoverActions
innerObject: TextDelegate {
Layout.fillWidth: Config.compactLayout
Layout.maximumWidth: messageContainer.bubbleMaxWidth
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText)
}
}
}
}
@@ -405,8 +424,9 @@ Kirigami.ScrollablePage {
innerObject: TextDelegate {
Layout.fillWidth: !Config.compactLayout
hasContextMenu: false
Layout.maximumWidth: noticeContainer.bubbleMaxWidth
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
}
}
}
@@ -427,19 +447,13 @@ Kirigami.ScrollablePage {
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
onTapped: {
fullScreenImage.createObject(parent, {
filename: eventId,
localPath: currentRoom.urlToDownload(eventId),
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h
}).showFullScreen();
fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId), "blurhash": model.content.info["xyz.amorgan.blurhash"], "imageWidth": content.info.w, "imageHeight": content.info.h}).showFullScreen()
}
}
}
@@ -478,11 +492,11 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: audioContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -506,11 +520,11 @@ Kirigami.ScrollablePage {
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -529,11 +543,11 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: fileContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
onTapped: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -713,6 +727,12 @@ Kirigami.ScrollablePage {
MessageDelegateContextMenu {}
}
Component {
id: messageSourceSheet
MessageSourceSheet {}
}
Component {
id: fileDelegateContextMenu
@@ -867,28 +887,28 @@ Kirigami.ScrollablePage {
}
/// Open message context dialog for file and videos
function openFileContext(event, file) {
function openFileContext(author, message, eventId, source, progressInfo, file) {
const contextMenu = fileDelegateContextMenu.createObject(page, {
author: event.author,
message: event.message,
eventId: event.eventId,
source: event.toolTip,
author: author,
message: message,
eventId: eventId,
source: source,
file: file,
progressInfo: event.progressInfo,
progressInfo: progressInfo,
});
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(event, selectedText) {
function openMessageContext(author, message, eventId, source, eventType, formattedBody, selectedText) {
const contextMenu = messageDelegateContextMenu.createObject(page, {
selectedText: selectedText,
author: event.author,
message: event.message,
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.toolTip,
eventType: event.eventType
author: author,
message: message,
eventId: eventId,
formattedBody: formattedBody,
source: source,
eventType: eventType
});
contextMenu.open();
}

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-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
import NeoChat.Settings 1.0
Kirigami.ScrollablePage {
title: i18n("Settings")
bottomPadding: 0
leftPadding: 0
rightPadding: 0
topPadding: 0
onBackRequested: {
if (pageSettingStack.depth > 1 && !pageSettingStack.wideMode && pageSettingStack.currentIndex !== 0) {
event.accepted = true;
pageSettingStack.pop();
}
}
Kirigami.PageRow {
id: pageSettingStack
anchors.fill: parent
columnView.columnWidth: Kirigami.Units.gridUnit * 12
initialPage: Kirigami.ScrollablePage {
bottomPadding: 0
leftPadding: 0
rightPadding: 0
topPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
ListView {
Component.onCompleted: if (pageSettingStack.wideMode) {
actions[0].trigger();
}
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("General")
icon.name: "org.kde.neochat"
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/GeneralSettingsPage.qml")
},
Kirigami.Action {
text: i18n("Appearance")
icon.name: "preferences-desktop-theme-global"
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/AppearanceSettingsPage.qml")
},
Kirigami.Action {
text: i18n("Accounts")
icon.name: "preferences-system-users"
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Page/AccountsPage.qml")
},
Kirigami.Action {
text: i18n("Custom Emoji")
icon.name: "preferences-desktop-emoticons"
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/Emoticons.qml")
},
Kirigami.Action {
text: i18n("Devices")
iconName: "network-connect"
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Page/DevicesPage.qml")
},
Kirigami.Action {
text: i18n("About NeoChat")
icon.name: "help-about"
onTriggered: pageSettingStack.push(aboutPage)
}
]
model: actions
delegate: Kirigami.BasicListItem {
action: modelData
}
}
}
}
Component {
id: aboutPage
Kirigami.AboutPage {
aboutData: Controller.aboutData
}
}
}

View File

@@ -5,5 +5,6 @@ RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml
ImageEditorPage 1.0 ImageEditorPage.qml

View File

@@ -18,46 +18,6 @@ Kirigami.OverlayDrawer {
id: roomDrawer
readonly property var room: RoomManager.currentRoom
width: modal ? undefined : actualWidth
readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
property int actualWidth: {
if (Config.roomDrawerWidth === -1) {
return Kirigami.Units.gridUnit * 20;
} else {
return Config.roomDrawerWidth
}
}
MouseArea {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: undefined
width: 2
z: 500
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
enabled: true
visible: true
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
onReleased: {
Config.roomDrawerWidth = roomDrawer.actualWidth;
Config.save();
}
property real _lastX: -1
onPositionChanged: {
if (_lastX === -1) {
return;
}
if (Qt.application.layoutDirection === Qt.RightToLeft) {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x))
} else {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x))
}
}
}
enabled: true
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
@@ -70,6 +30,7 @@ Kirigami.OverlayDrawer {
active: roomDrawer.drawerOpen
sourceComponent: ColumnLayout {
id: columnLayout
anchors.fill: parent
spacing: 0
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
@@ -86,7 +47,7 @@ Kirigami.OverlayDrawer {
icon.name: "list-add-user"
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {room: room})
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
roomDrawer.close();
}
}
@@ -108,7 +69,12 @@ Kirigami.OverlayDrawer {
ToolButton {
Layout.alignment: Qt.AlignRight
icon.name: 'settings-configure'
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
onClicked: {
roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
if (!wideScreen) {
roomDrawer.close();
}
}
ToolTip {
text: i18n("Room settings")
@@ -117,61 +83,67 @@ Kirigami.OverlayDrawer {
}
}
ColumnLayout {
Control {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Kirigami.Heading {
text: i18n("Room information")
level: 3
}
RowLayout {
padding: Kirigami.Units.largeSpacing
contentItem: ColumnLayout {
id: infoLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.name : i18n("No name")
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
Kirigami.Heading {
text: i18n("Room information")
level: 3
}
ColumnLayout {
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
Layout.margins: Kirigami.Units.largeSpacing
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
font.bold: true
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.name : i18n("No name")
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
}
Label {
ColumnLayout {
Layout.fillWidth: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
Layout.alignment: Qt.AlignVCenter
spacing: 0
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
font.bold: true
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
}
Label {
Layout.fillWidth: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
}
}
}
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:\/\/[^ ]*\)/
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
TextEdit {
Layout.maximumWidth: Kirigami.Units.gridUnit * 13
Layout.preferredWidth: Kirigami.Units.gridUnit * 13
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /\(https:\/\/[^ ]*\)/
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
}
@@ -181,7 +153,7 @@ Kirigami.OverlayDrawer {
activeFocusOnTab: false
Label {
Layout.alignment: Qt.AlignRight
text: room ? i18np("%1 Member", "%1 Members", room.joinedCount) : i18n("No Member Count")
text: room ? i18np("%1 Member", "%1 Members", room.totalMemberCount) : i18n("No Member Count")
}
}
@@ -189,11 +161,6 @@ Kirigami.OverlayDrawer {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
contentItem: Kirigami.SearchField {
id: userListSearchField
onAccepted: sortedMessageEventModel.filterString = text;
@@ -220,7 +187,6 @@ Kirigami.OverlayDrawer {
sortRole: "perm"
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
delegate: Kirigami.AbstractListItem {
@@ -288,6 +254,12 @@ Kirigami.OverlayDrawer {
}
}
Component {
id: roomSettingDialog
RoomSettingsDialog {}
}
Component {
id: userDetailDialog

View File

@@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
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
required property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
text: i18n("General")
icon.name: "settings-configure"
page: Qt.resolvedUrl("General.qml")
initialProperties: {
return {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Security")
icon.name: "security-low"
page: Qt.resolvedUrl("Security.qml")
initialProperties: {
return {
room: root.room
}
}
}
]
}

View File

@@ -1,202 +0,0 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
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
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
property var room
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
readonly property bool canChangeName: room.canSendState("m.room.name")
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
title: i18n('General')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
Kirigami.Avatar {
Layout.bottomMargin: Kirigami.Units.largeSpacing
name: room.name
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
RoundButton {
anchors.right: parent.right
anchors.bottom: parent.bottom
height: Kirigami.Units.gridUnits
width: Kirigami.Units.gridUnits
icon.name: 'cloud-upload'
Accessible.name: i18n("Update avatar")
enabled: canChangeAvatar
onClicked: {
const fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
room.changeAvatar(path)
})
fileDialog.open()
}
}
}
TextField {
id: roomNameField
text: room.name
Kirigami.FormData.label: i18n("Room Name:")
enabled: canChangeName
}
TextArea {
id: roomTopicField
Layout.fillWidth: true
text: room.topic
Kirigami.FormData.label: i18n("Room topic:")
enabled: canChangeTopic
}
Kirigami.Separator {
Layout.fillWidth: true
visible: canonicalAliasComboBox.visible || altAlias.visible
}
ComboBox {
id: canonicalAliasComboBox
visible: room.aliases && room.aliases.length
Kirigami.FormData.label: i18n("Canonical Alias:")
popup.z: 999; // HACK This is an absolute hack, but combos inside OverlaySheets have their popups show up underneath, because of fun z ordering stuff
enabled: canChangeCanonicalAlias
model: room.aliases
currentIndex: room.aliases.indexOf(room.canonicalAlias)
onCurrentIndexChanged: {
if (room.canonicalAlias != room.aliases[currentIndex]) {
room.setCanonicalAlias(room.aliases[currentIndex])
}
}
}
RowLayout {
id: altAlias
Kirigami.FormData.label: i18n("Other Aliases:")
Layout.fillWidth: true
visible: room.altAliases && room.altAliases.length
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: room.altAliases
delegate: RowLayout {
Layout.maximumWidth: parent.width
Label {
text: modelData
}
ToolButton {
icon.name: ""
onClicked: room.removeLocalAlias(modelData)
}
}
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
visible: next.visible || prev.visible
}
Control {
id: next
Layout.fillWidth: true
visible: room.predecessorId && room.connection.room(room.predecessorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room continues another conversation.")
actions: Kirigami.Action {
text: i18n("See older messages...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.predecessorId)
root.close()
}
}
}
}
Control {
id: prev
Layout.fillWidth: true
visible: room.successorId && room.connection.room(room.successorId)
padding: Kirigami.Units.largeSpacing
contentItem: Kirigami.InlineMessage {
text: i18n("This room has been replaced.")
actions: Kirigami.Action {
text: i18n("See new room...")
onTriggered: {
roomListForm.enteredRoom = Controller.activeConnection.room(room.successorId)
root.close()
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
}
footer: ToolBar {
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
enabled: room.name !== roomNameField.text || room.topic !== roomTopicField.text
text: i18n("Apply")
onClicked: {
if (room.name != roomNameField.text) {
room.setName(roomNameField.text)
}
if (room.topic != roomTopicField.text) {
room.setTopic(roomTopicField.text)
}
}
}
}
}
}

View File

@@ -1,69 +0,0 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
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
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
property var room
title: i18n('Security')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
CheckBox {
text: i18nc("@option:check", "Private (invite only)")
Kirigami.FormData.label: i18nc("@option:check", "Access:")
checked: room.joinRule === "invite"
enabled: false
}
Label {
text: i18n("Only invited people can join.")
font: Kirigami.Theme.smallFont
}
CheckBox {
text: i18nc("@option:check", "Space members")
checked: room.joinRule === "restricted"
enabled: false
}
Label {
text: i18n("Anyone in a space can find and join.")
font: Kirigami.Theme.smallFont
}
CheckBox {
text: i18nc("@option:check", "Public")
checked: room.joinRule === "public"
enabled: false
}
Label {
text: i18nc("@option:check", "Anyone can find and join.") + room.joinRule
font: Kirigami.Theme.smallFont
}
}
}
footer: ToolBar {
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
enabled: false
text: i18n("Apply")
}
}
}
}

View File

@@ -1,12 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.AboutPage {
title: i18nc('@title:window', 'About NeoChat')
aboutData: Controller.aboutData
}

View File

@@ -12,7 +12,6 @@ import org.kde.neochat 1.0
import NeoChat.Settings 1.0
Kirigami.ScrollablePage {
title: i18nc('@title:window', 'Appearance')
ColumnLayout {
RowLayout {
Layout.alignment: Qt.AlignCenter

View File

@@ -14,7 +14,6 @@ import NeoChat.Component 1.0 as Components
import NeoChat.Dialog 1.0
Kirigami.Page {
title: i18nc('@title:window', 'Custom Emojis')
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0

View File

@@ -11,7 +11,6 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18nc('@title:window', 'General')
ColumnLayout {
Kirigami.FormLayout {
QQC2.CheckBox {

View File

@@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
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 {
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
text: i18n("General")
icon.name: "org.kde.neochat"
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Appearance")
icon.name: "preferences-desktop-theme-global"
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Accounts")
icon.name: "preferences-system-users"
page: Qt.resolvedUrl("AccountsPage.qml")
},
Kirigami.SettingAction {
text: i18n("Custom Emoji")
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},
Kirigami.SettingAction {
text: i18n("Spell Checking")
iconName: "tools-check-spelling"
page: Qt.resolvedUrl("SonnetConfigPage.qml")
},
Kirigami.SettingAction {
text: i18n("Devices")
iconName: "network-connect"
page: Qt.resolvedUrl("DevicesPage.qml")
},
Kirigami.SettingAction {
text: i18n("About NeoChat")
icon.name: "help-about"
page: Qt.resolvedUrl("About.qml")
}
]
}

View File

@@ -1,336 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
import QtQml 2.15
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.sonnet 1.0 as Sonnet
Kirigami.Page {
id: page
/**
* This property holds whether the setting on that page are automatically
* applied or whether the user can apply then manually. By default, false.
*/
property bool instantApply: false
/**
* This property holds whether the ListViews inside the page should get
* extra padding and a background. By default, use the Kirigami.ApplicationWindow
* wideMode value.
*/
property bool wideMode: QQC2.ApplicationWindow.window.wideMode ?? QQC2.ApplicationWindow.window.width > Kirigami.Units.gridUnit * 40
/**
* Signal emmited when the user decide to discard it's change and close the
* setting page.
*
* For example when using the ConfigPage inside Kirigami PageRow:
*
* \code
* Sonnet.ConfigPage {
* onClose: applicationWindow().pageStack.pop();
* }
* \endcode
*/
signal close()
function onBackRequested(event) {
if (settings.modified) {
applyDialog.open();
event.accepted = true;
}
if (dialog) {
dialog.close();
}
}
title: i18nc('@window:title', 'Spellchecking')
QQC2.Dialog {
id: applyDialog
title: qsTr("Apply Settings")
contentItem: QQC2.Label {
text: qsTr("The settings of the current module have changed.<br /> Do you want to apply the changes or discard them?")
}
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel | QQC2.Dialog.Discard
onAccepted: {
settings.save();
applyDialog.close();
page.close();
}
onDiscarded: {
applyDialog.close();
page.close();
}
onRejected: applyDialog.close();
}
onWideModeChanged: scroll.background.visible = wideMode;
leftPadding: wideMode ? Kirigami.Units.gridUnit : 0
topPadding: wideMode ? Kirigami.Units.gridUnit : 0
bottomPadding: wideMode ? Kirigami.Units.gridUnit : 0
rightPadding: wideMode ? Kirigami.Units.gridUnit : 0
property var dialog: null
Sonnet.Settings {
id: settings
}
ColumnLayout {
anchors.fill: parent
Kirigami.FormLayout {
Layout.fillWidth: true
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
QQC2.ComboBox {
Kirigami.FormData.label: i18n("Selected default language:")
model: settings.dictionaryModel
textRole: "display"
valueRole: "languageCode"
Component.onCompleted: currentIndex = indexOfValue(settings.defaultLanguage);
onActivated: {
settings.defaultLanguage = currentValue;
}
}
QQC2.Button {
text: i18n("Open Personal Dictionary")
onClicked: if (!dialog) {
if (Kirigami.Settings.isMobile) {
dialog = mobileSheet.createObject(page, {settings: settings});
dialog.open();
} else {
dialog = desktopSheet.createObject(page, {settings: settings})
dialog.show();
}
} else {
if (Kirigami.Settings.isMobile) {
dialog.open();
} else {
dialog.show();
}
}
}
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Options:")
checked: settings.checkerEnabledByDefault
text: i18n("Enable automatic spell checking")
onCheckedChanged: {
settings.checkerEnabledByDefault = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
checked: settings.skipUppercase
text: i18n("Ignore uppercase words")
onCheckedChanged: {
settings.skipUppercase = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
checked: settings.skipRunTogether
text: i18n("Ignore hyphenated words")
onCheckedChanged: {
settings.skipRunTogether = checked;
if (instantApply) {
settings.save();
}
}
}
QQC2.CheckBox {
id: autodetectLanguageCheckbox
checked: settings.autodetectLanguage
text: i18n("Detect language automatically")
onCheckedChanged: {
settings.autodetectLanguage = checked;
if (instantApply) {
settings.save();
}
}
}
}
Kirigami.Heading {
level: 2
text: i18n("Spell checking languages")
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
}
QQC2.Label {
text: i18n("%1 will provide spell checking and suggestions for the languages listed here when autodetection is enabled.", Qt.application.displayName)
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.leftMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
Layout.rightMargin: wideMode ? 0 : Kirigami.Units.largeSpacing
}
QQC2.ScrollView {
id: scroll
Layout.fillWidth: true
Layout.fillHeight: true
enabled: autodetectLanguageCheckbox.checked
Component.onCompleted: background.visible = wideMode
ListView {
clip: true
model: settings.dictionaryModel
delegate: Kirigami.CheckableListItem {
label: model.display
action: Kirigami.Action {
onTriggered: model.checked = checked
}
checked: model.checked
trailing: Kirigami.Icon {
source: "favorite"
visible: model.isDefault
HoverHandler {
id: hover
}
QQC2.ToolTip {
visible: hover.hovered
text: qsTr("Default Language")
}
}
}
}
}
}
component SheetHeader : RowLayout {
QQC2.TextField {
id: dictionaryField
Layout.fillWidth: true
placeholderText: i18n("Add a new word to your personal dictionary…")
}
QQC2.Button {
text: i18nc("@action:button", "Add word")
icon.name: "list-add"
enabled: dictionaryField.text.length > 0
onClicked: {
add(dictionaryField.text);
dictionaryField.clear();
if (instantApply) {
settings.save();
}
}
Layout.rightMargin: Kirigami.Units.largeSpacing
}
}
Component {
id: desktopSheet
QQC2.ApplicationWindow {
id: window
required property Sonnet.Settings settings
title: i18n("Spell checking dictionary")
width: Kirigami.Units.gridUnit * 20
height: Kirigami.Units.gridUnit * 20
flags: Qt.Dialog | Qt.WindowCloseButtonHint
header: Kirigami.AbstractApplicationHeader {
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
contentItem: SheetHeader {
anchors.fill: parent
}
}
QQC2.ScrollView {
anchors.fill: parent
ListView {
model: settings.currentIgnoreList
delegate: Kirigami.BasicListItem {
label: model.modelData
trailing: QQC2.ToolButton {
icon.name: "delete"
onClicked: {
remove(modelData)
if (instantApply) {
settings.save();
}
}
QQC2.ToolTip {
text: i18n("Delete word")
}
}
}
}
}
}
}
Component {
id: mobileSheet
Kirigami.OverlaySheet {
required property Sonnet.Settings settings
id: dictionarySheet
header: SheetHeader {}
ListView {
implicitWidth: Kirigami.Units.gridUnit * 15
model: settings.currentIgnoreList
delegate: Kirigami.BasicListItem {
label: model.modelData
trailing: QQC2.ToolButton {
icon.name: "delete"
onClicked: {
remove(modelData)
if (instantApply) {
settings.save();
}
}
QQC2.ToolTip {
text: i18n("Delete word")
}
}
}
}
}
}
footer: QQC2.ToolBar {
visible: !instantApply
height: visible ? implicitHeight : 0
contentItem: RowLayout {
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18n("Apply")
enabled: settings.modified
onClicked: settings.save();
}
}
}
function add(word) {
const dictionary = settings.currentIgnoreList;
dictionary.push(word);
settings.currentIgnoreList = dictionary;
}
function remove(word) {
settings.currentIgnoreList = settings.currentIgnoreList.filter(function (value, _, _) {
return value !== word;
});
}
}

View File

@@ -1,4 +1,2 @@
module NeoChat.Settings
ThemeRadioButton 1.0 ThemeRadioButton.qml
SettingsPage 1.0 SettingsPage.qml
SonnetConfigPage 1.0 SonnetConfigPage.qml

View File

@@ -122,40 +122,3 @@ Comment[uk]=Надійшло нове повідомлення
Comment[x-test]=xxThere is a new messagexx
Comment[zh_CN]=有新消息
Action=Popup
[Event/invite]
Name=New Invitation
Name[az]=Yeni dəvət
Name[ca]=Invitació nova
Name[ca@valencia]=Invitació nova
Name[es]=Nueva invitación
Name[fr]=Nouvelle invitation
Name[ia]=Nove invitation
Name[it]=Nuovo invito
Name[ko]=새 초대장
Name[nl]=Nieuwe uitnodiging
Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite
Name[pt_BR]=Novo convite
Name[sl]=Novo povabilo
Name[sv]=Ny inbjudan
Name[uk]=Нове запрошення
Name[x-test]=xxNew Invitationxx
Comment=There is a new invitation to a room
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[es]=Hay una nueva invitación a una sala
Comment[fr]=Il y a une nouvelle invitation dans un salon.
Comment[ia]=Il ha un nove invitation a un sala
Comment[it]=È presente un nuovo invito a una stanza
Comment[ko]=새로운 대화방 초대장을 받음
Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala
Comment[pt_BR]=Existe um novo convite para uma sala
Comment[sl]=Tam je novo povabilo v sobo
Comment[sv]=Det finns en ny inbjudan till ett rum
Comment[uk]=У кімнаті нове запрошення
Comment[x-test]=xxThere is a new invitation to a roomxx
Action=Popup

View File

@@ -117,8 +117,8 @@
<p xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
<p>NeoChat works both on mobile and desktop while providing a consistent user experience.</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">El NeoChat funciona en el mòbils i a l'escriptori, proporcionant un experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">El NeoChat funciona en el mòbils i a l'escriptori, proporcionant un 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>
@@ -188,29 +188,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="21.12" date="2021-12-07">
<description>
<p>NeoChat 21.12 brings lots of new features and fixes</p>
<ul>
<li>Solved various problems related to login, logout and account switching</li>
<li>Fixed a few problems in the timeline layout</li>
<li>Added Spell checking while writing a message</li>
<li>Improved Settings pages</li>
<li>Many improvements to the android and general mobile support</li>
<li>Show blurhashes while images load</li>
<li>Support showing custom emojis</li>
<li>Added a global menu</li>
<li>Added support for spoilers</li>
<li>Added a quick switcher to switch between rooms</li>
<li>Added support for an optional fancy blur background effect</li>
<li>Resizable left and right drawers</li>
<li>Added Syntax highlighting in raw json messages</li>
<li>Better wayland support</li>
<li>Improved file reception and download</li>
</ul>
</description>
<url>https://www.plasma-mobile.org/2021/12/07/plasma-mobile-gear-21-12/</url>
</release>
<release version="1.2.0" date="2021-06-01">
<description>
<p>NeoChat 1.2 brings a major redesign of the user interface. The chat page is now using bubbles for the messages and the input component was completely rewritten with a nicer look as well.</p>

View File

@@ -6,7 +6,7 @@ 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.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
@@ -31,7 +31,9 @@ Kirigami.ApplicationWindow {
property bool roomListLoaded: false
property RoomPage roomPage
property RoomPage roomPage: RoomPage {
KeyNavigation.left: pageStack.get(0)
}
Connections {
target: root.quitAction
@@ -78,7 +80,7 @@ Kirigami.ApplicationWindow {
target: RoomManager
function onPushRoom(room, event) {
root.roomPage = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
pageStack.push(root.roomPage);
root.roomPage.forceActiveFocus();
if (event.length > 0) {
roomPage.goToEvent(event);
@@ -149,6 +151,7 @@ Kirigami.ApplicationWindow {
contextDrawer: RoomDrawer {
id: contextDrawer
contentItem.implicitWidth: columnWidth
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
modal: !root.wideScreen || !enabled
onEnabledChanged: drawerOpen = enabled && !modal
@@ -221,56 +224,6 @@ Kirigami.ApplicationWindow {
}
}
globalDrawer: Kirigami.GlobalDrawer {
property bool hasLayer
contentItem.implicitWidth: columnWidth
isMenu: true
actions: [
Kirigami.Action {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Start a Chat")
icon.name: "irc-join-channel"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection})
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Create a Room")
icon.name: "irc-join-channel"
onTriggered: {
let dialog = createRoomDialog.createObject(root.overlay);
dialog.open();
}
shortcut: StandardKey.New
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Settings")
icon.name: "settings-configure"
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: StandardKey.Preferences
},
Kirigami.Action {
text: i18n("Logout")
icon.name: "list-remove-user"
enabled: Controller.accountCount > 0
onTriggered: Controller.logout(Controller.activeConnection, true)
},
Kirigami.Action {
text: i18n("Quit")
icon.name: "gtk-quit"
shortcut: StandardKey.Quit
onTriggered: Qt.quit()
}
]
}
Component.onCompleted: Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
Connections {
target: Config
@@ -289,35 +242,6 @@ Kirigami.ApplicationWindow {
background: Rectangle {
color: Config.blur && !Config.compactLayout ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1 - Config.transparency) : "transparent"
}
Component {
id: roomListComponent
RoomListPage {
id: roomList
Connections {
target: root.roomPage
function onSwitchRoomUp() {
roomList.goToNextRoom();
}
function onSwitchRoomDown() {
roomList.goToPreviousRoom();
}
}
}
}
Connections {
target: AccountRegistry
function onRowsRemoved() {
if (AccountRegistry.rowCount() === 0) {
RoomManager.reset();
pageStack.clear();
roomListLoaded = false;
pageStack.push("qrc:/imports/NeoChat/Page/WelcomePage.qml");
}
}
}
Connections {
target: Controller
@@ -334,8 +258,17 @@ Kirigami.ApplicationWindow {
}
}
function onConnectionDropped() {
if (Controller.accountCount === 0) {
RoomManager.reset();
pageStack.clear();
roomListLoaded = false;
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
}
}
function onGlobalErrorOccured(error, detail) {
showPassiveNotification(i18n("%1: %2", error, detail));
showPassiveNotification(i18nc("%1: %2", error, detail));
}
function onShowWindow() {
@@ -361,7 +294,9 @@ Kirigami.ApplicationWindow {
property string url: ""
title: i18n("User consent")
header: Kirigami.Heading {
text: i18n("User consent")
}
QQC2.Label {
id: label
@@ -400,7 +335,9 @@ Kirigami.ApplicationWindow {
required property var user;
parent: QQC2.ApplicationWindow.overlay
title: i18n("Start a chat")
header: Kirigami.Heading {
text: i18n("Start a chat")
}
contentItem: QQC2.Label {
text: i18n("Do you want to start a chat with %1?", user.displayName)
wrapMode: Text.WordWrap
@@ -417,7 +354,7 @@ Kirigami.ApplicationWindow {
}
property Item hoverLinkIndicator: QQC2.Control {
parent: overlay.parent
parent: overlay.parent
property alias text: linkText.text
opacity: text.length > 0 ? 1 : 0
@@ -432,4 +369,69 @@ Kirigami.ApplicationWindow {
color: Kirigami.Theme.backgroundColor
}
}
Component {
id: settingsPageComponent
SettingsPage {}
}
Component {
id: roomListComponent
RoomListPage {
id: roomList
Connections {
target: root.roomPage
function onSwitchRoomUp() {
roomList.goToNextRoom();
}
function onSwitchRoomDown() {
roomList.goToPreviousRoom();
}
}
}
}
footer: Kirigami.NavigationTabBar {
actions: [
Kirigami.Action {
icon.name: "globe"
text: "Spaces"
checked: fooPage.visble
onTriggered: {
console.warn("foo")
}
},
Kirigami.Action {
icon.name: "dialog-messages"
text: "Rooms"
checked: fooPage.visble
onTriggered: {
while (pageStack.depth > 0) {
pageStack.pop();
}
pageStack.push(roomListComponent);
}
},
Kirigami.Action {
icon.name: "document-open-recent-symbolic"
text: "Recent"
checked: fooPage.visble
onTriggered: {
console.warn("foo")
}
},
Kirigami.Action {
icon.name: "settings-configure"
text: "Settings"
checked: fooPage.visble
onTriggered: {
while (pageStack.depth > 0) {
pageStack.pop();
}
pageStack.push(settingsPageComponent);
}
}
]
}
}

16
res.qrc
View File

@@ -6,14 +6,14 @@
<file>imports/NeoChat/Page/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file>
<file>imports/NeoChat/Page/RoomWindow.qml</file>
<file>imports/NeoChat/Page/AccountsPage.qml</file>
<file>imports/NeoChat/Page/JoinRoomPage.qml</file>
<file>imports/NeoChat/Page/InviteUserPage.qml</file>
<file>imports/NeoChat/Page/SettingsPage.qml</file>
<file>imports/NeoChat/Page/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
<file>imports/NeoChat/Page/DevicesPage.qml</file>
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/RoomSettings/General.qml</file>
<file>imports/NeoChat/RoomSettings/Security.qml</file>
<file>imports/NeoChat/RoomSettings/Categories.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/FullScreenImage.qml</file>
<file>imports/NeoChat/Component/FancyEffectsContainer.qml</file>
@@ -26,6 +26,10 @@
<file>imports/NeoChat/Component/ChatBox/AttachmentPane.qml</file>
<file>imports/NeoChat/Component/ChatBox/ReplyPane.qml</file>
<file>imports/NeoChat/Component/ChatBox/CompletionMenu.qml</file>
<file>imports/NeoChat/Component/ChatBox/CursorHandle.qml</file>
<file>imports/NeoChat/Component/ChatBox/CursorDelegate.qml</file>
<file>imports/NeoChat/Component/ChatBox/MobileTextActionsToolBar.qml</file>
<file>imports/NeoChat/Component/ChatBox/TextFieldContextMenu.qml</file>
<file>imports/NeoChat/Component/ChatBox/qmldir</file>
<file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file>
<file>imports/NeoChat/Component/Emoji/qmldir</file>
@@ -53,6 +57,7 @@
<file>imports/NeoChat/Panel/qmldir</file>
<file>imports/NeoChat/Panel/RoomDrawer.qml</file>
<file>imports/NeoChat/Dialog/qmldir</file>
<file>imports/NeoChat/Dialog/RoomSettingsDialog.qml</file>
<file>imports/NeoChat/Dialog/UserDetailDialog.qml</file>
<file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file>
<file>imports/NeoChat/Dialog/EmojiDialog.qml</file>
@@ -68,16 +73,11 @@
<file>qtquickcontrols2.conf</file>
<file>imports/NeoChat/Component/glowdot.png</file>
<file>imports/NeoChat/Component/confetti.png</file>
<file>imports/NeoChat/Settings/SettingsPage.qml</file>
<file>imports/NeoChat/Settings/ThemeRadioButton.qml</file>
<file>imports/NeoChat/Settings/ColorScheme.qml</file>
<file>imports/NeoChat/Settings/GeneralSettingsPage.qml</file>
<file>imports/NeoChat/Settings/Emoticons.qml</file>
<file>imports/NeoChat/Settings/AppearanceSettingsPage.qml</file>
<file>imports/NeoChat/Settings/AccountsPage.qml</file>
<file>imports/NeoChat/Settings/DevicesPage.qml</file>
<file>imports/NeoChat/Settings/About.qml</file>
<file>imports/NeoChat/Settings/SonnetConfigPage.qml</file>
<file>imports/NeoChat/Settings/qmldir</file>
</qresource>
</RCC>

View File

@@ -4,6 +4,7 @@
# SPDX-License-Identifier: BSD-2-Clause
add_executable(neochat
accountlistmodel.cpp
controller.cpp
actionshandler.cpp
emojimodel.cpp
@@ -35,14 +36,11 @@ add_executable(neochat
spellcheckhighlighter.cpp
blurhash.cpp
blurhashimageprovider.cpp
joinrulesevent.cpp
../res.qrc
)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PRIVATE QUOTIENT_07)
else()
target_sources(neochat PRIVATE accountregistry.cpp)
endif()
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)

60
src/accountlistmodel.cpp Normal file
View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#include "accountlistmodel.h"
#include "room.h"
AccountListModel::AccountListModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(&Controller::instance(), &Controller::connectionAdded, this, [=]() {
beginResetModel();
endResetModel();
});
connect(&Controller::instance(), &Controller::connectionDropped, this, [=]() {
beginResetModel();
endResetModel();
});
}
QVariant AccountListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= Controller::instance().connections().count()) {
return {};
}
auto connection = Controller::instance().connections().at(index.row());
if (role == UserRole) {
return QVariant::fromValue(connection->user());
}
if (role == ConnectionRole) {
return QVariant::fromValue(connection);
}
return {};
}
int AccountListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return Controller::instance().connections().count();
}
QHash<int, QByteArray> AccountListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[UserRole] = "user";
roles[ConnectionRole] = "connection";
return roles;
}

26
src/accountlistmodel.h Normal file
View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "controller.h"
#include <QAbstractListModel>
#include <QObject>
class AccountListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum EventRoles {
UserRole = Qt::UserRole + 1,
ConnectionRole,
};
AccountListModel(QObject *parent = nullptr);
[[nodiscard]] QVariant data(const QModelIndex &index, int role = UserRole) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
};

View File

@@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "accountregistry.h"
#include "connection.h"
using namespace Quotient;
void AccountRegistry::add(Connection *c)
{
if (m_accounts.contains(c))
return;
beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
m_accounts += c;
endInsertRows();
}
void AccountRegistry::drop(Connection *c)
{
beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
m_accounts.removeOne(c);
endRemoveRows();
Q_ASSERT(!m_accounts.contains(c));
}
bool AccountRegistry::isLoggedIn(const QString &userId) const
{
return std::any_of(m_accounts.cbegin(), m_accounts.cend(), [&userId](Connection *a) {
return a->userId() == userId;
});
}
bool AccountRegistry::contains(Connection *c) const
{
return m_accounts.contains(c);
}
AccountRegistry::AccountRegistry() = default;
QVariant AccountRegistry::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= m_accounts.count()) {
return {};
}
const auto account = m_accounts[index.row()];
if (role == ConnectionRole) {
return QVariant::fromValue(account);
}
return {};
}
int AccountRegistry::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_accounts.count();
}
QHash<int, QByteArray> AccountRegistry::roleNames() const
{
return {{ConnectionRole, "connection"}};
}
bool AccountRegistry::isEmpty() const
{
return m_accounts.isEmpty();
}
int AccountRegistry::count() const
{
return m_accounts.count();
}
const QVector<Connection *> AccountRegistry::accounts() const
{
return m_accounts;
}
Connection *AccountRegistry::get(const QString &userId)
{
for (const auto &connection : m_accounts) {
if (connection->userId() == userId) {
return connection;
}
}
return nullptr;
}

View File

@@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QObject>
namespace Quotient
{
class Connection;
class AccountRegistry : public QAbstractListModel
{
Q_OBJECT
public:
enum EventRoles {
ConnectionRole = Qt::UserRole + 1,
};
static AccountRegistry &instance()
{
static AccountRegistry _instance;
return _instance;
}
const QVector<Connection *> accounts() const;
void add(Connection *a);
void drop(Connection *a);
bool isLoggedIn(const QString &userId) const;
bool isEmpty() const;
int count() const;
bool contains(Connection *) const;
Connection *get(const QString &userId);
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
AccountRegistry();
QVector<Connection *> m_accounts;
};
}

View File

@@ -106,20 +106,7 @@ void ActionsHandler::postMessage(const QString &text,
CustomEmojiModel *cem)
{
QString rawText = text;
auto stringList = text.split(QStringLiteral("```"));
QString cleanedText;
const auto count = stringList.count();
for (int i = 0; i < count; i++) {
if (i % 2 == 0) {
if (i + 1 != count) {
cleanedText += stringList[i].toHtmlEscaped() + QStringLiteral("```");
} else {
cleanedText += stringList[i].toHtmlEscaped();
}
} else {
cleanedText += stringList[i] + QStringLiteral("```");
}
}
QString cleanedText = text;
auto preprocess = [cem](const QString &it) -> QString {
if (cem == nullptr) {

View File

@@ -34,7 +34,6 @@
#include <signal.h>
#include "accountregistry.h"
#include "csapi/account-data.h"
#include "csapi/content-repo.h"
#include "csapi/joining.h"
@@ -46,8 +45,6 @@
#include "neochatuser.h"
#include "roommanager.h"
#include "settings.h"
#include "utils.h"
#include <KStandardShortcut>
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
@@ -72,7 +69,7 @@ Controller::Controller(QObject *parent)
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
QGuiApplication::setQuitOnLastWindowClosed(false);
}
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this, trayIcon]() {
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [=]() {
if (NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
@@ -84,7 +81,7 @@ Controller::Controller(QObject *parent)
});
#endif
QTimer::singleShot(0, this, [this] {
QTimer::singleShot(0, this, [=] {
invokeLogin();
});
@@ -121,7 +118,7 @@ Controller::Controller(QObject *parent)
Controller::~Controller()
{
for (auto c : AccountRegistry::instance().accounts()) {
for (auto c : qAsConst(m_connections)) {
c->saveState();
}
}
@@ -152,7 +149,7 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
conn->setHomeserver(serverUrl);
}
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
connect(conn, &Connection::connected, this, [=] {
AccountSettings account(conn->userId());
account.setKeepLoggedIn(true);
account.setHomeserver(conn->homeserver());
@@ -165,7 +162,7 @@ void Controller::loginWithAccessToken(const QString &serverAddr, const QString &
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
connect(conn, &Connection::networkError, this, [=](QString error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
conn->assumeIdentity(user, token, deviceName);
@@ -189,28 +186,32 @@ void Controller::logout(Connection *conn, bool serverSideLogout)
job.start();
loop.exec();
if (conn == activeConnection() && AccountRegistry::instance().count() > 1) {
setActiveConnection(AccountRegistry::instance().accounts()[0]);
conn->stopSync();
Q_EMIT conn->stateChanged();
Q_EMIT conn->loggedOut();
if (conn == activeConnection() && !m_connections.isEmpty()) {
setActiveConnection(m_connections[0]);
} else {
setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
conn->logout();
auto logoutJob = conn->callApi<LogoutJob>();
connect(logoutJob, &LogoutJob::failure, this, [=] {
Q_EMIT errorOccured(i18n("Server-side Logout Failed: %1", logoutJob->errorString()));
});
}
void Controller::addConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
#ifndef QUOTIENT_07
AccountRegistry::instance().add(c);
#endif
m_connections += c;
c->setLazyLoading(true);
connect(c, &Connection::syncDone, this, [this, c] {
connect(c, &Connection::syncDone, this, [=] {
setBusy(false);
Q_EMIT syncDone();
@@ -218,11 +219,11 @@ void Controller::addConnection(Connection *c)
c->sync(30000);
c->saveState();
});
connect(c, &Connection::loggedOut, this, [this, c] {
connect(c, &Connection::loggedOut, this, [=] {
dropConnection(c);
});
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
connect(c, &Connection::requestFailed, this, [=](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequiredError) {
Q_EMIT userConsentRequired(job->errorUrl());
}
@@ -239,16 +240,11 @@ void Controller::addConnection(Connection *c)
void Controller::dropConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
#ifndef QUOTIENT_07
AccountRegistry::instance().drop(c);
#endif
m_connections.removeOne(c);
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
#ifndef QUOTIENT_07
c->deleteLater();
#endif
}
void Controller::invokeLogin()
@@ -265,7 +261,7 @@ void Controller::invokeLogin()
auto accessToken = loadAccessTokenFromKeyChain(account);
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connect(connection, &Connection::connected, this, [=] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
@@ -273,17 +269,17 @@ void Controller::invokeLogin()
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
connect(connection, &Connection::loginError, this, [=](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
Q_EMIT errorOccured(i18n("Login Failed", error));
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
@@ -311,30 +307,23 @@ QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
{
QKeychain::Error error;
QString errorString;
do {
qDebug() << "Reading access token from the keychain for" << account.userId();
QKeychain::ReadPasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
QEventLoop loop;
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
qDebug() << "Read the access token from the keychain for " << account.userId();
QKeychain::ReadPasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
QEventLoop loop;
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error() == QKeychain::Error::NoError) {
return job.binaryData();
}
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), i18n("Please make sure that the keychain is opened."));
error = job.error();
errorString = job.errorString();
} while (error == QKeychain::Error::OtherError);
if (job.error() == QKeychain::Error::NoError) {
return job.binaryData();
}
qWarning() << "Could not read the access token from the keychain:" << errorString;
qWarning() << "Could not read the access token from the keychain: " << qPrintable(job.errorString());
// no access token from the keychain, try token file
auto accessToken = loadAccessTokenFromFile(account);
if (error == QKeychain::Error::EntryNotFound) {
if (job.error() == QKeychain::Error::EntryNotFound) {
if (!accessToken.isEmpty()) {
qDebug() << "Migrating the access token from file to the keychain for " << account.userId();
bool removed = false;
@@ -393,7 +382,7 @@ void Controller::playAudio(const QUrl &localFile)
auto player = new QMediaPlayer;
player->setMedia(localFile);
player->play();
connect(player, &QMediaPlayer::stateChanged, [player] {
connect(player, &QMediaPlayer::stateChanged, [=] {
player->deleteLater();
});
}
@@ -502,9 +491,14 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
setRequestData(_data);
}
QVector<Connection *> Controller::connections() const
{
return m_connections;
}
int Controller::accountCount() const
{
return AccountRegistry::instance().count();
return m_connections.count();
}
bool Controller::quitOnLastWindowClosed()
@@ -581,7 +575,7 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [this, createRoomJob] {
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT errorOccured(i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
});
}

View File

@@ -40,6 +40,8 @@ class Controller : public QObject
public:
static Controller &instance();
[[nodiscard]] QVector<Connection *> connections() const;
void setActiveConnection(Connection *connection);
[[nodiscard]] Connection *activeConnection() const;
@@ -95,6 +97,7 @@ private:
explicit Controller(QObject *parent = nullptr);
~Controller() override;
QVector<Connection *> m_connections;
QPointer<Connection> m_connection;
bool m_busy = false;

View File

@@ -88,7 +88,7 @@ void CustomEmojiModel::setConnection(Connection *it)
QString CustomEmojiModel::preprocessText(const QString &it)
{
auto cp = it;
for (const auto &emoji : std::as_const(d->emojies)) {
for (const auto &emoji : qAsConst(d->emojies)) {
cp.replace(
emoji.regexp,
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
@@ -99,7 +99,7 @@ QString CustomEmojiModel::preprocessText(const QString &it)
QVariantList CustomEmojiModel::filterModel(const QString &filter)
{
QVariantList results;
for (const auto &emoji : std::as_const(d->emojies)) {
for (const auto &emoji : qAsConst(d->emojies)) {
if (results.length() >= 10)
break;
if (!emoji.name.contains(filter, Qt::CaseInsensitive))

View File

@@ -88,7 +88,7 @@ void DevicesModel::setName(int index, const QString &name)
beginResetModel();
m_devices[index].displayName = name;
endResetModel();
connect(job, &BaseJob::failure, this, [this, index, oldName]() {
connect(job, &BaseJob::failure, this, [=]() {
beginResetModel();
m_devices[index].displayName = oldName;
endResetModel();

View File

@@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "joinrulesevent.h"
using namespace Quotient;
QString JoinRulesEvent::joinRule() const
{
return fromJson<QString>(contentJson()["join_rule"_ls]);
}
QJsonArray JoinRulesEvent::allow() const
{
return contentJson()["allow"_ls].toArray();
}

View File

@@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <events/stateevent.h>
#include <quotient_common.h>
namespace Quotient
{
class JoinRulesEvent : public StateEventBase
{
public:
DEFINE_EVENT_TYPEID("m.room.join_rules", JoinRulesEvent)
explicit JoinRulesEvent()
: StateEventBase(typeId(), matrixTypeId())
{
}
explicit JoinRulesEvent(const QJsonObject &obj)
: StateEventBase(typeId(), obj)
{
}
QString joinRule() const;
QJsonArray allow() const;
};
REGISTER_EVENT_TYPE(JoinRulesEvent)
} // namespace Quotient

View File

@@ -27,7 +27,7 @@ void Login::init()
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &Login::matrixIdChanged, this, [this]() {
connect(this, &Login::matrixIdChanged, this, [=]() {
setHomeserverReachable(false);
if (m_matrixId == "@") {
@@ -40,7 +40,7 @@ void Login::init()
m_connection = new Connection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [=]() {
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
@@ -49,7 +49,7 @@ void Login::init()
Q_EMIT loginFlowsChanged();
});
});
connect(m_connection, &Connection::connected, this, [this] {
connect(m_connection, &Connection::connected, this, [=] {
Q_EMIT connected();
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
@@ -67,22 +67,22 @@ void Login::init()
Controller::instance().setActiveConnection(m_connection);
m_connection = nullptr;
});
connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
connect(m_connection, &Connection::networkError, this, [=](QString error, const QString &, int, int) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
connect(m_connection, &Connection::loginError, this, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::resolveError, this, [](QString error) {
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
connectSingleShot(m_connection, &Connection::syncDone, this, [=]() {
Q_EMIT Controller::instance().initiated();
});
}
@@ -160,7 +160,7 @@ QUrl Login::ssoUrl() const
void Login::loginWithSso()
{
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [=]() {
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
});

View File

@@ -30,7 +30,7 @@
#include "neochat-version.h"
#include "accountregistry.h"
#include "accountlistmodel.h"
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatboxhelper.h"
@@ -44,7 +44,6 @@
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h"
#include "joinrulesevent.h"
#include "login.h"
#include "matriximageprovider.h"
#include "messageeventmodel.h"
@@ -180,7 +179,7 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ChatBoxHelper", &chatBoxHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app));
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CommandModel", new CommandModel(&app));
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<SpellcheckHighlighter>("org.kde.neochat", 1, 0, "SpellcheckHighlighter");

View File

@@ -58,12 +58,12 @@ MessageEventModel::MessageEventModel(QObject *parent)
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
qRegisterMetaType<FileTransferInfo>();
QTimer::singleShot(0, this, [this]() {
QTimer::singleShot(0, this, [=]() {
if (!m_currentRoom) {
return;
}
m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [this]() {
connect(this, &QAbstractListModel::rowsInserted, this, [=]() {
if (m_currentRoom->readMarkerEventId().isEmpty()) {
return;
}
@@ -98,7 +98,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
lastReadEventId = room->readMarkerEventId();
using namespace Quotient;
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [=](RoomEventsRange events) {
if (NeoChatConfig::self()->showFancyEffects()) {
for (auto &event : events) {
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
@@ -134,13 +134,13 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
});
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [=](RoomEventsRange events) {
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
}
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
});
connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) {
connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) {
endInsertRows();
if (!m_lastReadEventIndex.isValid()) {
// no read marker, so see if we need to create one.
@@ -184,7 +184,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
beginRemoveRows({}, i, i);
});
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
connect(m_currentRoom, &Room::readMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) {
connect(m_currentRoom, &Room::readMarkerMoved, this, [=](const QString &fromEventId, const QString &toEventId) {
Q_UNUSED(fromEventId);
moveReadMarker(toEventId);
});
@@ -203,7 +203,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
#ifndef QUOTIENT_07
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
#endif
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [=] {
beginResetModel();
endResetModel();
});

View File

@@ -48,9 +48,6 @@
<entry name="RoomListPageWidth" type="int">
<default>-1</default>
</entry>
<entry name="RoomDrawerWidth" type="int">
<default>-1</default>
</entry>
</group>
<group name="Timeline">
<entry name="ShowAvatarInTimeline" type="bool">

View File

@@ -43,7 +43,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
{
connect(this, &NeoChatRoom::notificationCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &NeoChatRoom::highlightCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &Room::fileTransferCompleted, this, [this] {
connect(this, &Room::fileTransferCompleted, this, [=] {
setFileUploadingProgress(0);
setHasFileUploading(false);
});
@@ -52,27 +52,12 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
connect(this, &Room::joinStateChanged, this, [this](JoinState oldState, JoinState newState) {
connect(this, &Room::joinStateChanged, this, [=](JoinState oldState, JoinState newState) {
if (oldState == JoinState::Invite && newState != JoinState::Invite) {
Q_EMIT isInviteChanged();
}
});
connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged);
connectSingleShot(this, &Room::baseStateLoaded, this, [this]() {
if (this->joinState() != JoinState::Invite) {
return;
}
const QString senderId = getCurrentState<RoomMemberEvent>(localUser()->id())->senderId();
QImage avatar_image;
if (!user(senderId)->avatarUrl(this).isEmpty()) {
avatar_image = user(senderId)->avatar(128, this);
} else {
qWarning() << "using this room's avatar";
avatar_image = avatar(128);
}
NotificationsManager::instance().postInviteNotification(this, htmlSafeDisplayName(), htmlSafeMemberName(senderId), avatar_image);
});
}
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
@@ -83,19 +68,19 @@ void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
setHasFileUploading(true);
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
connect(this, &Room::fileTransferCompleted, [=](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
}
});
connect(this, &Room::fileTransferFailed, [this, txnId](const QString &id, const QString & /*error*/) {
connect(this, &Room::fileTransferFailed, [=](const QString &id, const QString & /*error*/) {
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
}
});
connect(this, &Room::fileTransferProgress, [this, txnId](const QString &id, qint64 progress, qint64 total) {
connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) {
if (id == txnId) {
setFileUploadingProgress(int(float(progress) / float(total) * 100));
}
@@ -477,9 +462,6 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1" : e.version().toHtmlEscaped())
: i18n("created the room, version %1", e.version().isEmpty() ? "1" : e.version().toHtmlEscaped());
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEventBase &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
@@ -750,11 +732,6 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user)
doDeleteMessagesByUser(user);
}
QString NeoChatRoom::joinRule() const
{
return getCurrentState<JoinRulesEvent>()->joinRule();
}
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user)
{
QStringList events;

View File

@@ -3,7 +3,6 @@
#pragma once
#include "joinrulesevent.h"
#include <events/encryptionevent.h>
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
@@ -34,7 +33,6 @@ class NeoChatRoom : public Room
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
Q_PROPERTY(QString joinRule READ joinRule CONSTANT)
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
public:
@@ -68,8 +66,6 @@ public:
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
[[nodiscard]] QString joinRule() const;
[[nodiscard]] bool hasFileUploading() const
{
return m_hasFileUploading;

View File

@@ -71,30 +71,3 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
m_notifications.insert(room->id(), notification);
}
void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
{
if (!NeoChatConfig::self()->showNotifications()) {
return;
}
QPixmap img;
img.convertFromImage(icon);
KNotification *notification = new KNotification("invite");
notification->setText(i18n("%1 invited you to a room", sender));
notification->setTitle(title);
notification->setPixmap(img);
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
connect(notification, &KNotification::defaultActivated, this, [=]() {
RoomManager::instance().enterRoom(room);
Q_EMIT Controller::instance().showWindow();
});
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
connect(notification, &KNotification::action1Activated, this, [room]() {
room->acceptInvitation();
});
connect(notification, &KNotification::action2Activated, this, [room]() {
RoomManager::instance().leaveRoom(room);
});
notification->sendEvent();
m_notifications.insert(room->id(), notification);
}

View File

@@ -21,7 +21,6 @@ public:
Q_INVOKABLE void
postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon, const QString &replyEventId);
void postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon);
private:
NotificationsManager(QObject *parent = nullptr);

View File

@@ -115,7 +115,7 @@ void PublicRoomListModel::next(int count)
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword});
connect(job, &BaseJob::finished, this, [this] {
connect(job, &BaseJob::finished, this, [=] {
attempted = true;
if (job->status() == BaseJob::Success) {

View File

@@ -92,7 +92,7 @@ void RoomListModel::setConnection(Connection *connection)
m_connection = connection;
for (NeoChatRoom *room : std::as_const(m_rooms)) {
for (NeoChatRoom *room : qAsConst(m_rooms)) {
room->disconnect(this);
}
@@ -101,9 +101,9 @@ void RoomListModel::setConnection(Connection *connection)
connect(connection, &Connection::joinedRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::aboutToDeleteRoom, this, &RoomListModel::deleteRoom);
connect(connection, &Connection::directChatsListChanged, this, [this, connection](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) {
connect(connection, &Connection::directChatsListChanged, this, [=](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) {
auto refreshRooms = [this, &connection](Quotient::DirectChatsMap rooms) {
for (const QString &roomID : std::as_const(rooms)) {
for (const QString &roomID : qAsConst(rooms)) {
auto room = connection->room(roomID);
if (room) {
refresh(static_cast<NeoChatRoom *>(room));
@@ -152,29 +152,29 @@ void RoomListModel::doAddRoom(Room *r)
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
connect(room, &Room::displaynameChanged, this, [=] {
refresh(room);
});
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
connect(room, &Room::unreadMessagesChanged, this, [=] {
refresh(room);
});
connect(room, &Room::notificationCountChanged, this, [this, room] {
connect(room, &Room::notificationCountChanged, this, [=] {
refresh(room);
});
connect(room, &Room::avatarChanged, this, [this, room] {
refresh(room, {AvatarRole});
});
connect(room, &Room::tagsChanged, this, [this, room] {
connect(room, &Room::tagsChanged, this, [=] {
refresh(room);
});
connect(room, &Room::joinStateChanged, this, [this, room] {
connect(room, &Room::joinStateChanged, this, [=] {
refresh(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
connect(room, &Room::addedMessages, this, [=] {
refresh(room, {LastEventRole});
});
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
connect(room, &Room::highlightCountChanged, this, [this, room] {
connect(room, &Room::highlightCountChanged, this, [=] {
if (room->highlightCount() == 0) {
return;
}
@@ -205,7 +205,7 @@ void RoomListModel::handleNotifications()
static QStringList oldNotifications;
auto job = m_connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [this, job]() {
connect(job, &BaseJob::success, this, [=]() {
const auto notifications = job->jsonData()["notifications"].toArray();
if (initial) {
initial = false;
@@ -249,7 +249,7 @@ void RoomListModel::handleNotifications()
void RoomListModel::refreshNotificationCount()
{
int count = 0;
for (auto room : std::as_const(m_rooms)) {
for (auto room : qAsConst(m_rooms)) {
count += room->notificationCount();
}
if (m_notificationCount == count) {
@@ -489,7 +489,7 @@ bool RoomListModel::categoryVisible(int category) const
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for (const auto &room : std::as_const(m_rooms)) {
for (const auto &room : qAsConst(m_rooms)) {
if (room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
return room;
}

View File

@@ -186,7 +186,7 @@ void RoomManager::visitRoom(Room *room, const QString &eventId)
void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QStringList &viaServers)
{
account->joinRoom(QUrl::toPercentEncoding(roomAliasOrId), viaServers);
connectSingleShot(account, &Quotient::Connection::newRoom, this, [this](Quotient::Room *room) {
connectSingleShot(account, &Quotient::Connection::newRoom, this, [=](Quotient::Room *room) {
enterRoom(dynamic_cast<NeoChatRoom *>(room));
});
}

View File

@@ -75,7 +75,7 @@ void UserDirectoryListModel::search(int count)
job = m_connection->callApi<SearchUserDirectoryJob>(m_keyword, count);
connect(job, &BaseJob::finished, this, [this] {
connect(job, &BaseJob::finished, this, [=] {
attempted = true;
if (job->status() == BaseJob::Success) {

View File

@@ -32,7 +32,7 @@ void UserListModel::setRoom(Quotient::Room *room)
if (m_currentRoom) {
m_currentRoom->disconnect(this);
// m_currentRoom->connection()->disconnect(this);
for (User *user : std::as_const(m_users)) {
for (User *user : qAsConst(m_users)) {
user->disconnect(this);
}
m_users.clear();
@@ -47,16 +47,16 @@ void UserListModel::setRoom(Quotient::Room *room)
m_users = m_currentRoom->users();
std::sort(m_users.begin(), m_users.end(), room->memberSorter());
}
for (User *user : std::as_const(m_users)) {
for (User *user : qAsConst(m_users)) {
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
connect(user, &User::defaultAvatarChanged, this, [=]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this] {
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [=] {
setRoom(nullptr);
});
qDebug() << m_users.count() << "user(s) in the room";
@@ -104,7 +104,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return UserType::Member;
}
if (userPl < pl->powerLevelForEvent("m.room.message")) {
if (userPl < pl->powerLevelForState("m.room.message")) {
return UserType::Muted;
}
@@ -154,7 +154,7 @@ void UserListModel::userAdded(Quotient::User *user)
m_users.insert(pos, user);
endInsertRows();
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
connect(user, &User::defaultAvatarChanged, this, [=]() {
avatarChanged(user, m_currentRoom);
});
#else