Compare commits

...

61 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
Carl Schwan
f2cf82ee8e Fix double quoting and missing new lines in message sent
* Don't encode text inside code block
* Make sure to replace \n with <br> in the html rendering. It's not
  respecting the common mark spec but this is the same behavior as
  Element
2021-10-23 20:35:19 +00:00
Carl Schwan
a146fab5a0 Fix color of Pane in room info drawer
This is temporary hack and the real solution is to add a Pane
implementation in qqc2-desktop-style
2021-10-23 20:33:34 +00:00
l10n daemon script
fb6745b49a GIT_SILENT made messages (after extraction) 2021-10-23 00:17:59 +00:00
Carl Schwan
6c3ae87340 Support resizing right drawer
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-10-21 23:04:52 +02:00
Carl Schwan
6afeaf1619 Move copy pasted to TextDelegate component
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-10-21 22:18:47 +02:00
Carl Schwan
890860df92 Improve room setting
* Port away from OverlaySheet
* Use Kirigami.CategorizedSettings
* Add join rules (read only for now)
2021-10-21 20:00:50 +00:00
Carl Schwan
6b8358874a Simplify function call in RoomPage
Instead of passing every argument in the right order, pass the entire
model/event object to the context menu functions. This is less copy
pasta of code and the order of the args is now less likely to break in
the future.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-10-21 19:54:06 +00:00
Carl Schwan
fc9f37d4a4 Port global settings to Kirigami.CategorizedSettings 2021-10-21 19:38:03 +00:00
Tobias Fella
48e410196c Don't allow opening a room in a new window on mobile 2021-10-18 21:07:35 +00:00
Tobias Fella
65cc392805 Fix flicking the timeline 2021-10-18 15:35:56 +02:00
Tobias Fella
a6dd5b9a57 Escape html before processing text to be sent 2021-10-16 20:45:14 +02:00
Tobias Fella
1d7c20e1c7 Make user list search case insensitive 2021-10-16 18:18:07 +00:00
Tobias Fella
22609b21df Add custom eventToString message for power level events 2021-10-16 18:17:49 +00:00
Tobias Fella
6c5ca0ac9d FIx querying power levels
Fixes #422
2021-10-16 20:16:36 +02:00
Tobias Fella
b22ebf3671 Show number of joined users instead of joined+invited users in room drawer 2021-10-16 19:28:51 +02:00
Carl Schwan
ec1cc34855 Fix missing import 2021-10-16 18:11:59 +02:00
Carl Schwan
a5aafde331 Unify look of loading pages 2021-10-16 17:59:31 +02:00
Carl Schwan
d42ad85b30 Port to OverlaySheet.title 2021-10-16 17:44:42 +02:00
Tobias Fella
8648b4a3bf Fix copying whole messages 2021-10-14 22:14:36 +02:00
Tobias Fella
bdca636fb8 Copy only selected text instead of whole message
Fixes #457
2021-10-14 21:44:32 +02:00
77 changed files with 1555 additions and 1303 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ neochat.kdev4
compile_commands.json compile_commands.json
.cache/ .cache/
.vscode/ .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) cmake_minimum_required(VERSION 3.16)
project(NeoChat) project(NeoChat)
set(PROJECT_VERSION "21.12")
set(KF5_MIN_VERSION "5.86.0") set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.0") set(QT_MIN_VERSION "5.15.2")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) 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 # 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) cmake_policy(SET CMP0063 OLD)
ecm_setup_version(1.2.80 ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
) )
@@ -114,6 +115,10 @@ find_package(QCoro REQUIRED)
qcoro_enable_coroutines() 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.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) 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) 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.neochat" package="org.kde.neochat"
android:versionName="0.0.1" android:versionName="${versionName}"
android:versionCode="1604412458" android:versionCode="${versionCode}"
android:installLocation="auto"> android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat"> <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" <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 Qt.labs.platform 1.1 as Platform
import QtQuick.Window 2.15 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 import org.kde.neochat 1.0
ToolBar { ToolBar {
@@ -69,7 +68,7 @@ ToolBar {
font: inputField.font font: inputField.font
} }
T.TextArea { TextArea {
id: inputField id: inputField
focus: true focus: true
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas. /* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
@@ -101,16 +100,9 @@ ToolBar {
wrapMode: Text.Wrap wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption readOnly: currentRoom.usesEncryption
palette: Kirigami.Theme.palette
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false 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 color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor selectionColor: Kirigami.Theme.highlightColor
@@ -119,65 +111,6 @@ ToolBar {
selectByMouse: !Kirigami.Settings.tabletMode 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 { ChatDocumentHandler {
id: documentHandler id: documentHandler
document: inputField.textDocument document: inputField.textDocument
@@ -187,18 +120,6 @@ ToolBar {
room: currentRoom ?? null 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 { Timer {
id: timeoutTimer id: timeoutTimer
repeat: false repeat: false
@@ -239,9 +160,6 @@ ToolBar {
} }
Keys.onPressed: { Keys.onPressed: {
// trigger if context menu button is pressed
TextFieldContextMenu.targetKeyPressed(event, inputField)
if (event.key === Qt.Key_PageDown) { if (event.key === Qt.Key_PageDown) {
switchRoomDown(); switchRoomDown();
} else if (event.key === Qt.Key_PageUp) { } else if (event.key === Qt.Key_PageUp) {
@@ -325,10 +243,7 @@ ToolBar {
chatBar.complete(); chatBar.complete();
} }
onPressed: MobileTextActionsToolBar.shouldBeVisible = true;
onTextChanged: { onTextChanged: {
MobileTextActionsToolBar.shouldBeVisible = false;
timeoutTimer.restart() timeoutTimer.restart()
repeatTimer.start() repeatTimer.start()
currentRoom.cachedInput = text currentRoom.cachedInput = text

View File

@@ -86,6 +86,7 @@ Popup {
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : "" source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
} }
labelItem.textFormat: Text.PlainText
text: modelData.displayName text: modelData.displayName
onClicked: completeTriggered(); 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 AttachmentPane 1.0 AttachmentPane.qml
CompletionMenu 1.0 CompletionMenu.qml CompletionMenu 1.0 CompletionMenu.qml
EmojiPickerPane 1.0 EmojiPickerPane.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 property int imageHeight: -1
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("Image View - %1", filename) title: i18n("Image View - %1", filename)

View File

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

View File

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

View File

@@ -17,8 +17,17 @@ TextEdit {
property bool isEmote: false property bool isEmote: false
property string textMessage: model.display property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage) property bool spoilerRevealed: !hasSpoiler.test(textMessage)
property bool hasContextMenu: true
signal requestOpenMessageContext()
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage)) 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> text: "<style>
table { table {
width:100%; width:100%;
@@ -53,8 +62,6 @@ a{
wrapMode: Text.Wrap wrapMode: Text.Wrap
textFormat: Text.RichText textFormat: Text.RichText
Layout.fillWidth: true
onLinkActivated: RoomManager.openResource(link) onLinkActivated: RoomManager.openResource(link)
onHoveredLinkChanged: if (hoveredLink.length > 0) { onHoveredLinkChanged: if (hoveredLink.length > 0) {
applicationWindow().hoverLinkIndicator.text = hoveredLink; applicationWindow().hoverLinkIndicator.text = hoveredLink;
@@ -70,4 +77,16 @@ a{
enabled: !parent.hoveredLink && !spoilerRevealed enabled: !parent.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true 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,6 +90,7 @@ Loader {
id: typingLabel id: typingLabel
elide: Text.ElideRight elide: Text.ElideRight
text: root.labelText text: root.labelText
textFormat: Text.PlainText
} }
} }

View File

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

View File

@@ -1,206 +0,0 @@
// 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,13 +27,7 @@ Kirigami.OverlaySheet {
rightPadding: 0 rightPadding: 0
topPadding: 0 topPadding: 0
header: Kirigami.Heading { title: i18nc("@title:menu Account detail dialog", "Account detail")
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 { contentItem: ColumnLayout {
spacing: 0 spacing: 0

View File

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

View File

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

View File

@@ -5,9 +5,12 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
import NeoChat.Page 1.0 import NeoChat.Page 1.0
/** /**
* Context menu when clicking on a room in the room list * Context menu when clicking on a room in the room list
*/ */
@@ -16,11 +19,15 @@ Menu {
property var room property var room
MenuItem { MenuItem {
id: newWindow
text: i18n("Open in new window") text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room); onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
} }
MenuSeparator {} MenuSeparator {
visible: newWindow.visible
}
MenuItem { MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites") text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
@@ -40,6 +47,18 @@ Menu {
onTriggered: room.markAllMessagesAsRead() 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 {} MenuSeparator {}
MenuItem { 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") text: i18n("View Source")
icon.name: "code-context" icon.name: "code-context"
onTriggered: { 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

@@ -43,13 +43,18 @@ Loader {
Kirigami.Action { Kirigami.Action {
text: i18n("Copy") text: i18n("Copy")
icon.name: "edit-copy" icon.name: "edit-copy"
onTriggered: Clipboard.saveText(message) onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.message : loadRoot.selectedText)
}, },
Kirigami.Action { Kirigami.Action {
text: i18n("View Source") text: i18n("View Source")
icon.name: "code-context" icon.name: "code-context"
onTriggered: { 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,25 +7,38 @@ import QtQuick.Controls 2.15
import org.kde.syntaxhighlighting 1.0 import org.kde.syntaxhighlighting 1.0
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.OverlaySheet { Kirigami.Page {
property string sourceText property string sourceText
header: Kirigami.Heading { topPadding: 0
text: i18n("Message Source") leftPadding: 0
} rightPadding: 0
bottomPadding: 0
TextArea { title: i18n("Message Source")
id: sourceTextArea
text: sourceText
readOnly: true
wrapMode: Text.WordWrap
SyntaxHighlighter { ScrollView {
textEdit: sourceTextArea anchors.fill: parent
repository: Repository contentWidth: availableWidth
definition: "JSON" 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
}
} }
} }
} }

View File

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

View File

@@ -303,16 +303,14 @@ Kirigami.ScrollablePage {
spacing: 0 spacing: 0
Repeater { Repeater {
id: accountList id: accountList
model: AccountListModel { model: AccountRegistry
id: accountListModel
}
delegate: Kirigami.BasicListItem { delegate: Kirigami.BasicListItem {
checkable: true 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 onClicked: Controller.activeConnection = model.connection
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
text: model.user.id text: model.connection.localUserId
} }
} }
} }

View File

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

View File

@@ -1,88 +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 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,6 +5,5 @@ RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml
ImageEditorPage 1.0 ImageEditorPage.qml ImageEditorPage 1.0 ImageEditorPage.qml

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -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,2 +1,4 @@
module NeoChat.Settings module NeoChat.Settings
ThemeRadioButton 1.0 ThemeRadioButton.qml 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[x-test]=xxThere is a new messagexx
Comment[zh_CN]=有新消息 Comment[zh_CN]=有新消息
Action=Popup 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 xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
<p>NeoChat works both on mobile and desktop while providing a consistent user experience.</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="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 el mòbils i a l'escriptori, proporcionant un experiència d'usuari coherent.</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="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="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> <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_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <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"> <release version="1.2.0" date="2021-06-01">
<description> <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> <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 bool roomListLoaded: false
property RoomPage roomPage: RoomPage { property RoomPage roomPage
KeyNavigation.left: pageStack.get(0)
}
Connections { Connections {
target: root.quitAction target: root.quitAction
@@ -80,7 +78,7 @@ Kirigami.ApplicationWindow {
target: RoomManager target: RoomManager
function onPushRoom(room, event) { function onPushRoom(room, event) {
pageStack.push(root.roomPage); root.roomPage = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
root.roomPage.forceActiveFocus(); root.roomPage.forceActiveFocus();
if (event.length > 0) { if (event.length > 0) {
roomPage.goToEvent(event); roomPage.goToEvent(event);
@@ -151,7 +149,6 @@ Kirigami.ApplicationWindow {
contextDrawer: RoomDrawer { contextDrawer: RoomDrawer {
id: contextDrawer id: contextDrawer
contentItem.implicitWidth: columnWidth
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
modal: !root.wideScreen || !enabled modal: !root.wideScreen || !enabled
onEnabledChanged: drawerOpen = enabled && !modal onEnabledChanged: drawerOpen = enabled && !modal
@@ -255,7 +252,7 @@ Kirigami.ApplicationWindow {
Kirigami.Action { Kirigami.Action {
text: i18n("Settings") text: i18n("Settings")
icon.name: "settings-configure" icon.name: "settings-configure"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml") onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings") enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: StandardKey.Preferences shortcut: StandardKey.Preferences
}, },
@@ -310,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 { Connections {
target: Controller target: Controller
@@ -325,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) { function onGlobalErrorOccured(error, detail) {
showPassiveNotification(i18nc("%1: %2", error, detail)); showPassiveNotification(i18n("%1: %2", error, detail));
} }
function onShowWindow() { function onShowWindow() {
@@ -361,9 +361,7 @@ Kirigami.ApplicationWindow {
property string url: "" property string url: ""
header: Kirigami.Heading { title: i18n("User consent")
text: i18n("User consent")
}
QQC2.Label { QQC2.Label {
id: label id: label
@@ -402,9 +400,7 @@ Kirigami.ApplicationWindow {
required property var user; required property var user;
parent: QQC2.ApplicationWindow.overlay parent: QQC2.ApplicationWindow.overlay
header: Kirigami.Heading { title: i18n("Start a chat")
text: i18n("Start a chat")
}
contentItem: QQC2.Label { contentItem: QQC2.Label {
text: i18n("Do you want to start a chat with %1?", user.displayName) text: i18n("Do you want to start a chat with %1?", user.displayName)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap

16
res.qrc
View File

@@ -6,14 +6,14 @@
<file>imports/NeoChat/Page/RoomListPage.qml</file> <file>imports/NeoChat/Page/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file> <file>imports/NeoChat/Page/RoomPage.qml</file>
<file>imports/NeoChat/Page/RoomWindow.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/JoinRoomPage.qml</file>
<file>imports/NeoChat/Page/InviteUserPage.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/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.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/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/qmldir</file>
<file>imports/NeoChat/Component/FullScreenImage.qml</file> <file>imports/NeoChat/Component/FullScreenImage.qml</file>
<file>imports/NeoChat/Component/FancyEffectsContainer.qml</file> <file>imports/NeoChat/Component/FancyEffectsContainer.qml</file>
@@ -26,10 +26,6 @@
<file>imports/NeoChat/Component/ChatBox/AttachmentPane.qml</file> <file>imports/NeoChat/Component/ChatBox/AttachmentPane.qml</file>
<file>imports/NeoChat/Component/ChatBox/ReplyPane.qml</file> <file>imports/NeoChat/Component/ChatBox/ReplyPane.qml</file>
<file>imports/NeoChat/Component/ChatBox/CompletionMenu.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/ChatBox/qmldir</file>
<file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file> <file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file>
<file>imports/NeoChat/Component/Emoji/qmldir</file> <file>imports/NeoChat/Component/Emoji/qmldir</file>
@@ -57,7 +53,6 @@
<file>imports/NeoChat/Panel/qmldir</file> <file>imports/NeoChat/Panel/qmldir</file>
<file>imports/NeoChat/Panel/RoomDrawer.qml</file> <file>imports/NeoChat/Panel/RoomDrawer.qml</file>
<file>imports/NeoChat/Dialog/qmldir</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/UserDetailDialog.qml</file>
<file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file> <file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file>
<file>imports/NeoChat/Dialog/EmojiDialog.qml</file> <file>imports/NeoChat/Dialog/EmojiDialog.qml</file>
@@ -73,11 +68,16 @@
<file>qtquickcontrols2.conf</file> <file>qtquickcontrols2.conf</file>
<file>imports/NeoChat/Component/glowdot.png</file> <file>imports/NeoChat/Component/glowdot.png</file>
<file>imports/NeoChat/Component/confetti.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/ThemeRadioButton.qml</file>
<file>imports/NeoChat/Settings/ColorScheme.qml</file> <file>imports/NeoChat/Settings/ColorScheme.qml</file>
<file>imports/NeoChat/Settings/GeneralSettingsPage.qml</file> <file>imports/NeoChat/Settings/GeneralSettingsPage.qml</file>
<file>imports/NeoChat/Settings/Emoticons.qml</file> <file>imports/NeoChat/Settings/Emoticons.qml</file>
<file>imports/NeoChat/Settings/AppearanceSettingsPage.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> <file>imports/NeoChat/Settings/qmldir</file>
</qresource> </qresource>
</RCC> </RCC>

View File

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

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

@@ -106,7 +106,20 @@ void ActionsHandler::postMessage(const QString &text,
CustomEmojiModel *cem) CustomEmojiModel *cem)
{ {
QString rawText = text; QString rawText = text;
QString cleanedText = 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("```");
}
}
auto preprocess = [cem](const QString &it) -> QString { auto preprocess = [cem](const QString &it) -> QString {
if (cem == nullptr) { if (cem == nullptr) {

View File

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

View File

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

View File

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

View File

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

16
src/joinrulesevent.cpp Normal file
View File

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

29
src/joinrulesevent.h Normal file
View File

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

View File

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

View File

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

View File

@@ -48,6 +48,9 @@
<entry name="RoomListPageWidth" type="int"> <entry name="RoomListPageWidth" type="int">
<default>-1</default> <default>-1</default>
</entry> </entry>
<entry name="RoomDrawerWidth" type="int">
<default>-1</default>
</entry>
</group> </group>
<group name="Timeline"> <group name="Timeline">
<entry name="ShowAvatarInTimeline" type="bool"> <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::notificationCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &NeoChatRoom::highlightCountChanged, this, &NeoChatRoom::countChanged); connect(this, &NeoChatRoom::highlightCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &Room::fileTransferCompleted, this, [=] { connect(this, &Room::fileTransferCompleted, this, [this] {
setFileUploadingProgress(0); setFileUploadingProgress(0);
setHasFileUploading(false); setHasFileUploading(false);
}); });
@@ -52,12 +52,27 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged); 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) { if (oldState == JoinState::Invite && newState != JoinState::Invite) {
Q_EMIT isInviteChanged(); Q_EMIT isInviteChanged();
} }
}); });
connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged); 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) 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); QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
setHasFileUploading(true); 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) { if (id == txnId) {
setFileUploadingProgress(0); setFileUploadingProgress(0);
setHasFileUploading(false); 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) { if (id == txnId) {
setFileUploadingProgress(0); setFileUploadingProgress(0);
setHasFileUploading(false); 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) { if (id == txnId) {
setFileUploadingProgress(int(float(progress) / float(total) * 100)); setFileUploadingProgress(int(float(progress) / float(total) * 100));
} }
@@ -462,6 +477,9 @@ 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()) 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()); : 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) { [](const StateEventBase &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) { if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room"); return i18n("changed the server access control lists for this room");
@@ -732,6 +750,11 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user)
doDeleteMessagesByUser(user); doDeleteMessagesByUser(user);
} }
QString NeoChatRoom::joinRule() const
{
return getCurrentState<JoinRulesEvent>()->joinRule();
}
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user) QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user)
{ {
QStringList events; QStringList events;

View File

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

View File

@@ -71,3 +71,30 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
m_notifications.insert(room->id(), notification); 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 Q_INVOKABLE void
postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon, const QString &replyEventId); postNotification(NeoChatRoom *room, const QString &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: private:
NotificationsManager(QObject *parent = nullptr); 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}); 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; attempted = true;
if (job->status() == BaseJob::Success) { if (job->status() == BaseJob::Success) {

View File

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

View File

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

View File

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