Compare commits

..

41 Commits

Author SHA1 Message Date
Tobias Fella
041a5ff590 Add 21.12 release notes 2021-12-05 20:06:33 +00:00
Bhushan Shah
28d68444d9 GIT_SILENT Update version number for 21.12 2021-12-05 10:18:09 +05:30
Bhushan Shah
32cd42f03f cmake: use the PROJECT_VERSION variable
Makes it easier to bump version using scripts
2021-12-04 18:01:38 +05:30
l10n daemon script
98bc0b8c46 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-30 01:51:14 +00:00
Tobias Fella
5498cf1cd7 Add CI 2021-11-29 13:53:34 +01:00
l10n daemon script
babc87d023 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-29 01:35:58 +00:00
l10n daemon script
724e9d50a6 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-28 01:30:02 +00:00
l10n daemon script
8c0a6c1079 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-25 01:30:18 +00:00
l10n daemon script
6f33ad529e SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-24 01:36:52 +00:00
l10n daemon script
f9b5aa328a SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-23 01:29:26 +00:00
Tobias Fella
5b6e3d0902 Revert "Fix updating events when delegate choice changes"
This reverts commit 7b7c659a3a
2021-11-22 19:36:16 +00:00
l10n daemon script
5c5b805d3c SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-22 01:29:40 +00:00
Tobias Fella
d65962cbaa Use plaintext in completion menu 2021-11-22 00:20:49 +01:00
l10n daemon script
3658715ff6 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-21 01:25:36 +00:00
Carl Schwan
bf08303a8e Fix glitch in timeline scrolling
Turnout that reuseItems with loader and dynamically sized items is not
great.
2021-11-19 22:52:51 +01:00
Tobias Fella
935a51b477 More invite -> invitation 2021-11-19 15:47:31 +01:00
l10n daemon script
5b9a95878e SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2021-11-19 01:31:57 +00:00
Tobias Fella
560bd739e0 Invite -> Invitation 2021-11-18 15:27:50 +01:00
Tobias Fella
5b893d7736 Show a notification for invited rooms 2021-11-17 12:24:25 +00:00
Tobias Fella
c81ca6f8bb Set the height of statedelegates 2021-11-16 00:45:42 +01:00
Tobias Fella
740662e3f0 Remove visibility setting from FullScreenImage 2021-11-16 00:21:01 +01:00
Carl Schwan
46e1e64ee1 Improve source menu 2021-11-14 19:35:00 +00:00
Tobias Fella
7b7c659a3a Fix updating events when delegate choice changes 2021-11-13 22:19:15 +00:00
Tobias Fella
0a19d42799 Improve handling of closed keychain 2021-11-13 22:18:53 +00:00
Carl Schwan
8aa710d50f Full reuse compliance + ci check 2021-11-13 19:13:55 +01:00
Carl Schwan
7b81b545b9 Port to std::as_const 2021-11-13 19:11:47 +01:00
Tobias Fella
b0fde6d6c3 Add things to .gitignore 2021-11-13 14:59:21 +01:00
Tobias Fella
cb7b8bac99 Fix i18n message 2021-11-13 14:32:50 +01:00
Tobias Fella
9027db264a Don't capture 'this' implicitely 2021-11-13 14:17:20 +01:00
Carl Schwan
0f7461bd66 Bump dependencies 2021-11-13 13:21:01 +01:00
Carl Schwan
b44963d572 Copy SonnetConfigPage since we can't put it in Sonnet for now 2021-11-13 13:10:28 +01:00
Carl Schwan
25ac18e800 Revert "Revert "Spellchecking with new Sonnet declarative API""
This reverts commit dada3e300b.
2021-11-13 13:10:16 +01:00
Tobias Fella
8089e5bdfa Fix pagestack after login after logout 2021-11-12 16:21:48 +01:00
Christopher Hock
d1dce37ea7 Allow user to copy the room address to the clipboard
Contributes to #469
2021-11-07 16:12:29 +00:00
Carl Schwan
dd75eaec2c Remove dead code
It seems this was never used even by the commit introducing it
2021-11-05 20:54:29 +01:00
Tobias Fella
0568bed62d Use plaintext in TypingPane 2021-11-02 00:08:02 +01:00
Tobias Fella
d494eb1c63 Use Quotient's accountregistry 2021-11-01 19:36:39 +00:00
Carl Schwan
ee8be4b755 bump dependencies 2021-10-27 08:02:14 +00:00
l10n daemon script
97b0767b8f GIT_SILENT made messages (after extraction) 2021-10-25 00:18:07 +00:00
Nicolas Fella
1e0ff63ab8 Fix version variable 2021-10-24 23:03:02 +02:00
Nicolas Fella
b6341eebfe Pass version information to AndroidManifest
Fixes #463
2021-10-24 22:49:33 +02:00
55 changed files with 931 additions and 938 deletions

