mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
Merge branch 'master' into nhekoRoomDirectory
This commit is contained in:
commit
2dfccda73c
61 changed files with 2099 additions and 1313 deletions
|
@ -52,14 +52,14 @@ build-macos:
|
|||
stage: build
|
||||
tags: [macos]
|
||||
before_script:
|
||||
- brew update
|
||||
- brew reinstall --force python3
|
||||
- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
||||
#- brew update
|
||||
#- brew reinstall --force python3
|
||||
#- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
||||
- pip3 install dmgbuild
|
||||
- rm -rf ../.hunter && mv .hunter ../.hunter || true
|
||||
script:
|
||||
- export PATH=/usr/local/opt/qt/bin/:${PATH}
|
||||
- export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
|
||||
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
|
||||
- export CMAKE_PREFIX_PATH=/usr/local/opt/qt@5
|
||||
- cmake -GNinja -H. -Bbuild
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
-DCMAKE_INSTALL_PREFIX=.deps/usr
|
||||
|
@ -91,7 +91,9 @@ build-flatpak-amd64:
|
|||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
# need flatpak 1.11.1 at least
|
||||
- apt-get update && apt-get install -y software-properties-common
|
||||
- add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||
|
@ -119,7 +121,9 @@ build-flatpak-arm64:
|
|||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker-arm64]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
# need flatpak 1.11.1 at least
|
||||
- apt-get update && apt-get install -y software-properties-common
|
||||
- add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||
|
|
|
@ -286,7 +286,6 @@ set(SRC_FILES
|
|||
src/dialogs/Logout.cpp
|
||||
src/dialogs/PreviewUploadOverlay.cpp
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
src/dialogs/ReadReceipts.cpp
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.cpp
|
||||
|
@ -305,7 +304,6 @@ set(SRC_FILES
|
|||
src/timeline/RoomlistModel.cpp
|
||||
|
||||
# UI components
|
||||
src/ui/Avatar.cpp
|
||||
src/ui/Badge.cpp
|
||||
src/ui/DropShadow.cpp
|
||||
src/ui/FlatButton.cpp
|
||||
|
@ -352,6 +350,7 @@ set(SRC_FILES
|
|||
src/MemberList.cpp
|
||||
src/MxcImageProvider.cpp
|
||||
src/Olm.cpp
|
||||
src/ReadReceiptsModel.cpp
|
||||
src/RegisterPage.cpp
|
||||
src/SSOHandler.cpp
|
||||
src/CombinedImagePackModel.cpp
|
||||
|
@ -383,7 +382,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||
GIT_TAG 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
|
||||
GIT_TAG bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
|
||||
)
|
||||
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
||||
|
@ -498,9 +497,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/dialogs/LeaveRoom.h
|
||||
src/dialogs/Logout.h
|
||||
src/dialogs/PreviewUploadOverlay.h
|
||||
src/dialogs/RawMessage.h
|
||||
src/dialogs/ReCaptcha.h
|
||||
src/dialogs/ReadReceipts.h
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.h
|
||||
|
@ -518,7 +515,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/timeline/RoomlistModel.h
|
||||
|
||||
# UI components
|
||||
src/ui/Avatar.h
|
||||
src/ui/Badge.h
|
||||
src/ui/FlatButton.h
|
||||
src/ui/FloatingButton.h
|
||||
|
@ -546,24 +542,26 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
|
||||
src/AvatarProvider.h
|
||||
src/BlurhashProvider.h
|
||||
src/Cache_p.h
|
||||
src/CacheCryptoStructs.h
|
||||
src/Cache_p.h
|
||||
src/CallDevices.h
|
||||
src/CallManager.h
|
||||
src/ChatPage.h
|
||||
src/Clipboard.h
|
||||
src/CombinedImagePackModel.h
|
||||
src/CompletionProxyModel.h
|
||||
src/DeviceVerificationFlow.h
|
||||
src/ImagePackListModel.h
|
||||
src/InviteesModel.h
|
||||
src/LoginPage.h
|
||||
src/MainWindow.h
|
||||
src/MemberList.h
|
||||
src/MxcImageProvider.h
|
||||
src/Olm.h
|
||||
src/RegisterPage.h
|
||||
src/RoomsModel.h
|
||||
src/SSOHandler.h
|
||||
src/CombinedImagePackModel.h
|
||||
src/SingleImagePackModel.h
|
||||
src/ImagePackListModel.h
|
||||
src/TrayIcon.h
|
||||
src/UserSettingsPage.h
|
||||
src/UsersModel.h
|
||||
|
@ -571,7 +569,8 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/RoomDirectoryModel.h
|
||||
src/WebRTCSession.h
|
||||
src/WelcomePage.h
|
||||
)
|
||||
src/ReadReceiptsModel.h
|
||||
)
|
||||
|
||||
#
|
||||
# Bundle translations.
|
||||
|
|
|
@ -19,6 +19,8 @@ finish-args:
|
|||
- --talk-name=org.freedesktop.secrets
|
||||
- --talk-name=org.freedesktop.StatusNotifierItem
|
||||
- --talk-name=org.kde.*
|
||||
# needed for SingleApplication to work
|
||||
- --allow=per-app-dev-shm
|
||||
cleanup:
|
||||
- /include
|
||||
- /bin/mdb*
|
||||
|
@ -161,7 +163,7 @@ modules:
|
|||
buildsystem: cmake-ninja
|
||||
name: mtxclient
|
||||
sources:
|
||||
- commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
|
||||
- commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
|
||||
type: git
|
||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||
- config-opts:
|
||||
|
|
|
@ -11,10 +11,11 @@ import im.nheko 1.0
|
|||
Rectangle {
|
||||
id: avatar
|
||||
|
||||
property alias url: img.source
|
||||
property string url
|
||||
property string userid
|
||||
property string displayName
|
||||
property alias textColor: label.color
|
||||
property bool crop: true
|
||||
|
||||
signal clicked(var mouse)
|
||||
|
||||
|
@ -44,12 +45,13 @@ Rectangle {
|
|||
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: true
|
||||
sourceSize.width: avatar.width
|
||||
sourceSize.height: avatar.height
|
||||
layer.enabled: true
|
||||
source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
|
|
@ -30,12 +30,12 @@ ApplicationWindow {
|
|||
}
|
||||
|
||||
title: qsTr("Invite users to %1").arg(plainRoomName)
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
height: 380
|
||||
width: 340
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(inviteDialogRoot)
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Enter"
|
||||
|
|
|
@ -7,7 +7,7 @@ import "./voip"
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
|
|
|
@ -10,7 +10,7 @@ import QtGraphicalEffects 1.0
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ScrollView {
|
||||
|
@ -212,9 +212,9 @@ ScrollView {
|
|||
|
||||
// force current read index to update
|
||||
onTriggered: {
|
||||
if (chat.model) {
|
||||
if (chat.model)
|
||||
chat.model.setCurrentIndex(chat.model.currentIndex);
|
||||
}
|
||||
|
||||
}
|
||||
interval: 1000
|
||||
}
|
||||
|
@ -349,6 +349,7 @@ ScrollView {
|
|||
required property string callType
|
||||
required property var reactions
|
||||
required property int trustlevel
|
||||
required property int encryptionError
|
||||
required property var timestamp
|
||||
required property int status
|
||||
required property int index
|
||||
|
@ -456,6 +457,7 @@ ScrollView {
|
|||
callType: wrapper.callType
|
||||
reactions: wrapper.reactions
|
||||
trustlevel: wrapper.trustlevel
|
||||
encryptionError: wrapper.encryptionError
|
||||
timestamp: wrapper.timestamp
|
||||
status: wrapper.status
|
||||
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
|
||||
|
@ -580,7 +582,7 @@ ScrollView {
|
|||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Read receip&ts")
|
||||
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
|
||||
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
|
|
52
resources/qml/RawMessageDialog.qml
Normal file
52
resources/qml/RawMessageDialog.qml
Normal file
|
@ -0,0 +1,52 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: rawMessageRoot
|
||||
|
||||
property alias rawMessage: rawMessageView.text
|
||||
|
||||
height: 420
|
||||
width: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(rawMessageRoot)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: rawMessageRoot.close()
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
palette: Nheko.colors
|
||||
padding: Nheko.paddingMedium
|
||||
|
||||
TextArea {
|
||||
id: rawMessageView
|
||||
|
||||
font: Nheko.monospaceFont()
|
||||
color: Nheko.colors.text
|
||||
readOnly: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Nheko.colors.base
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
standardButtons: DialogButtonBox.Ok
|
||||
onAccepted: rawMessageRoot.close()
|
||||
}
|
||||
|
||||
}
|
130
resources/qml/ReadReceipts.qml
Normal file
130
resources/qml/ReadReceipts.qml
Normal file
|
@ -0,0 +1,130 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: readReceiptsRoot
|
||||
|
||||
property ReadReceiptsProxy readReceipts
|
||||
property Room room
|
||||
|
||||
height: 380
|
||||
width: 340
|
||||
minimumHeight: 380
|
||||
minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(readReceiptsRoot)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: readReceiptsRoot.close()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
spacing: Nheko.paddingMedium
|
||||
|
||||
Label {
|
||||
id: headerTitle
|
||||
|
||||
color: Nheko.colors.text
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
text: qsTr("Read receipts")
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.5
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
palette: Nheko.colors
|
||||
padding: Nheko.paddingMedium
|
||||
ScrollBar.horizontal.visible: false
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 200
|
||||
Layout.fillWidth: true
|
||||
|
||||
ListView {
|
||||
id: readReceiptsList
|
||||
|
||||
clip: true
|
||||
spacing: Nheko.paddingMedium
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: readReceipts
|
||||
|
||||
delegate: RowLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
|
||||
Avatar {
|
||||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
userid: model.mxid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: model.displayName
|
||||
onClicked: room.openUserProfile(model.mxid)
|
||||
ToolTip.visible: avatarHover.hovered
|
||||
ToolTip.text: model.mxid
|
||||
|
||||
HoverHandler {
|
||||
id: avatarHover
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
Label {
|
||||
text: model.displayName
|
||||
color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
|
||||
font.pointSize: fontMetrics.font.pointSize
|
||||
ToolTip.visible: displayNameHover.hovered
|
||||
ToolTip.text: model.mxid
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: room.openUserProfile(userId)
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: displayNameHover
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
text: model.timestamp
|
||||
color: Nheko.colors.buttonText
|
||||
font.pointSize: fontMetrics.font.pointSize * 0.9
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
standardButtons: DialogButtonBox.Ok
|
||||
onAccepted: readReceiptsRoot.close()
|
||||
}
|
||||
|
||||
}
|
|
@ -179,31 +179,38 @@ Component {
|
|||
}
|
||||
]
|
||||
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
acceptedButtons: Qt.RightButton
|
||||
onSingleTapped: {
|
||||
if (!TimelineManager.isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
// NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that...
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onSingleTapped: {
|
||||
if (!TimelineManager.isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
|
||||
}
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||
}
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
onSingleTapped: Rooms.setCurrentRoom(roomId)
|
||||
onLongPressed: {
|
||||
if (!isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
onSingleTapped: Rooms.setCurrentRoom(roomId)
|
||||
onLongPressed: {
|
||||
if (!isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
margin: -Nheko.paddingSmall
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -439,6 +446,7 @@ Component {
|
|||
url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
|
||||
displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
|
||||
userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
|
||||
enabled: false
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
|
@ -6,7 +6,7 @@ import "./ui"
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Window 2.12
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
|
@ -15,13 +15,13 @@ ApplicationWindow {
|
|||
property MemberList members
|
||||
|
||||
title: qsTr("Members of %1").arg(members.roomName)
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
height: 650
|
||||
width: 420
|
||||
minimumHeight: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(roomMembersRoot)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
|
|
|
@ -7,7 +7,7 @@ import Qt.labs.platform 1.1 as Platform
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.3
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
|
@ -15,14 +15,13 @@ ApplicationWindow {
|
|||
|
||||
property var roomSettings
|
||||
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
minimumWidth: 420
|
||||
minimumHeight: 650
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(roomSettingsDialog)
|
||||
title: qsTr("Room Settings")
|
||||
|
||||
Shortcut {
|
||||
|
@ -155,7 +154,7 @@ ApplicationWindow {
|
|||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: 10
|
||||
rowSpacing: Nheko.paddingLarge
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("SETTINGS")
|
||||
|
@ -181,7 +180,7 @@ ApplicationWindow {
|
|||
}
|
||||
|
||||
MatrixText {
|
||||
text: "Room access"
|
||||
text: qsTr("Room access")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ import "./emoji"
|
|||
import "./voip"
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.15
|
||||
import im.nheko 1.0
|
||||
import im.nheko.EmojiModel 1.0
|
||||
|
||||
|
@ -96,6 +96,22 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: readReceiptsDialog
|
||||
|
||||
ReadReceipts {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rawMessageDialog
|
||||
|
||||
RawMessageDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
|
|
|
@ -23,6 +23,9 @@ MouseArea {
|
|||
// console.warn("Delta: ", wheel.pixelDelta.y);
|
||||
// console.warn("Old position: ", flickable.contentY);
|
||||
// console.warn("New position: ", newPos);
|
||||
// breaks ListView's with headers...
|
||||
//if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
|
||||
// minYExtent += flickableItem.headerItem.height;
|
||||
|
||||
id: root
|
||||
|
||||
|
@ -55,9 +58,6 @@ MouseArea {
|
|||
|
||||
var minYExtent = flickableItem.originY + flickableItem.topMargin;
|
||||
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
|
||||
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
|
||||
minYExtent += flickableItem.headerItem.height;
|
||||
|
||||
//Avoid overscrolling
|
||||
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ ImageButton {
|
|||
}
|
||||
onClicked: {
|
||||
if (status == MtxEvent.Read)
|
||||
room.readReceiptsAction(eventId);
|
||||
room.showReadReceipts(eventId);
|
||||
|
||||
}
|
||||
image: {
|
||||
|
|
|
@ -7,7 +7,7 @@ import "./emoji"
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
|
@ -38,6 +38,7 @@ Item {
|
|||
required property string callType
|
||||
required property var reactions
|
||||
required property int trustlevel
|
||||
required property int encryptionError
|
||||
required property var timestamp
|
||||
required property int status
|
||||
required property int relatedEventCacheBuster
|
||||
|
@ -110,6 +111,7 @@ Item {
|
|||
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
|
||||
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
|
||||
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
|
||||
encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
|
||||
relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
|
||||
}
|
||||
|
||||
|
@ -136,6 +138,7 @@ Item {
|
|||
roomTopic: r.roomTopic
|
||||
roomName: r.roomName
|
||||
callType: r.callType
|
||||
encryptionError: r.encryptionError
|
||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||
isReply: false
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import QtGraphicalEffects 1.0
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
import im.nheko.EmojiModel 1.0
|
||||
|
||||
|
@ -249,4 +249,23 @@ Item {
|
|||
roomid: room ? room.roomId : ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpenReadReceiptsDialog(rr) {
|
||||
var dialog = readReceiptsDialog.createObject(timelineRoot, {
|
||||
"readReceipts": rr,
|
||||
"room": room
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
function onShowRawMessageDialog(rawMessage) {
|
||||
var dialog = rawMessageDialog.createObject(timelineRoot, {
|
||||
"rawMessage": rawMessage
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
target: room
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,19 +4,20 @@
|
|||
|
||||
import "./device-verification"
|
||||
import "./ui"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.3
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
// this does not work in ApplicationWindow, just in Window
|
||||
//transientParent: Nheko.mainwindow()
|
||||
|
||||
id: userProfileDialog
|
||||
|
||||
property var profile
|
||||
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
height: 650
|
||||
width: 420
|
||||
minimumHeight: 420
|
||||
|
@ -24,7 +25,8 @@ ApplicationWindow {
|
|||
color: Nheko.colors.window
|
||||
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(userProfileDialog)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
|
|
133
resources/qml/components/AvatarListTile.qml
Normal file
133
resources/qml/components/AvatarListTile.qml
Normal file
|
@ -0,0 +1,133 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
property color bubbleText: Nheko.colors.highlightedText
|
||||
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
|
||||
required property string avatarUrl
|
||||
required property string title
|
||||
required property string subtitle
|
||||
required property int index
|
||||
required property int selectedIndex
|
||||
property bool crop: true
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
states: [
|
||||
State {
|
||||
name: "highlight"
|
||||
when: hovered.hovered && !(index == selectedIndex)
|
||||
|
||||
PropertyChanges {
|
||||
target: tile
|
||||
background: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
bubbleText: Nheko.colors.highlightedText
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "selected"
|
||||
when: index == selectedIndex
|
||||
|
||||
PropertyChanges {
|
||||
target: tile
|
||||
background: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
bubbleText: Nheko.colors.highlight
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
|
||||
Avatar {
|
||||
id: avatar
|
||||
|
||||
enabled: false
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
height: avatarSize
|
||||
width: avatarSize
|
||||
url: tile.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: title
|
||||
crop: tile.crop
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: textContent
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 100
|
||||
width: parent.width - avatar.width
|
||||
Layout.preferredWidth: parent.width - avatar.width
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ElidedLabel {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
color: tile.importantText
|
||||
elideWidth: textContent.width - Nheko.paddingMedium
|
||||
fullText: title
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ElidedLabel {
|
||||
color: tile.unimportantText
|
||||
font.pixelSize: fontMetrics.font.pixelSize * 0.9
|
||||
elideWidth: textContent.width - Nheko.paddingSmall
|
||||
fullText: subtitle
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
48
resources/qml/delegates/Encrypted.qml
Normal file
48
resources/qml/delegates/Encrypted.qml
Normal file
|
@ -0,0 +1,48 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: r
|
||||
|
||||
required property int encryptionError
|
||||
required property string eventId
|
||||
|
||||
width: parent ? parent.width : undefined
|
||||
|
||||
MatrixText {
|
||||
text: {
|
||||
switch (encryptionError) {
|
||||
case Olm.MissingSession:
|
||||
return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.");
|
||||
case Olm.MissingSessionIndex:
|
||||
return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message.");
|
||||
case Olm.DbError:
|
||||
return qsTr("There was an internal error reading the decryption key from the database.");
|
||||
case Olm.DecryptionFailed:
|
||||
return qsTr("There was an error decrypting this message.");
|
||||
case Olm.ParsingFailed:
|
||||
return qsTr("The message couldn't be parsed.");
|
||||
case Olm.ReplayAttack:
|
||||
return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!");
|
||||
default:
|
||||
return qsTr("Unknown decryption error");
|
||||
}
|
||||
}
|
||||
color: Nheko.colors.buttonText
|
||||
width: r ? r.width : undefined
|
||||
}
|
||||
|
||||
Button {
|
||||
palette: Nheko.colors
|
||||
visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex
|
||||
text: qsTr("Request key")
|
||||
onClicked: room.requestKeyForEvent(eventId)
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,7 @@ Item {
|
|||
required property string roomTopic
|
||||
required property string roomName
|
||||
required property string callType
|
||||
required property int encryptionError
|
||||
required property int relatedEventCacheBuster
|
||||
|
||||
height: chooser.childrenRect.height
|
||||
|
@ -189,6 +190,16 @@ Item {
|
|||
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.Encrypted
|
||||
|
||||
Encrypted {
|
||||
encryptionError: d.encryptionError
|
||||
eventId: d.eventId
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MtxEvent.Name
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
|
@ -30,6 +30,7 @@ Item {
|
|||
property string roomTopic
|
||||
property string roomName
|
||||
property string callType
|
||||
property int encryptionError
|
||||
property int relatedEventCacheBuster
|
||||
|
||||
width: parent.width
|
||||
|
@ -97,6 +98,7 @@ Item {
|
|||
roomName: r.roomName
|
||||
callType: r.callType
|
||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||
encryptionError: r.encryptionError
|
||||
enabled: false
|
||||
width: parent.width
|
||||
isReply: true
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Window 2.10
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
|
@ -14,13 +14,12 @@ ApplicationWindow {
|
|||
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||
title: stack.currentItem.title
|
||||
flags: Qt.Dialog
|
||||
modality: Qt.NonModal
|
||||
palette: Nheko.colors
|
||||
height: stack.implicitHeight
|
||||
width: stack.implicitWidth
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(dialog)
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
|
|
301
resources/qml/dialogs/ImagePackEditorDialog.qml
Normal file
301
resources/qml/dialogs/ImagePackEditorDialog.qml
Normal file
|
@ -0,0 +1,301 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import Qt.labs.platform 1.1
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
//Component.onCompleted: Nheko.reparent(win)
|
||||
|
||||
id: win
|
||||
|
||||
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
|
||||
property SingleImagePackModel imagePack
|
||||
property int currentImageIndex: -1
|
||||
readonly property int stickerDim: 128
|
||||
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
|
||||
|
||||
title: qsTr("Editing image pack")
|
||||
height: 600
|
||||
width: 600
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
|
||||
AdaptiveLayout {
|
||||
id: adaptiveView
|
||||
|
||||
anchors.fill: parent
|
||||
singlePageMode: false
|
||||
pageIndex: 0
|
||||
|
||||
AdaptiveLayoutElement {
|
||||
id: packlistC
|
||||
|
||||
visible: Settings.groupView
|
||||
minimumWidth: 200
|
||||
collapsedWidth: 200
|
||||
preferredWidth: 300
|
||||
maximumWidth: 300
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
//required property bool isEmote
|
||||
//required property bool isSticker
|
||||
|
||||
model: imagePack
|
||||
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
enabled: !Settings.mobileMode
|
||||
}
|
||||
|
||||
header: AvatarListTile {
|
||||
title: imagePack.packname
|
||||
avatarUrl: imagePack.avatarUrl
|
||||
subtitle: imagePack.statekey
|
||||
index: -1
|
||||
selectedIndex: currentImageIndex
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: currentImageIndex = -1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height - Nheko.paddingSmall * 2
|
||||
width: 3
|
||||
color: Nheko.colors.highlight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: Button {
|
||||
palette: Nheko.colors
|
||||
onClicked: addFilesDialog.open()
|
||||
width: ListView.view.width
|
||||
text: qsTr("Add images")
|
||||
|
||||
FileDialog {
|
||||
id: addFilesDialog
|
||||
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
fileMode: FileDialog.OpenFiles
|
||||
nameFilters: [qsTr("Stickers (*.png *.webp)")]
|
||||
onAccepted: imagePack.addStickers(files)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delegate: AvatarListTile {
|
||||
id: packItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
property color bubbleText: Nheko.colors.highlightedText
|
||||
required property string shortCode
|
||||
required property string url
|
||||
required property string body
|
||||
|
||||
title: shortCode
|
||||
subtitle: body
|
||||
avatarUrl: url
|
||||
selectedIndex: currentImageIndex
|
||||
crop: false
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: currentImageIndex = index
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AdaptiveLayoutElement {
|
||||
id: packinfoC
|
||||
|
||||
Rectangle {
|
||||
color: Nheko.colors.window
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
visible: currentImageIndex == -1
|
||||
enabled: visible
|
||||
columns: 2
|
||||
rowSpacing: Nheko.paddingLarge
|
||||
|
||||
Avatar {
|
||||
Layout.columnSpan: 2
|
||||
url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.packname
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
visible: imagePack.roomid
|
||||
text: qsTr("State key")
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
visible: imagePack.roomid
|
||||
Layout.fillWidth: true
|
||||
text: imagePack.statekey
|
||||
onTextEdited: imagePack.statekey = text
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Packname")
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
Layout.fillWidth: true
|
||||
text: imagePack.packname
|
||||
onTextEdited: imagePack.packname = text
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Attrbution")
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
Layout.fillWidth: true
|
||||
text: imagePack.attribution
|
||||
onTextEdited: imagePack.attribution = text
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Use as Emoji")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
checked: imagePack.isEmotePack
|
||||
onClicked: imagePack.isEmotePack = checked
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Use as Sticker")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
checked: imagePack.isStickerPack
|
||||
onClicked: imagePack.isStickerPack = checked
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
visible: currentImageIndex >= 0
|
||||
enabled: visible
|
||||
columns: 2
|
||||
rowSpacing: Nheko.paddingLarge
|
||||
|
||||
Avatar {
|
||||
Layout.columnSpan: 2
|
||||
url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Shortcode")
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
Layout.fillWidth: true
|
||||
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
|
||||
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode)
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Body")
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
Layout.fillWidth: true
|
||||
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body)
|
||||
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body)
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Use as Emoji")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote)
|
||||
onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Use as Sticker")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker)
|
||||
onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
id: buttons
|
||||
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
|
||||
onClicked: win.close()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Save")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole
|
||||
onClicked: {
|
||||
imagePack.save();
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -20,14 +20,21 @@ ApplicationWindow {
|
|||
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
|
||||
|
||||
title: qsTr("Image pack settings")
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
height: 400
|
||||
width: 600
|
||||
height: 600
|
||||
width: 800
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(win)
|
||||
|
||||
Component {
|
||||
id: packEditor
|
||||
|
||||
ImagePackEditorDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AdaptiveLayout {
|
||||
id: adaptiveView
|
||||
|
@ -55,7 +62,35 @@ ApplicationWindow {
|
|||
enabled: !Settings.mobileMode
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
footer: ColumnLayout {
|
||||
Button {
|
||||
palette: Nheko.colors
|
||||
onClicked: {
|
||||
var dialog = packEditor.createObject(timelineRoot, {
|
||||
"imagePack": packlist.newPack(false)
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
width: packlist.width
|
||||
visible: !packlist.containsAccountPack
|
||||
text: qsTr("Create account pack")
|
||||
}
|
||||
|
||||
Button {
|
||||
palette: Nheko.colors
|
||||
onClicked: {
|
||||
var dialog = packEditor.createObject(timelineRoot, {
|
||||
"imagePack": packlist.newPack(true)
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
width: packlist.width
|
||||
text: qsTr("New room pack")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delegate: AvatarListTile {
|
||||
id: packItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
|
@ -64,131 +99,24 @@ ApplicationWindow {
|
|||
property color bubbleBackground: Nheko.colors.highlight
|
||||
property color bubbleText: Nheko.colors.highlightedText
|
||||
required property string displayName
|
||||
required property string avatarUrl
|
||||
required property bool fromAccountData
|
||||
required property bool fromCurrentRoom
|
||||
required property int index
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
states: [
|
||||
State {
|
||||
name: "highlight"
|
||||
when: hovered.hovered && !(index == currentPackIndex)
|
||||
|
||||
PropertyChanges {
|
||||
target: packItem
|
||||
background: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
bubbleText: Nheko.colors.highlightedText
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "selected"
|
||||
when: index == currentPackIndex
|
||||
|
||||
PropertyChanges {
|
||||
target: packItem
|
||||
background: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
bubbleText: Nheko.colors.highlight
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
title: displayName
|
||||
subtitle: {
|
||||
if (fromAccountData)
|
||||
return qsTr("Private pack");
|
||||
else if (fromCurrentRoom)
|
||||
return qsTr("Pack from this room");
|
||||
else
|
||||
return qsTr("Globally enabled pack");
|
||||
}
|
||||
selectedIndex: currentPackIndex
|
||||
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
onSingleTapped: currentPackIndex = index
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
|
||||
Avatar {
|
||||
// In the future we could show an online indicator by setting the userid for the avatar
|
||||
//userid: Nheko.currentUser.userid
|
||||
|
||||
id: avatar
|
||||
|
||||
enabled: false
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
height: avatarSize
|
||||
width: avatarSize
|
||||
url: avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: packItem.displayName
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: textContent
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 100
|
||||
width: parent.width - avatar.width
|
||||
Layout.preferredWidth: parent.width - avatar.width
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ElidedLabel {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
color: packItem.importantText
|
||||
elideWidth: textContent.width - Nheko.paddingMedium
|
||||
fullText: displayName
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ElidedLabel {
|
||||
color: packItem.unimportantText
|
||||
font.pixelSize: fontMetrics.font.pixelSize * 0.9
|
||||
elideWidth: textContent.width - Nheko.paddingSmall
|
||||
fullText: {
|
||||
if (fromAccountData)
|
||||
return qsTr("Private pack");
|
||||
else if (fromCurrentRoom)
|
||||
return qsTr("Pack from this room");
|
||||
else
|
||||
return qsTr("Globally enabled pack");
|
||||
}
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -205,6 +133,7 @@ ApplicationWindow {
|
|||
id: packinfo
|
||||
|
||||
property string packName: currentPack ? currentPack.packname : ""
|
||||
property string attribution: currentPack ? currentPack.attribution : ""
|
||||
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
|
||||
|
||||
anchors.fill: parent
|
||||
|
@ -222,8 +151,18 @@ ApplicationWindow {
|
|||
|
||||
MatrixText {
|
||||
text: packinfo.packName
|
||||
font.pixelSize: 24
|
||||
font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1)
|
||||
horizontalAlignment: TextEdit.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: packinfo.attribution
|
||||
wrapMode: TextEdit.Wrap
|
||||
horizontalAlignment: TextEdit.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
|
@ -245,6 +184,18 @@ ApplicationWindow {
|
|||
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Edit")
|
||||
enabled: currentPack.canEdit
|
||||
onClicked: {
|
||||
var dialog = packEditor.createObject(timelineRoot, {
|
||||
"imagePack": currentPack
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
@ -267,7 +218,7 @@ ApplicationWindow {
|
|||
width: stickerDim
|
||||
height: stickerDim
|
||||
hoverEnabled: true
|
||||
ToolTip.text: ":" + model.shortcode + ": - " + model.body
|
||||
ToolTip.text: ":" + model.shortCode + ": - " + model.body
|
||||
ToolTip.visible: hovered
|
||||
|
||||
contentItem: Image {
|
||||
|
|
|
@ -16,6 +16,7 @@ ApplicationWindow {
|
|||
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog
|
||||
Component.onCompleted: Nheko.reparent(inputDialog)
|
||||
width: 350
|
||||
height: fontMetrics.lineSpacing * 7
|
||||
|
||||
|
|
|
@ -112,7 +112,6 @@
|
|||
</qresource>
|
||||
<qresource prefix="/">
|
||||
<file>qtquickcontrols2.conf</file>
|
||||
|
||||
<file>qml/Root.qml</file>
|
||||
<file>qml/ChatPage.qml</file>
|
||||
<file>qml/CommunitiesList.qml</file>
|
||||
|
@ -144,15 +143,21 @@
|
|||
<file>qml/emoji/StickerPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
<file>qml/RoomDirectory.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/TextMessage.qml</file>
|
||||
<file>qml/delegates/NoticeMessage.qml</file>
|
||||
<file>qml/delegates/ImageMessage.qml</file>
|
||||
<file>qml/delegates/PlayableMediaMessage.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/Encrypted.qml</file>
|
||||
<file>qml/delegates/FileMessage.qml</file>
|
||||
<file>qml/delegates/ImageMessage.qml</file>
|
||||
<file>qml/delegates/NoticeMessage.qml</file>
|
||||
<file>qml/delegates/Pill.qml</file>
|
||||
<file>qml/delegates/Placeholder.qml</file>
|
||||
<file>qml/delegates/PlayableMediaMessage.qml</file>
|
||||
<file>qml/delegates/Reply.qml</file>
|
||||
<file>qml/delegates/TextMessage.qml</file>
|
||||
<file>qml/device-verification/Waiting.qml</file>
|
||||
<file>qml/device-verification/DeviceVerification.qml</file>
|
||||
<file>qml/device-verification/DigitVerification.qml</file>
|
||||
|
@ -162,6 +167,7 @@
|
|||
<file>qml/device-verification/Success.qml</file>
|
||||
<file>qml/dialogs/InputDialog.qml</file>
|
||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
|
||||
<file>qml/ui/Ripple.qml</file>
|
||||
<file>qml/ui/Spinner.qml</file>
|
||||
<file>qml/ui/animations/BlinkAnimation.qml</file>
|
||||
|
@ -175,9 +181,12 @@
|
|||
<file>qml/voip/VideoCall.qml</file>
|
||||
<file>qml/components/AdaptiveLayout.qml</file>
|
||||
<file>qml/components/AdaptiveLayoutElement.qml</file>
|
||||
<file>qml/components/AvatarListTile.qml</file>
|
||||
<file>qml/components/FlatButton.qml</file>
|
||||
<file>qml/RoomMembers.qml</file>
|
||||
<file>qml/InviteDialog.qml</file>
|
||||
<file>qml/ReadReceipts.qml</file>
|
||||
<file>qml/RawMessageDialog.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/media">
|
||||
<file>media/ring.ogg</file>
|
||||
|
|
|
@ -125,7 +125,7 @@ template<class T>
|
|||
bool
|
||||
containsStateUpdates(const T &e)
|
||||
{
|
||||
return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e);
|
||||
return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -288,6 +288,9 @@ Cache::setup()
|
|||
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
|
||||
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
|
||||
|
||||
// What rooms are encrypted
|
||||
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
|
||||
|
||||
txn.commit();
|
||||
|
||||
databaseReady_ = true;
|
||||
|
@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
|
|||
{
|
||||
nhlog::db()->info("mark room {} as encrypted", room_id);
|
||||
|
||||
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
|
||||
db.put(txn, room_id, "0");
|
||||
encryptedRooms_.put(txn, room_id, "0");
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id)
|
|||
std::string_view unused;
|
||||
|
||||
auto txn = ro_txn(env_);
|
||||
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
|
||||
auto res = db.get(txn, room_id, unused);
|
||||
auto res = encryptedRooms_.get(txn, room_id, unused);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
@ -3400,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
|
|||
info.pack.pack = pack.pack;
|
||||
|
||||
for (const auto &img : pack.images) {
|
||||
if (img.second.overrides_usage() &&
|
||||
if (stickers.has_value() && img.second.overrides_usage() &&
|
||||
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
|
||||
continue;
|
||||
|
||||
|
@ -3541,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id)
|
|||
}
|
||||
|
||||
std::map<std::string, std::optional<UserKeyCache>>
|
||||
Cache::getMembersWithKeys(const std::string &room_id)
|
||||
Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
|
||||
{
|
||||
std::string_view keys;
|
||||
|
||||
|
@ -3558,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id)
|
|||
auto res = keysDb.get(txn, user_id, keys);
|
||||
|
||||
if (res) {
|
||||
members[std::string(user_id)] =
|
||||
json::parse(keys).get<UserKeyCache>();
|
||||
auto k = json::parse(keys).get<UserKeyCache>();
|
||||
if (verified_only) {
|
||||
auto verif = verificationStatus(std::string(user_id));
|
||||
if (verif.user_verified == crypto::Trust::Verified ||
|
||||
!verif.verified_devices.empty()) {
|
||||
auto keyCopy = k;
|
||||
keyCopy.device_keys.clear();
|
||||
|
||||
std::copy_if(
|
||||
k.device_keys.begin(),
|
||||
k.device_keys.end(),
|
||||
std::inserter(keyCopy.device_keys,
|
||||
keyCopy.device_keys.end()),
|
||||
[&verif](const auto &key) {
|
||||
auto curve25519 = key.second.keys.find(
|
||||
"curve25519:" + key.first);
|
||||
if (curve25519 == key.second.keys.end())
|
||||
return false;
|
||||
if (auto t =
|
||||
verif.verified_device_keys.find(
|
||||
curve25519->second);
|
||||
t ==
|
||||
verif.verified_device_keys.end() ||
|
||||
t->second != crypto::Trust::Verified)
|
||||
return false;
|
||||
|
||||
return key.first ==
|
||||
key.second.device_id &&
|
||||
std::find(
|
||||
verif.verified_devices.begin(),
|
||||
verif.verified_devices.end(),
|
||||
key.first) !=
|
||||
verif.verified_devices.end();
|
||||
});
|
||||
|
||||
if (!keyCopy.device_keys.empty())
|
||||
members[std::string(user_id)] =
|
||||
std::move(keyCopy);
|
||||
}
|
||||
} else {
|
||||
members[std::string(user_id)] = std::move(k);
|
||||
}
|
||||
} else {
|
||||
members[std::string(user_id)] = {};
|
||||
if (!verified_only)
|
||||
members[std::string(user_id)] = {};
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
|
|
|
@ -48,7 +48,8 @@ public:
|
|||
// user cache stores user keys
|
||||
std::optional<UserKeyCache> userKeys(const std::string &user_id);
|
||||
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
|
||||
const std::string &room_id);
|
||||
const std::string &room_id,
|
||||
bool verified_only);
|
||||
void updateUserKeys(const std::string &sync_token,
|
||||
const mtx::responses::QueryKeys &keyQuery);
|
||||
void markUserKeysOutOfDate(lmdb::txn &txn,
|
||||
|
@ -290,15 +291,9 @@ public:
|
|||
std::optional<std::string> secret(const std::string name);
|
||||
|
||||
template<class T>
|
||||
static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
template<class T>
|
||||
static constexpr bool isStateEvent(const mtx::events::Event<T> &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
constexpr static bool isStateEvent_ =
|
||||
std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
|
||||
mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
|
||||
|
||||
static int compare_state_key(const MDB_val *a, const MDB_val *b)
|
||||
{
|
||||
|
@ -415,11 +410,27 @@ private:
|
|||
}
|
||||
|
||||
std::visit(
|
||||
[&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
|
||||
if constexpr (isStateEvent(e)) {
|
||||
[&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
|
||||
if constexpr (isStateEvent_<decltype(e)>) {
|
||||
eventsDb.put(txn, e.event_id, json(e).dump());
|
||||
|
||||
if (e.type != EventType::Unsupported) {
|
||||
if (std::is_same_v<
|
||||
std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
|
||||
StateEvent<mtx::events::msg::Redacted>>) {
|
||||
if (e.type == EventType::RoomMember)
|
||||
membersdb.del(txn, e.state_key, "");
|
||||
else if (e.state_key.empty())
|
||||
statesdb.del(txn, to_string(e.type));
|
||||
else
|
||||
stateskeydb.del(
|
||||
txn,
|
||||
to_string(e.type),
|
||||
json::object({
|
||||
{"key", e.state_key},
|
||||
{"id", e.event_id},
|
||||
})
|
||||
.dump());
|
||||
} else if (e.type != EventType::Unsupported) {
|
||||
if (e.state_key.empty())
|
||||
statesdb.put(
|
||||
txn, to_string(e.type), json(e).dump());
|
||||
|
@ -689,6 +700,8 @@ private:
|
|||
lmdb::dbi outboundMegolmSessionDb_;
|
||||
lmdb::dbi megolmSessionDataDb_;
|
||||
|
||||
lmdb::dbi encryptedRooms_;
|
||||
|
||||
QString localUserId_;
|
||||
QString cacheDirectory_;
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
|
||||
#include "notifications/Manager.h"
|
||||
|
||||
#include "dialogs/ReadReceipts.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
|
||||
#include "blurhash.hpp"
|
||||
|
|
|
@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row)
|
|||
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
|
||||
return e;
|
||||
}
|
||||
|
||||
SingleImagePackModel *
|
||||
ImagePackListModel::newPack(bool inRoom)
|
||||
{
|
||||
ImagePackInfo info{};
|
||||
if (inRoom)
|
||||
info.source_room = room_id;
|
||||
return new SingleImagePackModel(info);
|
||||
}
|
||||
|
||||
bool
|
||||
ImagePackListModel::containsAccountPack() const
|
||||
{
|
||||
for (const auto &p : packs)
|
||||
if (p->roomid().isEmpty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ class SingleImagePackModel;
|
|||
class ImagePackListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
|
||||
public:
|
||||
enum Roles
|
||||
{
|
||||
|
@ -29,6 +30,9 @@ public:
|
|||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
Q_INVOKABLE SingleImagePackModel *packAt(int row);
|
||||
Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
|
||||
|
||||
bool containsAccountPack() const;
|
||||
|
||||
private:
|
||||
std::string room_id;
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
#include "dialogs/JoinRoom.h"
|
||||
#include "dialogs/LeaveRoom.h"
|
||||
#include "dialogs/Logout.h"
|
||||
#include "dialogs/ReadReceipts.h"
|
||||
|
||||
MainWindow *MainWindow::instance_ = nullptr;
|
||||
|
||||
|
@ -398,27 +397,6 @@ MainWindow::openLogoutDialog()
|
|||
showDialog(dialog);
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::openReadReceiptsDialog(const QString &event_id)
|
||||
{
|
||||
auto dialog = new dialogs::ReadReceipts(this);
|
||||
|
||||
const auto room_id = chat_page_->currentRoom();
|
||||
|
||||
try {
|
||||
dialog->addUsers(cache::readReceipts(event_id, room_id));
|
||||
} catch (const lmdb::error &) {
|
||||
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
|
||||
event_id.toStdString(),
|
||||
chat_page_->currentRoom().toStdString());
|
||||
dialog->deleteLater();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(dialog);
|
||||
}
|
||||
|
||||
bool
|
||||
MainWindow::hasActiveDialogs() const
|
||||
{
|
||||
|
|
|
@ -65,7 +65,6 @@ public:
|
|||
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
||||
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
||||
void openLogoutDialog();
|
||||
void openReadReceiptsDialog(const QString &event_id);
|
||||
|
||||
void hideOverlay();
|
||||
void showSolidOverlayModal(QWidget *content,
|
||||
|
|
|
@ -2,16 +2,6 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QAbstractSlider>
|
||||
#include <QLabel>
|
||||
#include <QListWidgetItem>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QShortcut>
|
||||
#include <QStyleOption>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "MemberList.h"
|
||||
|
||||
#include "Cache.h"
|
||||
|
@ -20,7 +10,6 @@
|
|||
#include "Logging.h"
|
||||
#include "Utils.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
#include "ui/Avatar.h"
|
||||
|
||||
MemberList::MemberList(const QString &room_id, QObject *parent)
|
||||
: QAbstractListModel{parent}
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "CacheStructs.h"
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "CacheStructs.h"
|
||||
|
||||
class MemberList : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
|
@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
|
|||
QQuickImageResponse *
|
||||
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
|
||||
auto id_ = id;
|
||||
bool crop = true;
|
||||
if (id.endsWith("?scale")) {
|
||||
crop = false;
|
||||
id_.remove("?scale");
|
||||
}
|
||||
|
||||
MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
|
||||
pool.start(response);
|
||||
return response;
|
||||
}
|
||||
|
@ -36,20 +43,24 @@ void
|
|||
MxcImageResponse::run()
|
||||
{
|
||||
MxcImageProvider::download(
|
||||
m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
|
||||
m_id,
|
||||
m_requestedSize,
|
||||
[this](QString, QSize, QImage image, QString) {
|
||||
if (image.isNull()) {
|
||||
m_error = "Failed to download image.";
|
||||
} else {
|
||||
m_image = image;
|
||||
}
|
||||
emit finished();
|
||||
});
|
||||
},
|
||||
m_crop);
|
||||
}
|
||||
|
||||
void
|
||||
MxcImageProvider::download(const QString &id,
|
||||
const QSize &requestedSize,
|
||||
std::function<void(QString, QSize, QImage, QString)> then)
|
||||
std::function<void(QString, QSize, QImage, QString)> then,
|
||||
bool crop)
|
||||
{
|
||||
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
|
||||
auto temp = infos.find("mxc://" + id);
|
||||
|
@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
|
|||
|
||||
if (requestedSize.isValid() && !encryptionInfo) {
|
||||
QString fileName =
|
||||
QString("%1_%2x%3_crop")
|
||||
QString("%1_%2x%3_%4")
|
||||
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
|
||||
QByteArray::OmitTrailingEquals)))
|
||||
.arg(requestedSize.width())
|
||||
.arg(requestedSize.height());
|
||||
.arg(requestedSize.height())
|
||||
.arg(crop ? "crop" : "scale");
|
||||
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||
"/media_cache",
|
||||
fileName);
|
||||
|
@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
|
|||
opts.mxc_url = "mxc://" + id.toStdString();
|
||||
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
|
||||
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
|
||||
opts.method = "crop";
|
||||
opts.method = crop ? "crop" : "scale";
|
||||
http::client()->get_thumbnail(
|
||||
opts,
|
||||
[fileInfo, requestedSize, then, id](const std::string &res,
|
||||
|
|
|
@ -19,9 +19,10 @@ class MxcImageResponse
|
|||
, public QRunnable
|
||||
{
|
||||
public:
|
||||
MxcImageResponse(const QString &id, const QSize &requestedSize)
|
||||
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
|
||||
: m_id(id)
|
||||
, m_requestedSize(requestedSize)
|
||||
, m_crop(crop)
|
||||
{
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
@ -37,6 +38,7 @@ public:
|
|||
QString m_id, m_error;
|
||||
QSize m_requestedSize;
|
||||
QImage m_image;
|
||||
bool m_crop;
|
||||
};
|
||||
|
||||
class MxcImageProvider
|
||||
|
@ -51,7 +53,8 @@ public slots:
|
|||
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
|
||||
static void download(const QString &id,
|
||||
const QSize &requestedSize,
|
||||
std::function<void(QString, QSize, QImage, QString)> then);
|
||||
std::function<void(QString, QSize, QImage, QString)> then,
|
||||
bool crop = true);
|
||||
|
||||
private:
|
||||
QThreadPool pool;
|
||||
|
|
41
src/Olm.cpp
41
src/Olm.cpp
|
@ -286,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
|
|||
|
||||
bool from_their_device = false;
|
||||
for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
|
||||
if (key.keys.at("curve25519:" + device_id) == msg.sender_key) {
|
||||
if (key.keys.at("ed25519:" + device_id) == sender_ed25519) {
|
||||
from_their_device = true;
|
||||
break;
|
||||
}
|
||||
auto c_key = key.keys.find("curve25519:" + device_id);
|
||||
auto e_key = key.keys.find("ed25519:" + device_id);
|
||||
|
||||
if (c_key == key.keys.end() || e_key == key.keys.end()) {
|
||||
nhlog::crypto()->warn(
|
||||
"Skipping device {} as we have no keys for it.",
|
||||
device_id);
|
||||
} else if (c_key->second == msg.sender_key &&
|
||||
e_key->second == sender_ed25519) {
|
||||
from_their_device = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!from_their_device) {
|
||||
|
@ -518,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
|||
|
||||
auto own_user_id = http::client()->user_id().to_string();
|
||||
|
||||
auto members = cache::client()->getMembersWithKeys(room_id);
|
||||
auto members = cache::client()->getMembersWithKeys(
|
||||
room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
|
||||
|
||||
std::map<std::string, std::vector<std::string>> sendSessionTo;
|
||||
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
|
||||
|
@ -1062,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index,
|
|||
mtx::events::collections::TimelineEvent te;
|
||||
mtx::events::collections::from_json(body, te);
|
||||
|
||||
return {std::nullopt, std::nullopt, std::move(te.data)};
|
||||
return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
|
||||
} catch (std::exception &e) {
|
||||
return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
|
||||
}
|
||||
|
@ -1138,9 +1145,23 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
|
|||
|
||||
auto session = cache::getLatestOlmSession(device_curve);
|
||||
if (!session || force_new_session) {
|
||||
claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519;
|
||||
pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
|
||||
pks[user][device].curve25519 = d.keys.at("curve25519:" + device);
|
||||
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
|
||||
auto currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
|
||||
currentTime) {
|
||||
claims.one_time_keys[user][device] =
|
||||
mtx::crypto::SIGNED_CURVE25519;
|
||||
pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
|
||||
pks[user][device].curve25519 =
|
||||
d.keys.at("curve25519:" + device);
|
||||
|
||||
rateLimit.insert(QPair(user, device), currentTime);
|
||||
} else {
|
||||
nhlog::crypto()->warn("Not creating new session with {}:{} "
|
||||
"because of rate limit",
|
||||
user,
|
||||
device);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
namespace olm {
|
||||
Q_NAMESPACE
|
||||
|
||||
enum class DecryptionErrorCode
|
||||
enum DecryptionErrorCode
|
||||
{
|
||||
NoError,
|
||||
MissingSession, // Session was not found, retrieve from backup or request from other devices
|
||||
// and try again
|
||||
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
|
||||
|
@ -25,14 +27,13 @@ enum class DecryptionErrorCode
|
|||
DecryptionFailed, // libolm error
|
||||
ParsingFailed, // Failed to parse the actual event
|
||||
ReplayAttack, // Megolm index reused
|
||||
UnknownFingerprint, // Unknown device Fingerprint
|
||||
};
|
||||
Q_ENUM_NS(DecryptionErrorCode)
|
||||
|
||||
struct DecryptionResult
|
||||
{
|
||||
std::optional<DecryptionErrorCode> error;
|
||||
DecryptionErrorCode error;
|
||||
std::optional<std::string> error_message;
|
||||
|
||||
std::optional<mtx::events::collections::TimelineEvents> event;
|
||||
};
|
||||
|
||||
|
|
131
src/ReadReceiptsModel.cpp
Normal file
131
src/ReadReceiptsModel.cpp
Normal file
|
@ -0,0 +1,131 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ReadReceiptsModel.h"
|
||||
|
||||
#include <QLocale>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "Logging.h"
|
||||
#include "Utils.h"
|
||||
|
||||
ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent)
|
||||
: QAbstractListModel{parent}
|
||||
, event_id_{event_id}
|
||||
, room_id_{room_id}
|
||||
{
|
||||
try {
|
||||
addUsers(cache::readReceipts(event_id_, room_id_));
|
||||
} catch (const lmdb::error &) {
|
||||
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
|
||||
event_id_.toStdString(),
|
||||
room_id_.toStdString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update);
|
||||
}
|
||||
|
||||
void
|
||||
ReadReceiptsModel::update()
|
||||
{
|
||||
try {
|
||||
addUsers(cache::readReceipts(event_id_, room_id_));
|
||||
} catch (const lmdb::error &) {
|
||||
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
|
||||
event_id_.toStdString(),
|
||||
room_id_.toStdString());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
ReadReceiptsModel::roleNames() const
|
||||
{
|
||||
// Note: RawTimestamp is purposely not included here
|
||||
return {
|
||||
{Mxid, "mxid"},
|
||||
{DisplayName, "displayName"},
|
||||
{AvatarUrl, "avatarUrl"},
|
||||
{Timestamp, "timestamp"},
|
||||
};
|
||||
}
|
||||
|
||||
QVariant
|
||||
ReadReceiptsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0)
|
||||
return {};
|
||||
|
||||
switch (role) {
|
||||
case Mxid:
|
||||
return readReceipts_[index.row()].first;
|
||||
case DisplayName:
|
||||
return cache::displayName(room_id_, readReceipts_[index.row()].first);
|
||||
case AvatarUrl:
|
||||
return cache::avatarUrl(room_id_, readReceipts_[index.row()].first);
|
||||
case Timestamp:
|
||||
return dateFormat(readReceipts_[index.row()].second);
|
||||
case RawTimestamp:
|
||||
return readReceipts_[index.row()].second;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ReadReceiptsModel::addUsers(
|
||||
const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users)
|
||||
{
|
||||
auto newReceipts = users.size() - readReceipts_.size();
|
||||
|
||||
if (newReceipts > 0) {
|
||||
beginInsertRows(
|
||||
QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1);
|
||||
|
||||
for (const auto &user : users) {
|
||||
QPair<QString, QDateTime> item = {
|
||||
QString::fromStdString(user.second),
|
||||
QDateTime::fromMSecsSinceEpoch(user.first)};
|
||||
if (!readReceipts_.contains(item))
|
||||
readReceipts_.push_back(item);
|
||||
}
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
ReadReceiptsModel::dateFormat(const QDateTime &then) const
|
||||
{
|
||||
auto now = QDateTime::currentDateTime();
|
||||
auto days = then.daysTo(now);
|
||||
|
||||
if (days == 0)
|
||||
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
|
||||
else if (days < 2)
|
||||
return tr("Yesterday, %1")
|
||||
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
|
||||
else if (days < 7)
|
||||
//: %1 is the name of the current day, %2 is the time the read receipt was read. The
|
||||
//: result may look like this: Monday, 7:15
|
||||
return QString("%1, %2")
|
||||
.arg(then.toString("dddd"))
|
||||
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
|
||||
|
||||
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent)
|
||||
: QSortFilterProxyModel{parent}
|
||||
, model_{event_id, room_id, this}
|
||||
{
|
||||
setSourceModel(&model_);
|
||||
setSortRole(ReadReceiptsModel::RawTimestamp);
|
||||
sort(0, Qt::DescendingOrder);
|
||||
setDynamicSortFilter(true);
|
||||
}
|
73
src/ReadReceiptsModel.h
Normal file
73
src/ReadReceiptsModel.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef READRECEIPTSMODEL_H
|
||||
#define READRECEIPTSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
|
||||
class ReadReceiptsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles
|
||||
{
|
||||
Mxid,
|
||||
DisplayName,
|
||||
AvatarUrl,
|
||||
Timestamp,
|
||||
RawTimestamp,
|
||||
};
|
||||
|
||||
explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr);
|
||||
|
||||
QString eventId() const { return event_id_; }
|
||||
QString roomId() const { return room_id_; }
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return readReceipts_.size();
|
||||
}
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
public slots:
|
||||
void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
|
||||
void update();
|
||||
|
||||
private:
|
||||
QString dateFormat(const QDateTime &then) const;
|
||||
|
||||
QString event_id_;
|
||||
QString room_id_;
|
||||
QVector<QPair<QString, QDateTime>> readReceipts_;
|
||||
};
|
||||
|
||||
class ReadReceiptsProxy : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString eventId READ eventId CONSTANT)
|
||||
Q_PROPERTY(QString roomId READ roomId CONSTANT)
|
||||
|
||||
public:
|
||||
explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr);
|
||||
|
||||
QString eventId() const { return event_id_; }
|
||||
QString roomId() const { return room_id_; }
|
||||
|
||||
private:
|
||||
QString event_id_;
|
||||
QString room_id_;
|
||||
|
||||
ReadReceiptsModel model_;
|
||||
};
|
||||
|
||||
#endif // READRECEIPTSMODEL_H
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
#include <mtx/responses/register.hpp>
|
||||
#include <mtx/responses/well-known.hpp>
|
||||
#include <mtxclient/http/client.hpp>
|
||||
|
||||
#include "Config.h"
|
||||
#include "Logging.h"
|
||||
|
@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent)
|
|||
|
||||
server_input_ = new TextField();
|
||||
server_input_->setLabel(tr("Homeserver"));
|
||||
server_input_->setRegexp(QRegularExpression(".+"));
|
||||
server_input_->setToolTip(
|
||||
tr("A server that allows registration. Since matrix is decentralized, you need to first "
|
||||
"find a server you can register on or host your own."));
|
||||
|
@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent)
|
|||
top_layout_->addLayout(button_layout_);
|
||||
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
|
||||
top_layout_->addStretch(1);
|
||||
|
||||
connect(
|
||||
this,
|
||||
&RegisterPage::versionErrorCb,
|
||||
this,
|
||||
[this](const QString &msg) {
|
||||
error_server_label_->show();
|
||||
server_input_->setValid(false);
|
||||
showError(error_server_label_, msg);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
setLayout(top_layout_);
|
||||
|
||||
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
||||
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
|
||||
|
||||
connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||
connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
||||
connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
|
||||
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||
connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
||||
connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
|
||||
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||
connect(
|
||||
password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
||||
connect(password_confirmation_,
|
||||
&TextField::editingFinished,
|
||||
this,
|
||||
&RegisterPage::checkPasswordConfirmation);
|
||||
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
||||
connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) {
|
||||
showError(msg);
|
||||
});
|
||||
connect(
|
||||
this,
|
||||
&RegisterPage::registrationFlow,
|
||||
this,
|
||||
[this](const std::string &user,
|
||||
const std::string &pass,
|
||||
const mtx::user_interactive::Unauthorized &unauthorized) {
|
||||
auto completed_stages = unauthorized.completed;
|
||||
auto flows = unauthorized.flows;
|
||||
auto session = unauthorized.session.empty() ? http::client()->generate_txn_id()
|
||||
: unauthorized.session;
|
||||
|
||||
nhlog::ui()->info("Completed stages: {}", completed_stages.size());
|
||||
|
||||
if (!completed_stages.empty())
|
||||
flows.erase(std::remove_if(
|
||||
flows.begin(),
|
||||
flows.end(),
|
||||
[completed_stages](auto flow) {
|
||||
if (completed_stages.size() > flow.stages.size())
|
||||
return true;
|
||||
for (size_t f = 0; f < completed_stages.size(); f++)
|
||||
if (completed_stages[f] != flow.stages[f])
|
||||
return true;
|
||||
return false;
|
||||
}),
|
||||
flows.end());
|
||||
|
||||
if (flows.empty()) {
|
||||
nhlog::net()->error("No available registration flows!");
|
||||
emit registerErrorCb(tr("No supported registration flows!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto current_stage = flows.front().stages.at(completed_stages.size());
|
||||
|
||||
if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
|
||||
auto captchaDialog =
|
||||
new dialogs::ReCaptcha(QString::fromStdString(session), this);
|
||||
|
||||
connect(captchaDialog,
|
||||
&dialogs::ReCaptcha::confirmation,
|
||||
this,
|
||||
[this, user, pass, session, captchaDialog]() {
|
||||
captchaDialog->close();
|
||||
captchaDialog->deleteLater();
|
||||
|
||||
emit registerAuth(
|
||||
user,
|
||||
pass,
|
||||
mtx::user_interactive::Auth{
|
||||
session, mtx::user_interactive::auth::Fallback{}});
|
||||
});
|
||||
connect(captchaDialog,
|
||||
&dialogs::ReCaptcha::cancel,
|
||||
this,
|
||||
&RegisterPage::errorOccurred);
|
||||
|
||||
QTimer::singleShot(
|
||||
1000, this, [captchaDialog]() { captchaDialog->show(); });
|
||||
} else if (current_stage == mtx::user_interactive::auth_types::dummy) {
|
||||
emit registerAuth(user,
|
||||
pass,
|
||||
mtx::user_interactive::Auth{
|
||||
session, mtx::user_interactive::auth::Dummy{}});
|
||||
} else {
|
||||
// use fallback
|
||||
auto dialog =
|
||||
new dialogs::FallbackAuth(QString::fromStdString(current_stage),
|
||||
QString::fromStdString(session),
|
||||
this);
|
||||
|
||||
connect(dialog,
|
||||
&dialogs::FallbackAuth::confirmation,
|
||||
this,
|
||||
[this, user, pass, session, dialog]() {
|
||||
dialog->close();
|
||||
dialog->deleteLater();
|
||||
|
||||
emit registerAuth(
|
||||
user,
|
||||
pass,
|
||||
mtx::user_interactive::Auth{
|
||||
session, mtx::user_interactive::auth::Fallback{}});
|
||||
});
|
||||
connect(dialog,
|
||||
&dialogs::FallbackAuth::cancel,
|
||||
this,
|
||||
&RegisterPage::errorOccurred);
|
||||
|
||||
dialog->show();
|
||||
}
|
||||
});
|
||||
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
|
||||
|
||||
connect(
|
||||
this,
|
||||
&RegisterPage::registerAuth,
|
||||
&RegisterPage::serverError,
|
||||
this,
|
||||
[this](const std::string &user,
|
||||
const std::string &pass,
|
||||
const mtx::user_interactive::Auth &auth) {
|
||||
http::client()->registration(
|
||||
user,
|
||||
pass,
|
||||
auth,
|
||||
[this, user, pass](const mtx::responses::Register &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (!err) {
|
||||
http::client()->set_user(res.user_id);
|
||||
http::client()->set_access_token(res.access_token);
|
||||
http::client()->set_device_id(res.device_id);
|
||||
[this](const QString &msg) {
|
||||
server_input_->setValid(false);
|
||||
showError(error_server_label_, msg);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
emit registerOk();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server requires registration flows.
|
||||
if (err->status_code == 401) {
|
||||
if (err->matrix_error.unauthorized.flows.empty()) {
|
||||
nhlog::net()->warn(
|
||||
"failed to retrieve registration flows: ({}) "
|
||||
"{}",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error);
|
||||
emit registerErrorCb(
|
||||
QString::fromStdString(err->matrix_error.error));
|
||||
return;
|
||||
}
|
||||
|
||||
emit registrationFlow(
|
||||
user, pass, err->matrix_error.unauthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::net()->warn("failed to register: status_code ({}), "
|
||||
"matrix_error: ({}), parser error ({})",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error,
|
||||
err->parse_error);
|
||||
|
||||
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
|
||||
});
|
||||
});
|
||||
|
||||
setLayout(top_layout_);
|
||||
connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
|
||||
connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
|
||||
connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
|
||||
connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA);
|
||||
connect(
|
||||
this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth);
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -345,191 +208,298 @@ RegisterPage::showError(QLabel *label, const QString &msg)
|
|||
int height = rect.height();
|
||||
label->setFixedHeight((int)qCeil(width / 200.0) * height);
|
||||
label->setText(msg);
|
||||
label->show();
|
||||
}
|
||||
|
||||
bool
|
||||
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
|
||||
{
|
||||
if (t_field->isValid()) {
|
||||
label->setText("");
|
||||
label->hide();
|
||||
return true;
|
||||
} else {
|
||||
label->show();
|
||||
showError(label, msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RegisterPage::checkFields()
|
||||
RegisterPage::checkUsername()
|
||||
{
|
||||
error_label_->setText("");
|
||||
error_username_label_->setText("");
|
||||
error_password_label_->setText("");
|
||||
error_password_confirmation_label_->setText("");
|
||||
error_server_label_->setText("");
|
||||
return checkOneField(error_username_label_,
|
||||
username_input_,
|
||||
tr("The username must not be empty, and must contain only the "
|
||||
"characters a-z, 0-9, ., _, =, -, and /."));
|
||||
}
|
||||
|
||||
error_username_label_->hide();
|
||||
error_password_label_->hide();
|
||||
error_password_confirmation_label_->hide();
|
||||
error_server_label_->hide();
|
||||
bool
|
||||
RegisterPage::checkPassword()
|
||||
{
|
||||
return checkOneField(
|
||||
error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
|
||||
}
|
||||
|
||||
password_confirmation_->setValid(true);
|
||||
server_input_->setValid(true);
|
||||
|
||||
bool all_fields_good = true;
|
||||
if (username_input_->isModified() &&
|
||||
!checkOneField(error_username_label_,
|
||||
username_input_,
|
||||
tr("The username must not be empty, and must contain only the "
|
||||
"characters a-z, 0-9, ., _, =, -, and /."))) {
|
||||
all_fields_good = false;
|
||||
} else if (password_input_->isModified() &&
|
||||
!checkOneField(error_password_label_,
|
||||
password_input_,
|
||||
tr("Password is not long enough (min 8 chars)"))) {
|
||||
all_fields_good = false;
|
||||
} else if (password_confirmation_->isModified() &&
|
||||
password_input_->text() != password_confirmation_->text()) {
|
||||
error_password_confirmation_label_->show();
|
||||
bool
|
||||
RegisterPage::checkPasswordConfirmation()
|
||||
{
|
||||
if (password_input_->text() == password_confirmation_->text()) {
|
||||
error_password_confirmation_label_->hide();
|
||||
password_confirmation_->setValid(true);
|
||||
return true;
|
||||
} else {
|
||||
showError(error_password_confirmation_label_, tr("Passwords don't match"));
|
||||
password_confirmation_->setValid(false);
|
||||
all_fields_good = false;
|
||||
} else if (server_input_->isModified() &&
|
||||
(!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) {
|
||||
error_server_label_->show();
|
||||
showError(error_server_label_, tr("Invalid server name"));
|
||||
server_input_->setValid(false);
|
||||
all_fields_good = false;
|
||||
return false;
|
||||
}
|
||||
if (!username_input_->isModified() || !password_input_->isModified() ||
|
||||
!password_confirmation_->isModified() || !server_input_->isModified()) {
|
||||
all_fields_good = false;
|
||||
}
|
||||
return all_fields_good;
|
||||
}
|
||||
|
||||
bool
|
||||
RegisterPage::checkServer()
|
||||
{
|
||||
// This doesn't check that the server is reachable,
|
||||
// just that the input is not obviously wrong.
|
||||
return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::onRegisterButtonClicked()
|
||||
{
|
||||
if (!checkFields()) {
|
||||
showError(error_label_,
|
||||
tr("One or more fields have invalid inputs. Please correct those issues "
|
||||
"and try again."));
|
||||
return;
|
||||
} else {
|
||||
auto username = username_input_->text().toStdString();
|
||||
auto password = password_input_->text().toStdString();
|
||||
auto server = server_input_->text().toStdString();
|
||||
if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
|
||||
auto server = server_input_->text().toStdString();
|
||||
|
||||
http::client()->set_server(server);
|
||||
http::client()->verify_certificates(
|
||||
!UserSettings::instance()->disableCertificateValidation());
|
||||
|
||||
http::client()->well_known(
|
||||
[this, username, password](const mtx::responses::WellKnown &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
if (err->status_code == 404) {
|
||||
nhlog::net()->info("Autodiscovery: No .well-known.");
|
||||
checkVersionAndRegister(username, password);
|
||||
return;
|
||||
}
|
||||
// This starts a chain of `emit`s which ends up doing the
|
||||
// registration. Signals are used rather than normal function
|
||||
// calls so that the dialogs used in UIA work correctly.
|
||||
//
|
||||
// The sequence of events looks something like this:
|
||||
//
|
||||
// dowellKnownLookup
|
||||
// v
|
||||
// doVersionsCheck
|
||||
// v
|
||||
// doRegistration
|
||||
// v
|
||||
// doUIA <-----------------+
|
||||
// v | More auth required
|
||||
// doRegistrationWithAuth -+
|
||||
// | Success
|
||||
// v
|
||||
// registering
|
||||
|
||||
if (!err->parse_error.empty()) {
|
||||
emit versionErrorCb(tr(
|
||||
"Autodiscovery failed. Received malformed response."));
|
||||
nhlog::net()->error(
|
||||
"Autodiscovery failed. Received malformed response.");
|
||||
return;
|
||||
}
|
||||
|
||||
emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
|
||||
"requesting .well-known."));
|
||||
nhlog::net()->error("Autodiscovery failed. Unknown error when "
|
||||
"requesting .well-known. {} {}",
|
||||
err->status_code,
|
||||
err->error_code);
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::net()->info("Autodiscovery: Discovered '" +
|
||||
res.homeserver.base_url + "'");
|
||||
http::client()->set_server(res.homeserver.base_url);
|
||||
checkVersionAndRegister(username, password);
|
||||
});
|
||||
emit wellKnownLookup();
|
||||
|
||||
emit registering();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password)
|
||||
RegisterPage::doWellKnownLookup()
|
||||
{
|
||||
http::client()->versions(
|
||||
[this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) {
|
||||
http::client()->well_known(
|
||||
[this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
if (err->status_code == 404) {
|
||||
emit versionErrorCb(tr("The required endpoints were not found. "
|
||||
"Possibly not a Matrix server."));
|
||||
nhlog::net()->info("Autodiscovery: No .well-known.");
|
||||
// Check that the homeserver can be reached
|
||||
emit versionsCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!err->parse_error.empty()) {
|
||||
emit versionErrorCb(tr("Received malformed response. Make sure "
|
||||
"the homeserver domain is valid."));
|
||||
emit serverError(
|
||||
tr("Autodiscovery failed. Received malformed response."));
|
||||
nhlog::net()->error(
|
||||
"Autodiscovery failed. Received malformed response.");
|
||||
return;
|
||||
}
|
||||
|
||||
emit versionErrorCb(tr(
|
||||
"An unknown error occured. Make sure the homeserver domain is valid."));
|
||||
emit serverError(tr("Autodiscovery failed. Unknown error when "
|
||||
"requesting .well-known."));
|
||||
nhlog::net()->error("Autodiscovery failed. Unknown error when "
|
||||
"requesting .well-known. {} {}",
|
||||
err->status_code,
|
||||
err->error_code);
|
||||
return;
|
||||
}
|
||||
|
||||
http::client()->registration(
|
||||
username,
|
||||
password,
|
||||
[this, username, password](const mtx::responses::Register &res,
|
||||
mtx::http::RequestErr err) {
|
||||
if (!err) {
|
||||
http::client()->set_user(res.user_id);
|
||||
http::client()->set_access_token(res.access_token);
|
||||
|
||||
emit registerOk();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server requires registration flows.
|
||||
if (err->status_code == 401) {
|
||||
if (err->matrix_error.unauthorized.flows.empty()) {
|
||||
nhlog::net()->warn(
|
||||
"failed to retrieve registration flows1: ({}) "
|
||||
"{}",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error);
|
||||
emit errorOccurred();
|
||||
emit registerErrorCb(
|
||||
QString::fromStdString(err->matrix_error.error));
|
||||
return;
|
||||
}
|
||||
|
||||
emit registrationFlow(
|
||||
username, password, err->matrix_error.unauthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::net()->error(
|
||||
"failed to register: status_code ({}), matrix_error({})",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error);
|
||||
|
||||
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
|
||||
emit errorOccurred();
|
||||
});
|
||||
nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
|
||||
http::client()->set_server(res.homeserver.base_url);
|
||||
// Check that the homeserver can be reached
|
||||
emit versionsCheck();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::doVersionsCheck()
|
||||
{
|
||||
// Make a request to /_matrix/client/versions to check the address
|
||||
// given is a Matrix homeserver.
|
||||
http::client()->versions(
|
||||
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
if (err->status_code == 404) {
|
||||
emit serverError(
|
||||
tr("The required endpoints were not found. Possibly "
|
||||
"not a Matrix server."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!err->parse_error.empty()) {
|
||||
emit serverError(
|
||||
tr("Received malformed response. Make sure the homeserver "
|
||||
"domain is valid."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit serverError(tr("An unknown error occured. Make sure the "
|
||||
"homeserver domain is valid."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt registration without an `auth` dict
|
||||
emit registration();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::doRegistration()
|
||||
{
|
||||
// These inputs should still be alright, but check just in case
|
||||
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
|
||||
auto username = username_input_->text().toStdString();
|
||||
auto password = password_input_->text().toStdString();
|
||||
http::client()->registration(username, password, registrationCb());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
|
||||
{
|
||||
// These inputs should still be alright, but check just in case
|
||||
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
|
||||
auto username = username_input_->text().toStdString();
|
||||
auto password = password_input_->text().toStdString();
|
||||
http::client()->registration(username, password, auth, registrationCb());
|
||||
}
|
||||
}
|
||||
|
||||
mtx::http::Callback<mtx::responses::Register>
|
||||
RegisterPage::registrationCb()
|
||||
{
|
||||
// Return a function to be used as the callback when an attempt at
|
||||
// registration is made.
|
||||
return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
|
||||
if (!err) {
|
||||
http::client()->set_user(res.user_id);
|
||||
http::client()->set_access_token(res.access_token);
|
||||
emit registerOk();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server requires registration flows.
|
||||
if (err->status_code == 401) {
|
||||
if (err->matrix_error.unauthorized.flows.empty()) {
|
||||
nhlog::net()->warn("failed to retrieve registration flows: "
|
||||
"status_code({}), matrix_error({}) ",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error);
|
||||
showError(QString::fromStdString(err->matrix_error.error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to complete a UIA stage
|
||||
emit UIA(err->matrix_error.unauthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
|
||||
static_cast<int>(err->status_code),
|
||||
err->matrix_error.error);
|
||||
|
||||
showError(QString::fromStdString(err->matrix_error.error));
|
||||
};
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
|
||||
{
|
||||
auto completed_stages = unauthorized.completed;
|
||||
auto flows = unauthorized.flows;
|
||||
auto session =
|
||||
unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session;
|
||||
|
||||
nhlog::ui()->info("Completed stages: {}", completed_stages.size());
|
||||
|
||||
if (!completed_stages.empty()) {
|
||||
// Get rid of all flows which don't start with the sequence of
|
||||
// stages that have already been completed.
|
||||
flows.erase(
|
||||
std::remove_if(flows.begin(),
|
||||
flows.end(),
|
||||
[completed_stages](auto flow) {
|
||||
if (completed_stages.size() > flow.stages.size())
|
||||
return true;
|
||||
for (size_t f = 0; f < completed_stages.size(); f++)
|
||||
if (completed_stages[f] != flow.stages[f])
|
||||
return true;
|
||||
return false;
|
||||
}),
|
||||
flows.end());
|
||||
}
|
||||
|
||||
if (flows.empty()) {
|
||||
nhlog::ui()->error("No available registration flows!");
|
||||
showError(tr("No supported registration flows!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto current_stage = flows.front().stages.at(completed_stages.size());
|
||||
|
||||
if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
|
||||
auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this);
|
||||
|
||||
connect(captchaDialog,
|
||||
&dialogs::ReCaptcha::confirmation,
|
||||
this,
|
||||
[this, session, captchaDialog]() {
|
||||
captchaDialog->close();
|
||||
captchaDialog->deleteLater();
|
||||
doRegistrationWithAuth(mtx::user_interactive::Auth{
|
||||
session, mtx::user_interactive::auth::Fallback{}});
|
||||
});
|
||||
|
||||
connect(
|
||||
captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
|
||||
|
||||
QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); });
|
||||
|
||||
} else if (current_stage == mtx::user_interactive::auth_types::dummy) {
|
||||
doRegistrationWithAuth(
|
||||
mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
|
||||
|
||||
} else {
|
||||
// use fallback
|
||||
auto dialog = new dialogs::FallbackAuth(
|
||||
QString::fromStdString(current_stage), QString::fromStdString(session), this);
|
||||
|
||||
connect(
|
||||
dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() {
|
||||
dialog->close();
|
||||
dialog->deleteLater();
|
||||
emit registrationWithAuth(mtx::user_interactive::Auth{
|
||||
session, mtx::user_interactive::auth::Fallback{}});
|
||||
});
|
||||
|
||||
connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred);
|
||||
|
||||
dialog->show();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RegisterPage::paintEvent(QPaintEvent *)
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <memory>
|
||||
|
||||
#include <mtx/user_interactive.hpp>
|
||||
#include <mtxclient/http/client.hpp>
|
||||
|
||||
class FlatButton;
|
||||
class RaisedButton;
|
||||
|
@ -33,17 +34,16 @@ signals:
|
|||
void errorOccurred();
|
||||
|
||||
//! Used to trigger the corresponding slot outside of the main thread.
|
||||
void versionErrorCb(const QString &err);
|
||||
void serverError(const QString &err);
|
||||
|
||||
void wellKnownLookup();
|
||||
void versionsCheck();
|
||||
void registration();
|
||||
void UIA(const mtx::user_interactive::Unauthorized &unauthorized);
|
||||
void registrationWithAuth(const mtx::user_interactive::Auth &auth);
|
||||
|
||||
void registering();
|
||||
void registerOk();
|
||||
void registerErrorCb(const QString &msg);
|
||||
void registrationFlow(const std::string &user,
|
||||
const std::string &pass,
|
||||
const mtx::user_interactive::Unauthorized &unauthorized);
|
||||
void registerAuth(const std::string &user,
|
||||
const std::string &pass,
|
||||
const mtx::user_interactive::Auth &auth);
|
||||
|
||||
private slots:
|
||||
void onBackButtonClicked();
|
||||
|
@ -51,12 +51,22 @@ private slots:
|
|||
|
||||
// function for showing different errors
|
||||
void showError(const QString &msg);
|
||||
void showError(QLabel *label, const QString &msg);
|
||||
|
||||
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
|
||||
bool checkUsername();
|
||||
bool checkPassword();
|
||||
bool checkPasswordConfirmation();
|
||||
bool checkServer();
|
||||
|
||||
void doWellKnownLookup();
|
||||
void doVersionsCheck();
|
||||
void doRegistration();
|
||||
void doUIA(const mtx::user_interactive::Unauthorized &unauthorized);
|
||||
void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth);
|
||||
mtx::http::Callback<mtx::responses::Register> registrationCb();
|
||||
|
||||
private:
|
||||
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
|
||||
bool checkFields();
|
||||
void showError(QLabel *label, const QString &msg);
|
||||
void checkVersionAndRegister(const std::string &username, const std::string &password);
|
||||
QVBoxLayout *top_layout_;
|
||||
|
||||
QHBoxLayout *back_layout_;
|
||||
|
@ -69,6 +79,7 @@ private:
|
|||
QLabel *error_password_label_;
|
||||
QLabel *error_password_confirmation_label_;
|
||||
QLabel *error_server_label_;
|
||||
QLabel *error_registration_token_label_;
|
||||
|
||||
FlatButton *back_button_;
|
||||
RaisedButton *register_button_;
|
||||
|
@ -81,4 +92,5 @@ private:
|
|||
TextField *password_input_;
|
||||
TextField *password_confirmation_;
|
||||
TextField *server_input_;
|
||||
TextField *registration_token_input_;
|
||||
};
|
||||
|
|
|
@ -4,20 +4,35 @@
|
|||
|
||||
#include "SingleImagePackModel.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Utils.h"
|
||||
#include "timeline/Permissions.h"
|
||||
#include "timeline/TimelineModel.h"
|
||||
|
||||
Q_DECLARE_METATYPE(mtx::common::ImageInfo)
|
||||
|
||||
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, roomid_(std::move(pack_.source_room))
|
||||
, statekey_(std::move(pack_.state_key))
|
||||
, old_statekey_(statekey_)
|
||||
, pack(std::move(pack_.pack))
|
||||
{
|
||||
[[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
|
||||
|
||||
if (!pack.pack)
|
||||
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
|
||||
|
||||
for (const auto &e : pack.images)
|
||||
shortcodes.push_back(e.first);
|
||||
|
||||
connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
|
||||
}
|
||||
|
||||
int
|
||||
|
@ -61,6 +76,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
|
|||
return {};
|
||||
}
|
||||
|
||||
bool
|
||||
SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
using mtx::events::msc2545::PackUsage;
|
||||
|
||||
if (hasIndex(index.row(), index.column(), index.parent())) {
|
||||
auto &img = pack.images.at(shortcodes.at(index.row()));
|
||||
switch (role) {
|
||||
case ShortCode: {
|
||||
auto newCode = value.toString().toStdString();
|
||||
|
||||
// otherwise we delete this by accident
|
||||
if (pack.images.count(newCode))
|
||||
return false;
|
||||
|
||||
auto tmp = img;
|
||||
auto oldCode = shortcodes.at(index.row());
|
||||
pack.images.erase(oldCode);
|
||||
shortcodes[index.row()] = newCode;
|
||||
pack.images.insert({newCode, tmp});
|
||||
|
||||
emit dataChanged(
|
||||
this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
|
||||
return true;
|
||||
}
|
||||
case Body:
|
||||
img.body = value.toString().toStdString();
|
||||
emit dataChanged(
|
||||
this->index(index.row()), this->index(index.row()), {Roles::Body});
|
||||
return true;
|
||||
case IsEmote: {
|
||||
bool isEmote = value.toBool();
|
||||
bool isSticker =
|
||||
img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
|
||||
|
||||
img.usage.set(PackUsage::Emoji, isEmote);
|
||||
img.usage.set(PackUsage::Sticker, isSticker);
|
||||
|
||||
if (img.usage == pack.pack->usage)
|
||||
img.usage.reset();
|
||||
|
||||
emit dataChanged(
|
||||
this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
|
||||
|
||||
return true;
|
||||
}
|
||||
case IsSticker: {
|
||||
bool isEmote =
|
||||
img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
|
||||
bool isSticker = value.toBool();
|
||||
|
||||
img.usage.set(PackUsage::Emoji, isEmote);
|
||||
img.usage.set(PackUsage::Sticker, isSticker);
|
||||
|
||||
if (img.usage == pack.pack->usage)
|
||||
img.usage.reset();
|
||||
|
||||
emit dataChanged(
|
||||
this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
SingleImagePackModel::isGloballyEnabled() const
|
||||
{
|
||||
|
@ -98,3 +180,171 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
|
|||
// emit this->globallyEnabledChanged();
|
||||
});
|
||||
}
|
||||
|
||||
bool
|
||||
SingleImagePackModel::canEdit() const
|
||||
{
|
||||
if (roomid_.empty())
|
||||
return true;
|
||||
else
|
||||
return Permissions(QString::fromStdString(roomid_))
|
||||
.canChange(qml_mtx_events::ImagePackInRoom);
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setPackname(QString val)
|
||||
{
|
||||
auto val_ = val.toStdString();
|
||||
if (val_ != this->pack.pack->display_name) {
|
||||
this->pack.pack->display_name = val_;
|
||||
emit packnameChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setAttribution(QString val)
|
||||
{
|
||||
auto val_ = val.toStdString();
|
||||
if (val_ != this->pack.pack->attribution) {
|
||||
this->pack.pack->attribution = val_;
|
||||
emit attributionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setAvatarUrl(QString val)
|
||||
{
|
||||
auto val_ = val.toStdString();
|
||||
if (val_ != this->pack.pack->avatar_url) {
|
||||
this->pack.pack->avatar_url = val_;
|
||||
emit avatarUrlChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setStatekey(QString val)
|
||||
{
|
||||
auto val_ = val.toStdString();
|
||||
if (val_ != statekey_) {
|
||||
statekey_ = val_;
|
||||
emit statekeyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setIsStickerPack(bool val)
|
||||
{
|
||||
using mtx::events::msc2545::PackUsage;
|
||||
if (val != pack.pack->is_sticker()) {
|
||||
pack.pack->usage.set(PackUsage::Sticker, val);
|
||||
emit isStickerPackChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::setIsEmotePack(bool val)
|
||||
{
|
||||
using mtx::events::msc2545::PackUsage;
|
||||
if (val != pack.pack->is_emoji()) {
|
||||
pack.pack->usage.set(PackUsage::Emoji, val);
|
||||
emit isEmotePackChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::save()
|
||||
{
|
||||
if (roomid_.empty()) {
|
||||
http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
|
||||
if (e)
|
||||
ChatPage::instance()->showNotification(
|
||||
tr("Failed to update image pack: {}")
|
||||
.arg(QString::fromStdString(e->matrix_error.error)));
|
||||
});
|
||||
} else {
|
||||
if (old_statekey_ != statekey_) {
|
||||
http::client()->send_state_event(
|
||||
roomid_,
|
||||
to_string(mtx::events::EventType::ImagePackInRoom),
|
||||
old_statekey_,
|
||||
nlohmann::json::object(),
|
||||
[](const mtx::responses::EventId &, mtx::http::RequestErr e) {
|
||||
if (e)
|
||||
ChatPage::instance()->showNotification(
|
||||
tr("Failed to delete old image pack: {}")
|
||||
.arg(QString::fromStdString(e->matrix_error.error)));
|
||||
});
|
||||
}
|
||||
|
||||
http::client()->send_state_event(
|
||||
roomid_,
|
||||
statekey_,
|
||||
pack,
|
||||
[this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
|
||||
if (e)
|
||||
ChatPage::instance()->showNotification(
|
||||
tr("Failed to update image pack: {}")
|
||||
.arg(QString::fromStdString(e->matrix_error.error)));
|
||||
|
||||
nhlog::net()->info("Uploaded image pack: {}", statekey_);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SingleImagePackModel::addStickers(QList<QUrl> files)
|
||||
{
|
||||
for (const auto &f : files) {
|
||||
auto file = QFile(f.toLocalFile());
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
ChatPage::instance()->showNotification(
|
||||
tr("Failed to open image: {}").arg(f.toLocalFile()));
|
||||
return;
|
||||
}
|
||||
|
||||
auto bytes = file.readAll();
|
||||
auto img = utils::readImage(bytes);
|
||||
|
||||
mtx::common::ImageInfo info{};
|
||||
|
||||
auto sz = img.size() / 2;
|
||||
if (sz.width() > 512 || sz.height() > 512) {
|
||||
sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
|
||||
}
|
||||
|
||||
info.h = sz.height();
|
||||
info.w = sz.width();
|
||||
info.size = bytes.size();
|
||||
|
||||
auto filename = f.fileName().toStdString();
|
||||
http::client()->upload(
|
||||
bytes.toStdString(),
|
||||
QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
|
||||
filename,
|
||||
[this, filename, info](const mtx::responses::ContentURI &uri,
|
||||
mtx::http::RequestErr e) {
|
||||
if (e) {
|
||||
ChatPage::instance()->showNotification(
|
||||
tr("Failed to upload image: {}")
|
||||
.arg(QString::fromStdString(e->matrix_error.error)));
|
||||
return;
|
||||
}
|
||||
|
||||
emit addImage(uri.content_uri, filename, info);
|
||||
});
|
||||
}
|
||||
}
|
||||
void
|
||||
SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
|
||||
{
|
||||
mtx::events::msc2545::PackImage img{};
|
||||
img.url = uri;
|
||||
img.info = info;
|
||||
beginInsertRows(
|
||||
QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
|
||||
|
||||
pack.images[filename] = img;
|
||||
shortcodes.push_back(filename);
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
|
||||
#include <mtx/events/mscs/image_packs.hpp>
|
||||
|
||||
|
@ -15,14 +17,18 @@ class SingleImagePackModel : public QAbstractListModel
|
|||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString roomid READ roomid CONSTANT)
|
||||
Q_PROPERTY(QString statekey READ statekey CONSTANT)
|
||||
Q_PROPERTY(QString attribution READ statekey CONSTANT)
|
||||
Q_PROPERTY(QString packname READ packname CONSTANT)
|
||||
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
|
||||
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
|
||||
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
|
||||
Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
|
||||
Q_PROPERTY(
|
||||
QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
|
||||
Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
|
||||
Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
|
||||
Q_PROPERTY(
|
||||
bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
|
||||
Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
|
||||
Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
|
||||
globallyEnabledChanged)
|
||||
Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
|
||||
|
||||
public:
|
||||
enum Roles
|
||||
{
|
||||
|
@ -32,11 +38,15 @@ public:
|
|||
IsEmote,
|
||||
IsSticker,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
bool setData(const QModelIndex &index,
|
||||
const QVariant &value,
|
||||
int role = Qt::EditRole) override;
|
||||
|
||||
QString roomid() const { return QString::fromStdString(roomid_); }
|
||||
QString statekey() const { return QString::fromStdString(statekey_); }
|
||||
|
@ -47,14 +57,36 @@ public:
|
|||
bool isEmotePack() const { return pack.pack->is_emoji(); }
|
||||
|
||||
bool isGloballyEnabled() const;
|
||||
bool canEdit() const;
|
||||
void setGloballyEnabled(bool enabled);
|
||||
|
||||
void setPackname(QString val);
|
||||
void setAttribution(QString val);
|
||||
void setAvatarUrl(QString val);
|
||||
void setStatekey(QString val);
|
||||
void setIsStickerPack(bool val);
|
||||
void setIsEmotePack(bool val);
|
||||
|
||||
Q_INVOKABLE void save();
|
||||
Q_INVOKABLE void addStickers(QList<QUrl> files);
|
||||
|
||||
signals:
|
||||
void globallyEnabledChanged();
|
||||
void statekeyChanged();
|
||||
void attributionChanged();
|
||||
void packnameChanged();
|
||||
void avatarUrlChanged();
|
||||
void isEmotePackChanged();
|
||||
void isStickerPackChanged();
|
||||
|
||||
void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
|
||||
|
||||
private slots:
|
||||
void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
|
||||
|
||||
private:
|
||||
std::string roomid_;
|
||||
std::string statekey_;
|
||||
std::string statekey_, old_statekey_;
|
||||
|
||||
mtx::events::msc2545::ImagePack pack;
|
||||
std::vector<std::string> shortcodes;
|
||||
|
|
|
@ -90,13 +90,11 @@ UserSettings::load(std::optional<QString> profile)
|
|||
decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool();
|
||||
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
|
||||
privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
|
||||
shareKeysWithTrustedUsers_ =
|
||||
settings.value("user/automatically_share_keys_with_trusted_users", false).toBool();
|
||||
mobileMode_ = settings.value("user/mobile_mode", false).toBool();
|
||||
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
|
||||
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
|
||||
auto tempPresence = settings.value("user/presence", "").toString().toStdString();
|
||||
auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
|
||||
mobileMode_ = settings.value("user/mobile_mode", false).toBool();
|
||||
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
|
||||
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
|
||||
auto tempPresence = settings.value("user/presence", "").toString().toStdString();
|
||||
auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
|
||||
if (presenceValue < 0)
|
||||
presenceValue = 0;
|
||||
presence_ = static_cast<Presence>(presenceValue);
|
||||
|
@ -123,6 +121,12 @@ UserSettings::load(std::optional<QString> profile)
|
|||
userId_ = settings.value(prefix + "auth/user_id", "").toString();
|
||||
deviceId_ = settings.value(prefix + "auth/device_id", "").toString();
|
||||
|
||||
shareKeysWithTrustedUsers_ =
|
||||
settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false)
|
||||
.toBool();
|
||||
onlyShareKeysWithVerifiedUsers_ =
|
||||
settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
|
||||
|
||||
disableCertificateValidation_ =
|
||||
settings.value("disable_certificate_validation", false).toBool();
|
||||
|
||||
|
@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer)
|
|||
save();
|
||||
}
|
||||
|
||||
void
|
||||
UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
|
||||
{
|
||||
if (shareKeys == onlyShareKeysWithVerifiedUsers_)
|
||||
return;
|
||||
|
||||
onlyShareKeysWithVerifiedUsers_ = shareKeys;
|
||||
emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
|
||||
save();
|
||||
}
|
||||
|
||||
void
|
||||
UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
|
||||
{
|
||||
|
@ -610,8 +625,6 @@ UserSettings::save()
|
|||
settings.setValue("decrypt_sidebar", decryptSidebar_);
|
||||
settings.setValue("privacy_screen", privacyScreen_);
|
||||
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
|
||||
settings.setValue("automatically_share_keys_with_trusted_users",
|
||||
shareKeysWithTrustedUsers_);
|
||||
settings.setValue("mobile_mode", mobileMode_);
|
||||
settings.setValue("font_size", baseFontSize_);
|
||||
settings.setValue("typing_notifications", typingNotifications_);
|
||||
|
@ -650,6 +663,11 @@ UserSettings::save()
|
|||
settings.setValue(prefix + "auth/user_id", userId_);
|
||||
settings.setValue(prefix + "auth/device_id", deviceId_);
|
||||
|
||||
settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users",
|
||||
shareKeysWithTrustedUsers_);
|
||||
settings.setValue(prefix + "user/only_share_keys_with_verified_users",
|
||||
onlyShareKeysWithVerifiedUsers_);
|
||||
|
||||
settings.setValue("disable_certificate_validation", disableCertificateValidation_);
|
||||
|
||||
settings.sync();
|
||||
|
@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
|
||||
general_->setFont(font);
|
||||
|
||||
trayToggle_ = new Toggle{this};
|
||||
startInTrayToggle_ = new Toggle{this};
|
||||
avatarCircles_ = new Toggle{this};
|
||||
decryptSidebar_ = new Toggle(this);
|
||||
privacyScreen_ = new Toggle{this};
|
||||
shareKeysWithTrustedUsers_ = new Toggle(this);
|
||||
groupViewToggle_ = new Toggle{this};
|
||||
timelineButtonsToggle_ = new Toggle{this};
|
||||
typingNotifications_ = new Toggle{this};
|
||||
messageHoverHighlight_ = new Toggle{this};
|
||||
enlargeEmojiOnlyMessages_ = new Toggle{this};
|
||||
sortByImportance_ = new Toggle{this};
|
||||
readReceipts_ = new Toggle{this};
|
||||
markdown_ = new Toggle{this};
|
||||
desktopNotifications_ = new Toggle{this};
|
||||
alertOnNotification_ = new Toggle{this};
|
||||
useStunServer_ = new Toggle{this};
|
||||
mobileMode_ = new Toggle{this};
|
||||
scaleFactorCombo_ = new QComboBox{this};
|
||||
fontSizeCombo_ = new QComboBox{this};
|
||||
fontSelectionCombo_ = new QFontComboBox{this};
|
||||
emojiFontSelectionCombo_ = new QComboBox{this};
|
||||
ringtoneCombo_ = new QComboBox{this};
|
||||
microphoneCombo_ = new QComboBox{this};
|
||||
cameraCombo_ = new QComboBox{this};
|
||||
cameraResolutionCombo_ = new QComboBox{this};
|
||||
cameraFrameRateCombo_ = new QComboBox{this};
|
||||
timelineMaxWidthSpin_ = new QSpinBox{this};
|
||||
privacyScreenTimeout_ = new QSpinBox{this};
|
||||
trayToggle_ = new Toggle{this};
|
||||
startInTrayToggle_ = new Toggle{this};
|
||||
avatarCircles_ = new Toggle{this};
|
||||
decryptSidebar_ = new Toggle(this);
|
||||
privacyScreen_ = new Toggle{this};
|
||||
onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
|
||||
shareKeysWithTrustedUsers_ = new Toggle(this);
|
||||
groupViewToggle_ = new Toggle{this};
|
||||
timelineButtonsToggle_ = new Toggle{this};
|
||||
typingNotifications_ = new Toggle{this};
|
||||
messageHoverHighlight_ = new Toggle{this};
|
||||
enlargeEmojiOnlyMessages_ = new Toggle{this};
|
||||
sortByImportance_ = new Toggle{this};
|
||||
readReceipts_ = new Toggle{this};
|
||||
markdown_ = new Toggle{this};
|
||||
desktopNotifications_ = new Toggle{this};
|
||||
alertOnNotification_ = new Toggle{this};
|
||||
useStunServer_ = new Toggle{this};
|
||||
mobileMode_ = new Toggle{this};
|
||||
scaleFactorCombo_ = new QComboBox{this};
|
||||
fontSizeCombo_ = new QComboBox{this};
|
||||
fontSelectionCombo_ = new QFontComboBox{this};
|
||||
emojiFontSelectionCombo_ = new QComboBox{this};
|
||||
ringtoneCombo_ = new QComboBox{this};
|
||||
microphoneCombo_ = new QComboBox{this};
|
||||
cameraCombo_ = new QComboBox{this};
|
||||
cameraResolutionCombo_ = new QComboBox{this};
|
||||
cameraFrameRateCombo_ = new QComboBox{this};
|
||||
timelineMaxWidthSpin_ = new QSpinBox{this};
|
||||
privacyScreenTimeout_ = new QSpinBox{this};
|
||||
|
||||
trayToggle_->setChecked(settings_->tray());
|
||||
startInTrayToggle_->setChecked(settings_->startInTray());
|
||||
avatarCircles_->setChecked(settings_->avatarCircles());
|
||||
decryptSidebar_->setChecked(settings_->decryptSidebar());
|
||||
privacyScreen_->setChecked(settings_->privacyScreen());
|
||||
onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
|
||||
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
|
||||
groupViewToggle_->setChecked(settings_->groupView());
|
||||
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
|
||||
|
@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
formLayout_->addRow(new HorizontalLine{this});
|
||||
boxWrap(tr("Device ID"), deviceIdValue_);
|
||||
boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
|
||||
boxWrap(
|
||||
tr("Share keys with verified users and devices"),
|
||||
shareKeysWithTrustedUsers_,
|
||||
tr("Automatically replies to key requests from other users, if they are verified."));
|
||||
boxWrap(tr("Send encrypted messages to verified users only"),
|
||||
onlyShareKeysWithVerifiedUsers_,
|
||||
tr("Requires a user to be verified to send encrypted messages to them. This "
|
||||
"improves safety but makes E2EE more tedious."));
|
||||
boxWrap(tr("Share keys with verified users and devices"),
|
||||
shareKeysWithTrustedUsers_,
|
||||
tr("Automatically replies to key requests from other users, if they are verified, "
|
||||
"even if that device shouldn't have access to those keys otherwise."));
|
||||
formLayout_->addRow(new HorizontalLine{this});
|
||||
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
|
||||
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
|
||||
|
@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
}
|
||||
});
|
||||
|
||||
connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
|
||||
settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
|
||||
});
|
||||
|
||||
connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
|
||||
settings_->setShareKeysWithTrustedUsers(enabled);
|
||||
});
|
||||
|
@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *)
|
|||
groupViewToggle_->setState(settings_->groupView());
|
||||
decryptSidebar_->setState(settings_->decryptSidebar());
|
||||
privacyScreen_->setState(settings_->privacyScreen());
|
||||
onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
|
||||
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
|
||||
avatarCircles_->setState(settings_->avatarCircles());
|
||||
typingNotifications_->setState(settings_->typingNotifications());
|
||||
|
|
|
@ -88,6 +88,8 @@ class UserSettings : public QObject
|
|||
setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
|
||||
Q_PROPERTY(
|
||||
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
|
||||
Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE
|
||||
setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
|
||||
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
|
||||
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
|
||||
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
|
||||
|
@ -152,6 +154,7 @@ public:
|
|||
void setScreenShareRemoteVideo(bool state);
|
||||
void setScreenShareHideCursor(bool state);
|
||||
void setUseStunServer(bool state);
|
||||
void setOnlyShareKeysWithVerifiedUsers(bool state);
|
||||
void setShareKeysWithTrustedUsers(bool state);
|
||||
void setProfile(QString profile);
|
||||
void setUserId(QString userId);
|
||||
|
@ -208,6 +211,7 @@ public:
|
|||
bool screenShareHideCursor() const { return screenShareHideCursor_; }
|
||||
bool useStunServer() const { return useStunServer_; }
|
||||
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
|
||||
bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
|
||||
QString profile() const { return profile_; }
|
||||
QString userId() const { return userId_; }
|
||||
QString accessToken() const { return accessToken_; }
|
||||
|
@ -252,6 +256,7 @@ signals:
|
|||
void screenShareRemoteVideoChanged(bool state);
|
||||
void screenShareHideCursorChanged(bool state);
|
||||
void useStunServerChanged(bool state);
|
||||
void onlyShareKeysWithVerifiedUsersChanged(bool state);
|
||||
void shareKeysWithTrustedUsersChanged(bool state);
|
||||
void profileChanged(QString profile);
|
||||
void userIdChanged(QString userId);
|
||||
|
@ -284,6 +289,7 @@ private:
|
|||
bool privacyScreen_;
|
||||
int privacyScreenTimeout_;
|
||||
bool shareKeysWithTrustedUsers_;
|
||||
bool onlyShareKeysWithVerifiedUsers_;
|
||||
bool mobileMode_;
|
||||
int timelineMaxWidth_;
|
||||
int roomListWidth_;
|
||||
|
@ -372,6 +378,7 @@ private:
|
|||
Toggle *privacyScreen_;
|
||||
QSpinBox *privacyScreenTimeout_;
|
||||
Toggle *shareKeysWithTrustedUsers_;
|
||||
Toggle *onlyShareKeysWithVerifiedUsers_;
|
||||
Toggle *mobileMode_;
|
||||
QLabel *deviceFingerprintValue_;
|
||||
QLabel *deviceIdValue_;
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontDatabase>
|
||||
#include <QTextBrowser>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "Logging.h"
|
||||
#include "MainWindow.h"
|
||||
#include "ui/FlatButton.h"
|
||||
|
||||
namespace dialogs {
|
||||
|
||||
class RawMessage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RawMessage(QString msg, QWidget *parent = nullptr)
|
||||
: QWidget{parent}
|
||||
{
|
||||
QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
|
||||
auto layout = new QVBoxLayout{this};
|
||||
auto viewer = new QTextBrowser{this};
|
||||
viewer->setFont(monospaceFont);
|
||||
viewer->setText(msg);
|
||||
|
||||
layout->setSpacing(0);
|
||||
layout->setMargin(0);
|
||||
layout->addWidget(viewer);
|
||||
|
||||
setAutoFillBackground(true);
|
||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
QSize winsize;
|
||||
QPoint center;
|
||||
|
||||
auto window = MainWindow::instance();
|
||||
if (window) {
|
||||
winsize = window->frameGeometry().size();
|
||||
center = window->frameGeometry().center();
|
||||
|
||||
move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
|
||||
} else {
|
||||
nhlog::ui()->warn("unable to retrieve MainWindow's size");
|
||||
}
|
||||
|
||||
raise();
|
||||
show();
|
||||
}
|
||||
};
|
||||
} // namespace dialogs
|
|
@ -1,179 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QDebug>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QListWidgetItem>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QShortcut>
|
||||
#include <QStyleOption>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "dialogs/ReadReceipts.h"
|
||||
|
||||
#include "AvatarProvider.h"
|
||||
#include "Cache.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Config.h"
|
||||
#include "Utils.h"
|
||||
#include "ui/Avatar.h"
|
||||
|
||||
using namespace dialogs;
|
||||
|
||||
ReceiptItem::ReceiptItem(QWidget *parent,
|
||||
const QString &user_id,
|
||||
uint64_t timestamp,
|
||||
const QString &room_id)
|
||||
: QWidget(parent)
|
||||
{
|
||||
topLayout_ = new QHBoxLayout(this);
|
||||
topLayout_->setMargin(0);
|
||||
|
||||
textLayout_ = new QVBoxLayout;
|
||||
textLayout_->setMargin(0);
|
||||
textLayout_->setSpacing(conf::modals::TEXT_SPACING);
|
||||
|
||||
QFont nameFont;
|
||||
nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
|
||||
|
||||
auto displayName = cache::displayName(room_id, user_id);
|
||||
|
||||
avatar_ = new Avatar(this, 44);
|
||||
avatar_->setLetter(utils::firstChar(displayName));
|
||||
|
||||
// If it's a matrix id we use the second letter.
|
||||
if (displayName.size() > 1 && displayName.at(0) == '@')
|
||||
avatar_->setLetter(QChar(displayName.at(1)));
|
||||
|
||||
userName_ = new QLabel(displayName, this);
|
||||
userName_->setFont(nameFont);
|
||||
|
||||
timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this);
|
||||
|
||||
textLayout_->addWidget(userName_);
|
||||
textLayout_->addWidget(timestamp_);
|
||||
|
||||
topLayout_->addWidget(avatar_);
|
||||
topLayout_->addLayout(textLayout_, 1);
|
||||
|
||||
avatar_->setImage(ChatPage::instance()->currentRoom(), user_id);
|
||||
}
|
||||
|
||||
void
|
||||
ReceiptItem::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QStyleOption opt;
|
||||
opt.init(this);
|
||||
QPainter p(this);
|
||||
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
||||
}
|
||||
|
||||
QString
|
||||
ReceiptItem::dateFormat(const QDateTime &then) const
|
||||
{
|
||||
auto now = QDateTime::currentDateTime();
|
||||
auto days = then.daysTo(now);
|
||||
|
||||
if (days == 0)
|
||||
return tr("Today %1")
|
||||
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
|
||||
else if (days < 2)
|
||||
return tr("Yesterday %1")
|
||||
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
|
||||
else if (days < 7)
|
||||
return QString("%1 %2")
|
||||
.arg(then.toString("dddd"))
|
||||
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
|
||||
|
||||
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
ReadReceipts::ReadReceipts(QWidget *parent)
|
||||
: QFrame(parent)
|
||||
{
|
||||
setAutoFillBackground(true);
|
||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||
setWindowModality(Qt::WindowModal);
|
||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(conf::modals::WIDGET_SPACING);
|
||||
layout->setMargin(conf::modals::WIDGET_MARGIN);
|
||||
|
||||
userList_ = new QListWidget;
|
||||
userList_->setFrameStyle(QFrame::NoFrame);
|
||||
userList_->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
userList_->setSpacing(conf::modals::TEXT_SPACING);
|
||||
|
||||
QFont largeFont;
|
||||
largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
|
||||
|
||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||
setMinimumHeight(userList_->sizeHint().height() * 2);
|
||||
setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
|
||||
QFontMetrics(largeFont).averageCharWidth() * 30 -
|
||||
2 * conf::modals::WIDGET_MARGIN));
|
||||
|
||||
QFont font;
|
||||
font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
|
||||
|
||||
topLabel_ = new QLabel(tr("Read receipts"), this);
|
||||
topLabel_->setAlignment(Qt::AlignCenter);
|
||||
topLabel_->setFont(font);
|
||||
|
||||
auto okBtn = new QPushButton(tr("Close"), this);
|
||||
|
||||
auto buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setSpacing(15);
|
||||
buttonLayout->addStretch(1);
|
||||
buttonLayout->addWidget(okBtn);
|
||||
|
||||
layout->addWidget(topLabel_);
|
||||
layout->addWidget(userList_);
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
|
||||
connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close);
|
||||
connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close);
|
||||
}
|
||||
|
||||
void
|
||||
ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &receipts)
|
||||
{
|
||||
// We want to remove any previous items that have been set.
|
||||
userList_->clear();
|
||||
|
||||
for (const auto &receipt : receipts) {
|
||||
auto user = new ReceiptItem(this,
|
||||
QString::fromStdString(receipt.second),
|
||||
receipt.first,
|
||||
ChatPage::instance()->currentRoom());
|
||||
auto item = new QListWidgetItem(userList_);
|
||||
|
||||
item->setSizeHint(user->minimumSizeHint());
|
||||
item->setFlags(Qt::NoItemFlags);
|
||||
item->setTextAlignment(Qt::AlignCenter);
|
||||
|
||||
userList_->setItemWidget(item, user);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ReadReceipts::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QStyleOption opt;
|
||||
opt.init(this);
|
||||
QPainter p(this);
|
||||
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
||||
}
|
||||
|
||||
void
|
||||
ReadReceipts::hideEvent(QHideEvent *event)
|
||||
{
|
||||
userList_->clear();
|
||||
QFrame::hideEvent(event);
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFrame>
|
||||
|
||||
class Avatar;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QHBoxLayout;
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace dialogs {
|
||||
|
||||
class ReceiptItem : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ReceiptItem(QWidget *parent,
|
||||
const QString &user_id,
|
||||
uint64_t timestamp,
|
||||
const QString &room_id);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
|
||||
private:
|
||||
QString dateFormat(const QDateTime &then) const;
|
||||
|
||||
QHBoxLayout *topLayout_;
|
||||
QVBoxLayout *textLayout_;
|
||||
|
||||
Avatar *avatar_;
|
||||
|
||||
QLabel *userName_;
|
||||
QLabel *timestamp_;
|
||||
};
|
||||
|
||||
class ReadReceipts : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ReadReceipts(QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
private:
|
||||
QLabel *topLabel_;
|
||||
|
||||
QListWidget *userList_;
|
||||
};
|
||||
} // dialogs
|
|
@ -20,8 +20,7 @@
|
|||
|
||||
Q_DECLARE_METATYPE(Reaction)
|
||||
|
||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
|
||||
1000};
|
||||
QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
|
||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
|
||||
1000};
|
||||
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000};
|
||||
|
@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *)
|
|||
mtx::events::msg::Encrypted>) {
|
||||
auto event =
|
||||
decryptEvent({room_id_, e.event_id}, e);
|
||||
if (auto dec =
|
||||
std::get_if<mtx::events::RoomEvent<
|
||||
mtx::events::msg::
|
||||
KeyVerificationRequest>>(event)) {
|
||||
emit updateFlowEventId(
|
||||
event_id.event_id.to_string());
|
||||
if (event->event) {
|
||||
if (auto dec = std::get_if<
|
||||
mtx::events::RoomEvent<
|
||||
mtx::events::msg::
|
||||
KeyVerificationRequest>>(
|
||||
&event->event.value())) {
|
||||
emit updateFlowEventId(
|
||||
event_id.event_id
|
||||
.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
|
|||
if (auto encrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
&event)) {
|
||||
mtx::events::collections::TimelineEvents *d_event =
|
||||
decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||
if (std::visit(
|
||||
auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||
if (d_event->event &&
|
||||
std::visit(
|
||||
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
|
||||
*d_event)) {
|
||||
handle_room_verification(*d_event);
|
||||
*d_event->event)) {
|
||||
handle_room_verification(*d_event->event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
|
|||
events_.insert(index, event_ptr);
|
||||
}
|
||||
|
||||
if (decrypt)
|
||||
if (decrypt) {
|
||||
if (auto encrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
event_ptr))
|
||||
return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||
event_ptr)) {
|
||||
auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||
if (decrypted->event)
|
||||
return &*decrypted->event;
|
||||
}
|
||||
}
|
||||
|
||||
return event_ptr;
|
||||
}
|
||||
|
@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
|
|||
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
|
||||
}
|
||||
|
||||
mtx::events::collections::TimelineEvents *
|
||||
olm::DecryptionResult *
|
||||
EventStore::decryptEvent(const IdIndex &idx,
|
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
|
||||
{
|
||||
|
@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||
index.session_id = e.content.session_id;
|
||||
index.sender_key = e.content.sender_key;
|
||||
|
||||
auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
|
||||
auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
|
||||
auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
|
||||
auto event_ptr = new olm::DecryptionResult(std::move(event));
|
||||
decryptedEvents_.insert(idx, event_ptr);
|
||||
return event_ptr;
|
||||
};
|
||||
|
||||
auto decryptionResult = olm::decryptEvent(index, e);
|
||||
|
||||
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
|
||||
dummy.origin_server_ts = e.origin_server_ts;
|
||||
dummy.event_id = e.event_id;
|
||||
dummy.sender = e.sender;
|
||||
|
||||
if (decryptionResult.error) {
|
||||
switch (*decryptionResult.error) {
|
||||
switch (decryptionResult.error) {
|
||||
case olm::DecryptionErrorCode::MissingSession:
|
||||
case olm::DecryptionErrorCode::MissingSessionIndex: {
|
||||
if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession)
|
||||
dummy.content.body =
|
||||
tr("-- Encrypted Event (No keys found for decryption) --",
|
||||
"Placeholder, when the message was not decrypted yet or can't "
|
||||
"be "
|
||||
"decrypted.")
|
||||
.toStdString();
|
||||
else
|
||||
dummy.content.body =
|
||||
tr("-- Encrypted Event (Key not valid for this index) --",
|
||||
"Placeholder, when the message can't be decrypted with this "
|
||||
"key since it is not valid for this index ")
|
||||
.toStdString();
|
||||
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
|
||||
index.room_id,
|
||||
index.session_id,
|
||||
e.sender);
|
||||
// we may not want to request keys during initial sync and such
|
||||
if (suppressKeyRequests)
|
||||
break;
|
||||
// TODO: Check if this actually works and look in key backup
|
||||
auto copy = e;
|
||||
copy.room_id = room_id_;
|
||||
if (pending_key_requests.count(e.content.session_id)) {
|
||||
pending_key_requests.at(e.content.session_id)
|
||||
.events.push_back(copy);
|
||||
} else {
|
||||
PendingKeyRequests request;
|
||||
request.request_id =
|
||||
"key_request." + http::client()->generate_txn_id();
|
||||
request.events.push_back(copy);
|
||||
olm::send_key_request_for(copy, request.request_id);
|
||||
pending_key_requests[e.content.session_id] = request;
|
||||
}
|
||||
|
||||
requestSession(e, false);
|
||||
break;
|
||||
}
|
||||
case olm::DecryptionErrorCode::DbError:
|
||||
|
@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||
index.session_id,
|
||||
index.sender_key,
|
||||
decryptionResult.error_message.value_or(""));
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
|
||||
"Placeholder, when the message can't be decrypted, because the DB "
|
||||
"access "
|
||||
"failed.")
|
||||
.toStdString();
|
||||
break;
|
||||
case olm::DecryptionErrorCode::DecryptionFailed:
|
||||
nhlog::crypto()->critical(
|
||||
|
@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||
index.session_id,
|
||||
index.sender_key,
|
||||
decryptionResult.error_message.value_or(""));
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (%1) --",
|
||||
"Placeholder, when the message can't be decrypted. In this case, the "
|
||||
"Olm "
|
||||
"decrytion returned an error, which is passed as %1.")
|
||||
.arg(
|
||||
QString::fromStdString(decryptionResult.error_message.value_or("")))
|
||||
.toStdString();
|
||||
break;
|
||||
case olm::DecryptionErrorCode::ParsingFailed:
|
||||
dummy.content.body =
|
||||
tr("-- Encrypted Event (Unknown event type) --",
|
||||
"Placeholder, when the message was decrypted, but we couldn't parse "
|
||||
"it, because "
|
||||
"Nheko/mtxclient don't support that event type yet.")
|
||||
.toStdString();
|
||||
break;
|
||||
case olm::DecryptionErrorCode::ReplayAttack:
|
||||
nhlog::crypto()->critical(
|
||||
|
@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||
e.event_id,
|
||||
room_id_,
|
||||
index.sender_key);
|
||||
dummy.content.body =
|
||||
tr("-- Replay attack! This message index was reused! --").toStdString();
|
||||
break;
|
||||
case olm::DecryptionErrorCode::UnknownFingerprint:
|
||||
// TODO: don't fail, just show in UI.
|
||||
nhlog::crypto()->critical("Message by unverified fingerprint {}",
|
||||
index.sender_key);
|
||||
dummy.content.body =
|
||||
tr("-- Message by unverified device! --").toStdString();
|
||||
case olm::DecryptionErrorCode::NoError:
|
||||
// unreachable
|
||||
break;
|
||||
}
|
||||
return asCacheEntry(std::move(dummy));
|
||||
}
|
||||
|
||||
std::string msg_str;
|
||||
try {
|
||||
auto session = cache::client()->getInboundMegolmSession(index);
|
||||
auto res =
|
||||
olm::client()->decrypt_group_message(session.get(), e.content.ciphertext);
|
||||
msg_str = std::string((char *)res.data.data(), res.data.size());
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
|
||||
index.room_id,
|
||||
index.session_id,
|
||||
index.sender_key,
|
||||
e.what());
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
|
||||
"Placeholder, when the message can't be decrypted, because the DB "
|
||||
"access "
|
||||
"failed.")
|
||||
.toStdString();
|
||||
return asCacheEntry(std::move(dummy));
|
||||
} catch (const mtx::crypto::olm_exception &e) {
|
||||
nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
|
||||
index.room_id,
|
||||
index.session_id,
|
||||
index.sender_key,
|
||||
e.what());
|
||||
dummy.content.body =
|
||||
tr("-- Decryption Error (%1) --",
|
||||
"Placeholder, when the message can't be decrypted. In this case, the "
|
||||
"Olm "
|
||||
"decrytion returned an error, which is passed as %1.")
|
||||
.arg(e.what())
|
||||
.toStdString();
|
||||
return asCacheEntry(std::move(dummy));
|
||||
}
|
||||
|
||||
// Add missing fields for the event.
|
||||
json body = json::parse(msg_str);
|
||||
body["event_id"] = e.event_id;
|
||||
body["sender"] = e.sender;
|
||||
body["origin_server_ts"] = e.origin_server_ts;
|
||||
body["unsigned"] = e.unsigned_data;
|
||||
|
||||
// relations are unencrypted in content...
|
||||
mtx::common::add_relations(body["content"], e.content.relations);
|
||||
|
||||
json event_array = json::array();
|
||||
event_array.push_back(body);
|
||||
|
||||
std::vector<mtx::events::collections::TimelineEvents> temp_events;
|
||||
mtx::responses::utils::parse_timeline_events(event_array, temp_events);
|
||||
|
||||
if (temp_events.size() == 1) {
|
||||
auto encInfo = mtx::accessors::file(temp_events[0]);
|
||||
|
||||
if (encInfo)
|
||||
emit newEncryptedImage(encInfo.value());
|
||||
|
||||
return asCacheEntry(std::move(temp_events[0]));
|
||||
return asCacheEntry(std::move(decryptionResult));
|
||||
}
|
||||
|
||||
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
||||
if (encInfo)
|
||||
emit newEncryptedImage(encInfo.value());
|
||||
|
||||
return asCacheEntry(std::move(decryptionResult.event.value()));
|
||||
return asCacheEntry(std::move(decryptionResult));
|
||||
}
|
||||
|
||||
void
|
||||
EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
|
||||
bool manual)
|
||||
{
|
||||
// we may not want to request keys during initial sync and such
|
||||
if (suppressKeyRequests)
|
||||
return;
|
||||
|
||||
// TODO: Look in key backup
|
||||
auto copy = ev;
|
||||
copy.room_id = room_id_;
|
||||
if (pending_key_requests.count(ev.content.session_id)) {
|
||||
auto &r = pending_key_requests.at(ev.content.session_id);
|
||||
r.events.push_back(copy);
|
||||
|
||||
// automatically request once every 10 min, manually every 1 min
|
||||
qint64 delay = manual ? 60 : (60 * 10);
|
||||
if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
|
||||
r.requested_at = QDateTime::currentSecsSinceEpoch();
|
||||
olm::send_key_request_for(copy, r.request_id);
|
||||
}
|
||||
} else {
|
||||
PendingKeyRequests request;
|
||||
request.request_id = "key_request." + http::client()->generate_txn_id();
|
||||
request.requested_at = QDateTime::currentSecsSinceEpoch();
|
||||
request.events.push_back(copy);
|
||||
olm::send_key_request_for(copy, request.request_id);
|
||||
pending_key_requests[ev.content.session_id] = request;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool
|
|||
events_by_id_.insert(index, event_ptr);
|
||||
}
|
||||
|
||||
if (decrypt)
|
||||
if (decrypt) {
|
||||
if (auto encrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
event_ptr))
|
||||
return decryptEvent(index, *encrypted);
|
||||
event_ptr)) {
|
||||
auto decrypted = decryptEvent(index, *encrypted);
|
||||
if (decrypted->event)
|
||||
return &*decrypted->event;
|
||||
}
|
||||
}
|
||||
|
||||
return event_ptr;
|
||||
}
|
||||
|
||||
olm::DecryptionErrorCode
|
||||
EventStore::decryptionError(std::string id)
|
||||
{
|
||||
if (this->thread() != QThread::currentThread())
|
||||
nhlog::db()->warn("{} called from a different thread!", __func__);
|
||||
|
||||
if (id.empty())
|
||||
return olm::DecryptionErrorCode::NoError;
|
||||
|
||||
IdIndex index{room_id_, std::move(id)};
|
||||
auto edits_ = edits(index.id);
|
||||
if (!edits_.empty()) {
|
||||
index.id = mtx::accessors::event_id(edits_.back());
|
||||
auto event_ptr =
|
||||
new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
|
||||
events_by_id_.insert(index, event_ptr);
|
||||
}
|
||||
|
||||
auto event_ptr = events_by_id_.object(index);
|
||||
if (!event_ptr) {
|
||||
auto event = cache::client()->getEvent(room_id_, index.id);
|
||||
if (!event) {
|
||||
return olm::DecryptionErrorCode::NoError;
|
||||
}
|
||||
event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
|
||||
events_by_id_.insert(index, event_ptr);
|
||||
}
|
||||
|
||||
if (auto encrypted =
|
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
|
||||
auto decrypted = decryptEvent(index, *encrypted);
|
||||
return decrypted->error;
|
||||
}
|
||||
|
||||
return olm::DecryptionErrorCode::NoError;
|
||||
}
|
||||
|
||||
void
|
||||
EventStore::fetchMore()
|
||||
{
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <mtx/responses/messages.hpp>
|
||||
#include <mtx/responses/sync.hpp>
|
||||
|
||||
#include "Olm.h"
|
||||
#include "Reaction.h"
|
||||
|
||||
class EventStore : public QObject
|
||||
|
@ -78,6 +79,9 @@ public:
|
|||
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
|
||||
|
||||
QVariantList reactions(const std::string &event_id);
|
||||
olm::DecryptionErrorCode decryptionError(std::string id);
|
||||
void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
|
||||
bool manual);
|
||||
|
||||
int size() const
|
||||
{
|
||||
|
@ -119,7 +123,7 @@ public slots:
|
|||
|
||||
private:
|
||||
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
|
||||
mtx::events::collections::TimelineEvents *decryptEvent(
|
||||
olm::DecryptionResult *decryptEvent(
|
||||
const IdIndex &idx,
|
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
||||
void handle_room_verification(mtx::events::collections::TimelineEvents event);
|
||||
|
@ -129,7 +133,7 @@ private:
|
|||
uint64_t first = std::numeric_limits<uint64_t>::max(),
|
||||
last = std::numeric_limits<uint64_t>::max();
|
||||
|
||||
static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
|
||||
static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
|
||||
static QCache<Index, mtx::events::collections::TimelineEvents> events_;
|
||||
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
|
||||
|
||||
|
@ -137,6 +141,7 @@ private:
|
|||
{
|
||||
std::string request_id;
|
||||
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
|
||||
qint64 requested_at;
|
||||
};
|
||||
std::map<std::string, PendingKeyRequests> pending_key_requests;
|
||||
|
||||
|
|
|
@ -533,6 +533,8 @@ RoomlistModel::initializeRooms()
|
|||
for (const auto &id : cache::client()->roomIds())
|
||||
addRoom(id, true);
|
||||
|
||||
nhlog::db()->info("Restored {} rooms from cache", rowCount());
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,9 +28,9 @@
|
|||
#include "MemberList.h"
|
||||
#include "MxcImageProvider.h"
|
||||
#include "Olm.h"
|
||||
#include "ReadReceiptsModel.h"
|
||||
#include "TimelineViewManager.h"
|
||||
#include "Utils.h"
|
||||
#include "dialogs/RawMessage.h"
|
||||
|
||||
Q_DECLARE_METATYPE(QModelIndex)
|
||||
|
||||
|
@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
|
|||
case qml_mtx_events::KeyVerificationDone:
|
||||
case qml_mtx_events::KeyVerificationReady:
|
||||
return mtx::events::EventType::RoomMessage;
|
||||
//! m.image_pack, currently im.ponies.room_emotes
|
||||
case qml_mtx_events::ImagePackInRoom:
|
||||
return mtx::events::EventType::ImagePackRooms;
|
||||
//! m.image_pack, currently im.ponies.user_emotes
|
||||
case qml_mtx_events::ImagePackInAccountData:
|
||||
return mtx::events::EventType::ImagePackInAccountData;
|
||||
//! m.image_pack.rooms, currently im.ponies.emote_rooms
|
||||
case qml_mtx_events::ImagePackRooms:
|
||||
return mtx::events::EventType::ImagePackRooms;
|
||||
default:
|
||||
return mtx::events::EventType::Unsupported;
|
||||
};
|
||||
|
@ -443,6 +452,7 @@ TimelineModel::roleNames() const
|
|||
{IsEditable, "isEditable"},
|
||||
{IsEncrypted, "isEncrypted"},
|
||||
{Trustlevel, "trustlevel"},
|
||||
{EncryptionError, "encryptionError"},
|
||||
{ReplyTo, "replyTo"},
|
||||
{Reactions, "reactions"},
|
||||
{RoomId, "roomId"},
|
||||
|
@ -630,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||
return crypto::Trust::Unverified;
|
||||
}
|
||||
|
||||
case EncryptionError:
|
||||
return events.decryptionError(event_id(event));
|
||||
|
||||
case ReplyTo:
|
||||
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
|
||||
case Reactions: {
|
||||
|
@ -681,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||
m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
|
||||
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
|
||||
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
|
||||
m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
|
||||
|
||||
return QVariant(m);
|
||||
}
|
||||
|
@ -1025,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const
|
|||
}
|
||||
|
||||
void
|
||||
TimelineModel::viewRawMessage(QString id) const
|
||||
TimelineModel::viewRawMessage(QString id)
|
||||
{
|
||||
auto e = events.get(id.toStdString(), "", false);
|
||||
if (!e)
|
||||
return;
|
||||
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
||||
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
|
||||
Q_UNUSED(dialog);
|
||||
emit showRawMessageDialog(QString::fromStdString(ev));
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -1046,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId)
|
|||
}
|
||||
|
||||
void
|
||||
TimelineModel::viewDecryptedRawMessage(QString id) const
|
||||
TimelineModel::viewDecryptedRawMessage(QString id)
|
||||
{
|
||||
auto e = events.get(id.toStdString(), "");
|
||||
if (!e)
|
||||
return;
|
||||
|
||||
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
||||
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
|
||||
Q_UNUSED(dialog);
|
||||
emit showRawMessageDialog(QString::fromStdString(ev));
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -1089,9 +1101,9 @@ TimelineModel::relatedInfo(QString id)
|
|||
}
|
||||
|
||||
void
|
||||
TimelineModel::readReceiptsAction(QString id) const
|
||||
TimelineModel::showReadReceipts(QString id)
|
||||
{
|
||||
MainWindow::instance()->openReadReceiptsDialog(id);
|
||||
emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -1544,6 +1556,17 @@ TimelineModel::scrollTimerEvent()
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::requestKeyForEvent(QString id)
|
||||
{
|
||||
auto encrypted_event = events.get(id.toStdString(), "", false);
|
||||
if (encrypted_event) {
|
||||
if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||
encrypted_event))
|
||||
events.requestSession(*ev, true);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::copyLinkToEvent(QString eventId) const
|
||||
{
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "InviteesModel.h"
|
||||
#include "MemberList.h"
|
||||
#include "Permissions.h"
|
||||
#include "ReadReceiptsModel.h"
|
||||
#include "ui/RoomSettings.h"
|
||||
#include "ui/UserProfile.h"
|
||||
|
||||
|
@ -106,7 +107,13 @@ enum EventType
|
|||
KeyVerificationCancel,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationDone,
|
||||
KeyVerificationReady
|
||||
KeyVerificationReady,
|
||||
//! m.image_pack, currently im.ponies.room_emotes
|
||||
ImagePackInRoom,
|
||||
//! m.image_pack, currently im.ponies.user_emotes
|
||||
ImagePackInAccountData,
|
||||
//! m.image_pack.rooms, currently im.ponies.emote_rooms
|
||||
ImagePackRooms,
|
||||
};
|
||||
Q_ENUM_NS(EventType)
|
||||
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
|
||||
|
@ -205,6 +212,7 @@ public:
|
|||
IsEditable,
|
||||
IsEncrypted,
|
||||
Trustlevel,
|
||||
EncryptionError,
|
||||
ReplyTo,
|
||||
Reactions,
|
||||
RoomId,
|
||||
|
@ -235,13 +243,13 @@ public:
|
|||
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
|
||||
Q_INVOKABLE QString formatPowerLevelEvent(QString id);
|
||||
|
||||
Q_INVOKABLE void viewRawMessage(QString id) const;
|
||||
Q_INVOKABLE void viewRawMessage(QString id);
|
||||
Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
|
||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id);
|
||||
Q_INVOKABLE void openUserProfile(QString userid);
|
||||
Q_INVOKABLE void editAction(QString id);
|
||||
Q_INVOKABLE void replyAction(QString id);
|
||||
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
||||
Q_INVOKABLE void showReadReceipts(QString id);
|
||||
Q_INVOKABLE void redactEvent(QString id);
|
||||
Q_INVOKABLE int idToIndex(QString id) const;
|
||||
Q_INVOKABLE QString indexToId(int index) const;
|
||||
|
@ -257,6 +265,8 @@ public:
|
|||
endResetModel();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void requestKeyForEvent(QString id);
|
||||
|
||||
std::vector<::Reaction> reactions(const std::string &event_id)
|
||||
{
|
||||
auto list = events.reactions(event_id);
|
||||
|
@ -348,6 +358,8 @@ signals:
|
|||
void typingUsersChanged(std::vector<QString> users);
|
||||
void replyChanged(QString reply);
|
||||
void editChanged(QString reply);
|
||||
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
|
||||
void showRawMessageDialog(QString rawMessage);
|
||||
void paginationInProgressChanged(const bool);
|
||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
void scrollToIndex(int index);
|
||||
|
|
|
@ -161,6 +161,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||
0,
|
||||
"MtxEvent",
|
||||
"Can't instantiate enum!");
|
||||
qmlRegisterUncreatableMetaObject(
|
||||
olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
|
||||
qmlRegisterUncreatableMetaObject(
|
||||
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
|
||||
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
|
||||
|
@ -210,6 +212,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||
0,
|
||||
"InviteesModel",
|
||||
"InviteesModel needs to be instantiated on the C++ side");
|
||||
qmlRegisterUncreatableType<ReadReceiptsProxy>(
|
||||
"im.nheko",
|
||||
1,
|
||||
0,
|
||||
"ReadReceiptsProxy",
|
||||
"ReadReceiptsProxy needs to be instantiated on the C++ side");
|
||||
|
||||
static auto self = this;
|
||||
qmlRegisterSingletonType<MainWindow>(
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QSettings>
|
||||
|
||||
#include "AvatarProvider.h"
|
||||
#include "Utils.h"
|
||||
#include "ui/Avatar.h"
|
||||
|
||||
Avatar::Avatar(QWidget *parent, int size)
|
||||
: QWidget(parent)
|
||||
, size_(size)
|
||||
{
|
||||
type_ = ui::AvatarType::Letter;
|
||||
letter_ = "A";
|
||||
|
||||
QFont _font(font());
|
||||
_font.setPointSizeF(ui::FontSize);
|
||||
setFont(_font);
|
||||
|
||||
QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
|
||||
setSizePolicy(policy);
|
||||
}
|
||||
|
||||
QColor
|
||||
Avatar::textColor() const
|
||||
{
|
||||
if (!text_color_.isValid())
|
||||
return QColor("black");
|
||||
|
||||
return text_color_;
|
||||
}
|
||||
|
||||
QColor
|
||||
Avatar::backgroundColor() const
|
||||
{
|
||||
if (!text_color_.isValid())
|
||||
return QColor("white");
|
||||
|
||||
return background_color_;
|
||||
}
|
||||
|
||||
QSize
|
||||
Avatar::sizeHint() const
|
||||
{
|
||||
return QSize(size_ + 2, size_ + 2);
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setTextColor(const QColor &color)
|
||||
{
|
||||
text_color_ = color;
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setBackgroundColor(const QColor &color)
|
||||
{
|
||||
background_color_ = color;
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setLetter(const QString &letter)
|
||||
{
|
||||
letter_ = letter;
|
||||
type_ = ui::AvatarType::Letter;
|
||||
update();
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setImage(const QString &avatar_url)
|
||||
{
|
||||
avatar_url_ = avatar_url;
|
||||
AvatarProvider::resolve(avatar_url,
|
||||
static_cast<int>(size_ * pixmap_.devicePixelRatio()),
|
||||
this,
|
||||
[this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
|
||||
if (pm.isNull())
|
||||
return;
|
||||
type_ = ui::AvatarType::Image;
|
||||
pixmap_ = pm;
|
||||
pixmap_.setDevicePixelRatio(requestedRatio);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setImage(const QString &room, const QString &user)
|
||||
{
|
||||
room_ = room;
|
||||
user_ = user;
|
||||
AvatarProvider::resolve(room,
|
||||
user,
|
||||
static_cast<int>(size_ * pixmap_.devicePixelRatio()),
|
||||
this,
|
||||
[this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
|
||||
if (pm.isNull())
|
||||
return;
|
||||
type_ = ui::AvatarType::Image;
|
||||
pixmap_ = pm;
|
||||
pixmap_.setDevicePixelRatio(requestedRatio);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::setDevicePixelRatio(double ratio)
|
||||
{
|
||||
if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) {
|
||||
pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio);
|
||||
pixmap_.setDevicePixelRatio(ratio);
|
||||
|
||||
if (!avatar_url_.isEmpty())
|
||||
setImage(avatar_url_);
|
||||
else
|
||||
setImage(room_, user_);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Avatar::paintEvent(QPaintEvent *)
|
||||
{
|
||||
bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool();
|
||||
|
||||
QPainter painter(this);
|
||||
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform |
|
||||
QPainter::TextAntialiasing);
|
||||
|
||||
QRectF r = rect();
|
||||
const int hs = size_ / 2;
|
||||
|
||||
if (type_ != ui::AvatarType::Image) {
|
||||
QBrush brush;
|
||||
brush.setStyle(Qt::SolidPattern);
|
||||
brush.setColor(backgroundColor());
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(brush);
|
||||
rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3);
|
||||
} else if (painter.isActive()) {
|
||||
setDevicePixelRatio(painter.device()->devicePixelRatioF());
|
||||
}
|
||||
|
||||
switch (type_) {
|
||||
case ui::AvatarType::Image: {
|
||||
QPainterPath ppath;
|
||||
|
||||
rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_)
|
||||
: ppath.addRoundedRect(r, 3, 3);
|
||||
|
||||
painter.setClipPath(ppath);
|
||||
painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_),
|
||||
pixmap_);
|
||||
break;
|
||||
}
|
||||
case ui::AvatarType::Letter: {
|
||||
painter.setPen(textColor());
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
|
||||
#include "Theme.h"
|
||||
|
||||
class Avatar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
|
||||
Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
|
||||
|
||||
public:
|
||||
explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize);
|
||||
|
||||
void setBackgroundColor(const QColor &color);
|
||||
void setImage(const QString &avatar_url);
|
||||
void setImage(const QString &room, const QString &user);
|
||||
void setLetter(const QString &letter);
|
||||
void setTextColor(const QColor &color);
|
||||
void setDevicePixelRatio(double ratio);
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor textColor() const;
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
ui::AvatarType type_;
|
||||
QString letter_;
|
||||
QString avatar_url_, room_, user_;
|
||||
QColor background_color_;
|
||||
QColor text_color_;
|
||||
QPixmap pixmap_;
|
||||
int size_;
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QWindow>
|
||||
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
|
@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const
|
|||
MainWindow::instance()->openJoinRoomDialog(
|
||||
[](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
|
||||
}
|
||||
|
||||
void
|
||||
Nheko::reparent(QWindow *win) const
|
||||
{
|
||||
win->setTransientParent(MainWindow::instance()->windowHandle());
|
||||
}
|
||||
|
|
|
@ -4,12 +4,15 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QFontDatabase>
|
||||
#include <QObject>
|
||||
#include <QPalette>
|
||||
|
||||
#include "Theme.h"
|
||||
#include "UserProfile.h"
|
||||
|
||||
class QWindow;
|
||||
|
||||
class Nheko : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -38,12 +41,17 @@ public:
|
|||
int paddingLarge() const { return 20; }
|
||||
UserProfile *currentUser() const;
|
||||
|
||||
Q_INVOKABLE QFont monospaceFont() const
|
||||
{
|
||||
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
}
|
||||
Q_INVOKABLE void openLink(QString link) const;
|
||||
Q_INVOKABLE void setStatusMessage(QString msg) const;
|
||||
Q_INVOKABLE void showUserSettingsPage() const;
|
||||
Q_INVOKABLE void openLogoutDialog() const;
|
||||
Q_INVOKABLE void openCreateRoomDialog() const;
|
||||
Q_INVOKABLE void openJoinRoomDialog() const;
|
||||
Q_INVOKABLE void reparent(QWindow *win) const;
|
||||
|
||||
public slots:
|
||||
void updateUserProfile();
|
||||
|
|
Loading…
Reference in a new issue