1
.gitignore vendored
View File

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

7
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,7 @@
# 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

25
.kde-ci.yml Normal file
View File

@@ -0,0 +1,25 @@
# 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,9 +7,10 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "21.12")
set(KF5_MIN_VERSION "5.86.0")
set(QT_MIN_VERSION "5.15.0")
set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.2")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -38,7 +39,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(1.2.80
ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
@@ -114,6 +115,10 @@ 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)

11
LICENSES/BSD-3-Clause.txt Normal file
View File

@@ -0,0 +1,11 @@
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="0.0.1"
android:versionCode="1604412458"
android:versionName="${versionName}"
android:versionCode="${versionCode}"
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"

96
android/build.gradle Normal file
View File

@@ -0,0 +1,96 @@
/*
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

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

View File

@@ -9,8 +9,7 @@ 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.15 as Kirigami
import org.kde.kirigami 2.18 as Kirigami
import org.kde.neochat 1.0
ToolBar {
@@ -69,7 +68,7 @@ ToolBar {
font: inputField.font
}
T.TextArea {
TextArea {
id: inputField
focus: true
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
@@ -101,16 +100,9 @@ ToolBar {
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption
palette: Kirigami.Theme.palette
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
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)
Kirigami.SpellChecking.enabled: true
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
@@ -119,65 +111,6 @@ 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
@@ -187,18 +120,6 @@ 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
@@ -239,9 +160,6 @@ 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) {
@@ -325,10 +243,7 @@ ToolBar {
chatBar.complete();
}
onPressed: MobileTextActionsToolBar.shouldBeVisible = true;
onTextChanged: {
MobileTextActionsToolBar.shouldBeVisible = false;
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text

View File

@@ -86,6 +86,7 @@ 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

@@ -1,65 +0,0 @@
/* 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

@@ -1,98 +0,0 @@
/* 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

@@ -1,77 +0,0 @@
/*
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

@@ -1,259 +0,0 @@
/*
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,7 +5,3 @@ 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,7 +16,6 @@ ApplicationWindow {
property int imageHeight: -1
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("Image View - %1", filename)

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ 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
*/
@@ -46,6 +47,18 @@ 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

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

View File

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

View File

@@ -7,23 +7,38 @@ 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.OverlaySheet {
Kirigami.Page {
property string sourceText
topPadding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
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
repository: Repository
definition: "JSON"
SyntaxHighlighter {
textEdit: sourceTextArea
definition: "JSON"
repository: Repository
}
}
}
}

View File

@@ -303,16 +303,14 @@ Kirigami.ScrollablePage {
spacing: 0
Repeater {
id: accountList
model: AccountListModel {
id: accountListModel
}
model: AccountRegistry
delegate: Kirigami.BasicListItem {
checkable: true
checked: Controller.activeConnection && Controller.activeConnection.localUser.id === model.user.id
checked: Controller.activeConnection && Controller.activeConnection.localUserId === model.connection.localUserId
onClicked: Controller.activeConnection = model.connection
Layout.fillWidth: true
Layout.fillHeight: true
text: model.user.id
text: model.connection.localUserId
}
}
}

View File

@@ -28,6 +28,8 @@ Kirigami.ScrollablePage {
title: currentRoom.htmlSafeDisplayName
KeyNavigation.left: pageStack.get(0)
Connections {
target: RoomManager
function onCurrentRoomChanged() {
@@ -235,7 +237,6 @@ Kirigami.ScrollablePage {
readonly property bool isLoaded: page.width * page.height > 10
spacing: Config.compactLayout ? 1 : Kirigami.Units.smallSpacing
reuseItems: true
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
@@ -354,6 +355,7 @@ 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
}
@@ -711,12 +713,6 @@ Kirigami.ScrollablePage {
MessageDelegateContextMenu {}
}
Component {
id: messageSourceSheet
MessageSourceSheet {}
}
Component {
id: fileDelegateContextMenu

View File

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

View File

@@ -29,6 +29,11 @@ Kirigami.CategorizedSettings {
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"

View File

@@ -0,0 +1,336 @@
// 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,3 +1,4 @@
module NeoChat.Settings
ThemeRadioButton 1.0 ThemeRadioButton.qml
SettingsPage 1.0 SettingsPage.qml
SonnetConfigPage 1.0 SonnetConfigPage.qml

View File

@@ -122,3 +122,40 @@ 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

@@ -118,7 +118,7 @@
<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 el mòbils i a l'escriptori, proporcionant un 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="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,6 +188,29 @@
<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

@@ -31,9 +31,7 @@ Kirigami.ApplicationWindow {
property bool roomListLoaded: false
property RoomPage roomPage: RoomPage {
KeyNavigation.left: pageStack.get(0)
}
property RoomPage roomPage
Connections {
target: root.quitAction
@@ -80,7 +78,7 @@ Kirigami.ApplicationWindow {
target: RoomManager
function onPushRoom(room, event) {
pageStack.push(root.roomPage);
root.roomPage = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
root.roomPage.forceActiveFocus();
if (event.length > 0) {
roomPage.goToEvent(event);
@@ -309,6 +307,18 @@ Kirigami.ApplicationWindow {
}
}
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
@@ -324,17 +334,8 @@ 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(i18nc("%1: %2", error, detail));
showPassiveNotification(i18n("%1: %2", error, detail));
}
function onShowWindow() {

View File

@@ -26,10 +26,6 @@
<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>
@@ -81,6 +77,7 @@
<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,7 +4,6 @@
# SPDX-License-Identifier: BSD-2-Clause
add_executable(neochat
accountlistmodel.cpp
controller.cpp
actionshandler.cpp
emojimodel.cpp
@@ -42,6 +41,8 @@ add_executable(neochat
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)
@@ -64,18 +65,6 @@ target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PRIVATE Qt::Quick Qt::Qml Qt::Gui Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::QCoro)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(TARGET KF5::Purpose)
function(add_share_plugin name)
kcoreaddons_add_plugin(${name} SOURCES ${ARGN} INSTALL_NAMESPACE "kf5/purpose")
target_link_libraries(${name} Qt5::Core KF5::Purpose)
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kf5/purpose")
endfunction()
add_share_plugin(neochatpurposeplugin neochatpurposeplugin.cpp)
target_link_libraries(neochatpurposeplugin KF5::I18n KF5::Service)
endif()
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PRIVATE NEOCHAT_FLATPAK)
endif()

View File

@@ -1,60 +0,0 @@
// 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;
}

View File

@@ -1,26 +0,0 @@
// 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;
};

98
src/accountregistry.cpp Normal file
View File

@@ -0,0 +1,98 @@
// 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;
}

48
src/accountregistry.h Normal file
View File

@@ -0,0 +1,48 @@
// 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

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

View File

@@ -40,8 +40,6 @@ class Controller : public QObject
public:
static Controller &instance();
[[nodiscard]] QVector<Connection *> connections() const;
void setActiveConnection(Connection *connection);
[[nodiscard]] Connection *activeConnection() const;
@@ -97,7 +95,6 @@ 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 : qAsConst(d->emojies)) {
for (const auto &emoji : std::as_const(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 : qAsConst(d->emojies)) {
for (const auto &emoji : std::as_const(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, [=]() {
connect(job, &BaseJob::failure, this, [this, index, oldName]() {
beginResetModel();
m_devices[index].displayName = oldName;
endResetModel();

View File

@@ -27,7 +27,7 @@ void Login::init()
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &Login::matrixIdChanged, this, [=]() {
connect(this, &Login::matrixIdChanged, this, [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, [=]() {
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [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, [=] {
connect(m_connection, &Connection::connected, this, [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, [=](QString error, const QString &, int, int) {
connect(m_connection, &Connection::networkError, this, [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, [=](QString error, const QString &) {
connect(m_connection, &Connection::loginError, this, [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, [=]() {
connectSingleShot(m_connection, &Connection::syncDone, this, [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, [=]() {
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
});

View File

@@ -30,7 +30,7 @@
#include "neochat-version.h"
#include "accountlistmodel.h"
#include "accountregistry.h"
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatboxhelper.h"
@@ -180,7 +180,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));
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<SpellcheckHighlighter>("org.kde.neochat", 1, 0, "SpellcheckHighlighter");

View File

@@ -58,12 +58,12 @@ MessageEventModel::MessageEventModel(QObject *parent)
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
qRegisterMetaType<FileTransferInfo>();
QTimer::singleShot(0, this, [=]() {
QTimer::singleShot(0, this, [this]() {
if (!m_currentRoom) {
return;
}
m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [=]() {
connect(this, &QAbstractListModel::rowsInserted, this, [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, [=](RoomEventsRange events) {
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [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, [=](RoomEventsRange events) {
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
}
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
});
connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) {
connect(m_currentRoom, &Room::addedMessages, this, [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, [=](const QString &fromEventId, const QString &toEventId) {
connect(m_currentRoom, &Room::readMarkerMoved, this, [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, [=] {
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
beginResetModel();
endResetModel();
});

View File

@@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <purpose/pluginbase.h>
#include <KApplicationTrader>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QDesktopServices>
#include <QJsonArray>
#include <QProcess>
#include <QStandardPaths>
#include <QUrl>
#include <QUrlQuery>
EXPORT_SHARE_VERSION
namespace
{
class NeoChatShareJob : public Purpose::Job
{
Q_OBJECT
public:
explicit NeoChatShareJob(QObject *parent = nullptr)
: Purpose::Job(parent)
{
}
void start() override
{
}
};
}
class Q_DECL_EXPORT NeoChatPurposePlugin : public Purpose::PluginBase
{
Q_OBJECT
public:
NeoChatPurposePlugin(QObject *parent, const QVariantList &)
: Purpose::PluginBase(parent)
{
}
Purpose::Job *createJob() const override
{
return new NeoChatShareJob(nullptr);
}
};
K_PLUGIN_CLASS_WITH_JSON(NeoChatPurposePlugin, "neochatpurposeplugin.json")
#include "neochatpurposeplugin.moc"

View File

@@ -1,22 +0,0 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Carl Schwan"
}
],
"Category": "Utilities",
"Description": "Send vith NeoChat",
"Icon": "mail-message",
"License": "GPL",
"Name": "Send with NeoChat",
"X-Purpose-ActionDisplay": "Send with NeoChat..."
},
"X-Purpose-Configuration": [],
"X-Purpose-Constraints": [],
"X-Purpose-PluginTypes": [
"Export",
"ShareUrl"
]
}

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, [=] {
connect(this, &Room::fileTransferCompleted, this, [this] {
setFileUploadingProgress(0);
setHasFileUploading(false);
});
@@ -52,12 +52,27 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
connect(this, &Room::joinStateChanged, this, [=](JoinState oldState, JoinState newState) {
connect(this, &Room::joinStateChanged, this, [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)
@@ -68,19 +83,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, [=](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
}
});
connect(this, &Room::fileTransferFailed, [=](const QString &id, const QString & /*error*/) {
connect(this, &Room::fileTransferFailed, [this, txnId](const QString &id, const QString & /*error*/) {
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
}
});
connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) {
connect(this, &Room::fileTransferProgress, [this, txnId](const QString &id, qint64 progress, qint64 total) {
if (id == txnId) {
setFileUploadingProgress(int(float(progress) / float(total) * 100));
}

View File

@@ -71,3 +71,30 @@ 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,6 +21,7 @@ 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, [=] {
connect(job, &BaseJob::finished, this, [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 : qAsConst(m_rooms)) {
for (NeoChatRoom *room : std::as_const(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, [=](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) {
connect(connection, &Connection::directChatsListChanged, this, [this, connection](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) {
auto refreshRooms = [this, &connection](Quotient::DirectChatsMap rooms) {
for (const QString &roomID : qAsConst(rooms)) {
for (const QString &roomID : std::as_const(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, [=] {
connect(room, &Room::displaynameChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::unreadMessagesChanged, this, [=] {
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::notificationCountChanged, this, [=] {
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::avatarChanged, this, [this, room] {
refresh(room, {AvatarRole});
});
connect(room, &Room::tagsChanged, this, [=] {
connect(room, &Room::tagsChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::joinStateChanged, this, [=] {
connect(room, &Room::joinStateChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::addedMessages, this, [=] {
connect(room, &Room::addedMessages, this, [this, room] {
refresh(room, {LastEventRole});
});
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
connect(room, &Room::highlightCountChanged, this, [=] {
connect(room, &Room::highlightCountChanged, this, [this, room] {
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, [=]() {
connect(job, &BaseJob::success, this, [this, job]() {
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 : qAsConst(m_rooms)) {
for (auto room : std::as_const(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 : qAsConst(m_rooms)) {
for (const auto &room : std::as_const(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, [=](Quotient::Room *room) {
connectSingleShot(account, &Quotient::Connection::newRoom, this, [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, [=] {
connect(job, &BaseJob::finished, this, [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 : qAsConst(m_users)) {
for (User *user : std::as_const(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 : qAsConst(m_users)) {
for (User *user : std::as_const(m_users)) {
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [=]() {
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [=] {
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this] {
setRoom(nullptr);
});
qDebug() << m_users.count() << "user(s) in the room";
@@ -154,7 +154,7 @@ void UserListModel::userAdded(Quotient::User *user)
m_users.insert(pos, user);
endInsertRows();
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [=]() {
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else