Merge branch 'master' into nhekoRoomDirectory
This commit is contained in:
commit
2dfccda73c
@ -52,14 +52,14 @@ build-macos:
|
|||||||
stage: build
|
stage: build
|
||||||
tags: [macos]
|
tags: [macos]
|
||||||
before_script:
|
before_script:
|
||||||
- brew update
|
#- brew update
|
||||||
- brew reinstall --force python3
|
#- brew reinstall --force python3
|
||||||
- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
#- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
||||||
- pip3 install dmgbuild
|
- pip3 install dmgbuild
|
||||||
- rm -rf ../.hunter && mv .hunter ../.hunter || true
|
- rm -rf ../.hunter && mv .hunter ../.hunter || true
|
||||||
script:
|
script:
|
||||||
- export PATH=/usr/local/opt/qt/bin/:${PATH}
|
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
|
||||||
- export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
|
- export CMAKE_PREFIX_PATH=/usr/local/opt/qt@5
|
||||||
- cmake -GNinja -H. -Bbuild
|
- cmake -GNinja -H. -Bbuild
|
||||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||||
-DCMAKE_INSTALL_PREFIX=.deps/usr
|
-DCMAKE_INSTALL_PREFIX=.deps/usr
|
||||||
@ -91,7 +91,9 @@ build-flatpak-amd64:
|
|||||||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||||
tags: [docker]
|
tags: [docker]
|
||||||
before_script:
|
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 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.Platform//5.15
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//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'
|
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||||
tags: [docker-arm64]
|
tags: [docker-arm64]
|
||||||
before_script:
|
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 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.Platform//5.15
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//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/Logout.cpp
|
||||||
src/dialogs/PreviewUploadOverlay.cpp
|
src/dialogs/PreviewUploadOverlay.cpp
|
||||||
src/dialogs/ReCaptcha.cpp
|
src/dialogs/ReCaptcha.cpp
|
||||||
src/dialogs/ReadReceipts.cpp
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/EmojiModel.cpp
|
src/emoji/EmojiModel.cpp
|
||||||
@ -305,7 +304,6 @@ set(SRC_FILES
|
|||||||
src/timeline/RoomlistModel.cpp
|
src/timeline/RoomlistModel.cpp
|
||||||
|
|
||||||
# UI components
|
# UI components
|
||||||
src/ui/Avatar.cpp
|
|
||||||
src/ui/Badge.cpp
|
src/ui/Badge.cpp
|
||||||
src/ui/DropShadow.cpp
|
src/ui/DropShadow.cpp
|
||||||
src/ui/FlatButton.cpp
|
src/ui/FlatButton.cpp
|
||||||
@ -352,6 +350,7 @@ set(SRC_FILES
|
|||||||
src/MemberList.cpp
|
src/MemberList.cpp
|
||||||
src/MxcImageProvider.cpp
|
src/MxcImageProvider.cpp
|
||||||
src/Olm.cpp
|
src/Olm.cpp
|
||||||
|
src/ReadReceiptsModel.cpp
|
||||||
src/RegisterPage.cpp
|
src/RegisterPage.cpp
|
||||||
src/SSOHandler.cpp
|
src/SSOHandler.cpp
|
||||||
src/CombinedImagePackModel.cpp
|
src/CombinedImagePackModel.cpp
|
||||||
@ -383,7 +382,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
MatrixClient
|
MatrixClient
|
||||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
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_EXAMPLES OFF CACHE INTERNAL "")
|
||||||
set(BUILD_LIB_TESTS 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/LeaveRoom.h
|
||||||
src/dialogs/Logout.h
|
src/dialogs/Logout.h
|
||||||
src/dialogs/PreviewUploadOverlay.h
|
src/dialogs/PreviewUploadOverlay.h
|
||||||
src/dialogs/RawMessage.h
|
|
||||||
src/dialogs/ReCaptcha.h
|
src/dialogs/ReCaptcha.h
|
||||||
src/dialogs/ReadReceipts.h
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/EmojiModel.h
|
src/emoji/EmojiModel.h
|
||||||
@ -518,7 +515,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/timeline/RoomlistModel.h
|
src/timeline/RoomlistModel.h
|
||||||
|
|
||||||
# UI components
|
# UI components
|
||||||
src/ui/Avatar.h
|
|
||||||
src/ui/Badge.h
|
src/ui/Badge.h
|
||||||
src/ui/FlatButton.h
|
src/ui/FlatButton.h
|
||||||
src/ui/FloatingButton.h
|
src/ui/FloatingButton.h
|
||||||
@ -546,24 +542,26 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
|
|
||||||
src/AvatarProvider.h
|
src/AvatarProvider.h
|
||||||
src/BlurhashProvider.h
|
src/BlurhashProvider.h
|
||||||
src/Cache_p.h
|
|
||||||
src/CacheCryptoStructs.h
|
src/CacheCryptoStructs.h
|
||||||
|
src/Cache_p.h
|
||||||
src/CallDevices.h
|
src/CallDevices.h
|
||||||
src/CallManager.h
|
src/CallManager.h
|
||||||
src/ChatPage.h
|
src/ChatPage.h
|
||||||
src/Clipboard.h
|
src/Clipboard.h
|
||||||
|
src/CombinedImagePackModel.h
|
||||||
src/CompletionProxyModel.h
|
src/CompletionProxyModel.h
|
||||||
src/DeviceVerificationFlow.h
|
src/DeviceVerificationFlow.h
|
||||||
|
src/ImagePackListModel.h
|
||||||
src/InviteesModel.h
|
src/InviteesModel.h
|
||||||
src/LoginPage.h
|
src/LoginPage.h
|
||||||
src/MainWindow.h
|
src/MainWindow.h
|
||||||
src/MemberList.h
|
src/MemberList.h
|
||||||
src/MxcImageProvider.h
|
src/MxcImageProvider.h
|
||||||
|
src/Olm.h
|
||||||
src/RegisterPage.h
|
src/RegisterPage.h
|
||||||
|
src/RoomsModel.h
|
||||||
src/SSOHandler.h
|
src/SSOHandler.h
|
||||||
src/CombinedImagePackModel.h
|
|
||||||
src/SingleImagePackModel.h
|
src/SingleImagePackModel.h
|
||||||
src/ImagePackListModel.h
|
|
||||||
src/TrayIcon.h
|
src/TrayIcon.h
|
||||||
src/UserSettingsPage.h
|
src/UserSettingsPage.h
|
||||||
src/UsersModel.h
|
src/UsersModel.h
|
||||||
@ -571,6 +569,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/RoomDirectoryModel.h
|
src/RoomDirectoryModel.h
|
||||||
src/WebRTCSession.h
|
src/WebRTCSession.h
|
||||||
src/WelcomePage.h
|
src/WelcomePage.h
|
||||||
|
src/ReadReceiptsModel.h
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -19,6 +19,8 @@ finish-args:
|
|||||||
- --talk-name=org.freedesktop.secrets
|
- --talk-name=org.freedesktop.secrets
|
||||||
- --talk-name=org.freedesktop.StatusNotifierItem
|
- --talk-name=org.freedesktop.StatusNotifierItem
|
||||||
- --talk-name=org.kde.*
|
- --talk-name=org.kde.*
|
||||||
|
# needed for SingleApplication to work
|
||||||
|
- --allow=per-app-dev-shm
|
||||||
cleanup:
|
cleanup:
|
||||||
- /include
|
- /include
|
||||||
- /bin/mdb*
|
- /bin/mdb*
|
||||||
@ -161,7 +163,7 @@ modules:
|
|||||||
buildsystem: cmake-ninja
|
buildsystem: cmake-ninja
|
||||||
name: mtxclient
|
name: mtxclient
|
||||||
sources:
|
sources:
|
||||||
- commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
|
- commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
|
||||||
type: git
|
type: git
|
||||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||||
- config-opts:
|
- config-opts:
|
||||||
|
@ -11,10 +11,11 @@ import im.nheko 1.0
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: avatar
|
id: avatar
|
||||||
|
|
||||||
property alias url: img.source
|
property string url
|
||||||
property string userid
|
property string userid
|
||||||
property string displayName
|
property string displayName
|
||||||
property alias textColor: label.color
|
property alias textColor: label.color
|
||||||
|
property bool crop: true
|
||||||
|
|
||||||
signal clicked(var mouse)
|
signal clicked(var mouse)
|
||||||
|
|
||||||
@ -44,12 +45,13 @@ Rectangle {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||||
mipmap: true
|
mipmap: true
|
||||||
smooth: true
|
smooth: true
|
||||||
sourceSize.width: avatar.width
|
sourceSize.width: avatar.width
|
||||||
sourceSize.height: avatar.height
|
sourceSize.height: avatar.height
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
|
source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
@ -30,12 +30,12 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title: qsTr("Invite users to %1").arg(plainRoomName)
|
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
|
height: 380
|
||||||
width: 340
|
width: 340
|
||||||
palette: Nheko.colors
|
palette: Nheko.colors
|
||||||
color: Nheko.colors.window
|
color: Nheko.colors.window
|
||||||
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
|
Component.onCompleted: Nheko.reparent(inviteDialogRoot)
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
sequence: "Ctrl+Enter"
|
sequence: "Ctrl+Enter"
|
||||||
|
@ -7,7 +7,7 @@ import "./voip"
|
|||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -10,7 +10,7 @@ import QtGraphicalEffects 1.0
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15
|
import QtQuick.Controls 2.15
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -212,9 +212,9 @@ ScrollView {
|
|||||||
|
|
||||||
// force current read index to update
|
// force current read index to update
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (chat.model) {
|
if (chat.model)
|
||||||
chat.model.setCurrentIndex(chat.model.currentIndex);
|
chat.model.setCurrentIndex(chat.model.currentIndex);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
interval: 1000
|
interval: 1000
|
||||||
}
|
}
|
||||||
@ -349,6 +349,7 @@ ScrollView {
|
|||||||
required property string callType
|
required property string callType
|
||||||
required property var reactions
|
required property var reactions
|
||||||
required property int trustlevel
|
required property int trustlevel
|
||||||
|
required property int encryptionError
|
||||||
required property var timestamp
|
required property var timestamp
|
||||||
required property int status
|
required property int status
|
||||||
required property int index
|
required property int index
|
||||||
@ -456,6 +457,7 @@ ScrollView {
|
|||||||
callType: wrapper.callType
|
callType: wrapper.callType
|
||||||
reactions: wrapper.reactions
|
reactions: wrapper.reactions
|
||||||
trustlevel: wrapper.trustlevel
|
trustlevel: wrapper.trustlevel
|
||||||
|
encryptionError: wrapper.encryptionError
|
||||||
timestamp: wrapper.timestamp
|
timestamp: wrapper.timestamp
|
||||||
status: wrapper.status
|
status: wrapper.status
|
||||||
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
|
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
|
||||||
@ -580,7 +582,7 @@ ScrollView {
|
|||||||
|
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("Read receip&ts")
|
text: qsTr("Read receip&ts")
|
||||||
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
|
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.MenuItem {
|
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,8 +179,12 @@ Component {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 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 {
|
TapHandler {
|
||||||
margin: -Nheko.paddingSmall
|
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onSingleTapped: {
|
onSingleTapped: {
|
||||||
if (!TimelineManager.isInvite)
|
if (!TimelineManager.isInvite)
|
||||||
@ -188,6 +192,7 @@ Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
@ -203,7 +208,9 @@ Component {
|
|||||||
HoverHandler {
|
HoverHandler {
|
||||||
id: hovered
|
id: hovered
|
||||||
|
|
||||||
margin: -Nheko.paddingSmall
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@ -439,6 +446,7 @@ Component {
|
|||||||
url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
|
url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
|
||||||
displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
|
displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
|
||||||
userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
|
userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
|
||||||
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
@ -6,7 +6,7 @@ import "./ui"
|
|||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.12
|
import QtQuick.Controls 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import QtQuick.Window 2.12
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
@ -15,13 +15,13 @@ ApplicationWindow {
|
|||||||
property MemberList members
|
property MemberList members
|
||||||
|
|
||||||
title: qsTr("Members of %1").arg(members.roomName)
|
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
|
height: 650
|
||||||
width: 420
|
width: 420
|
||||||
minimumHeight: 420
|
minimumHeight: 420
|
||||||
palette: Nheko.colors
|
palette: Nheko.colors
|
||||||
color: Nheko.colors.window
|
color: Nheko.colors.window
|
||||||
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
|
Component.onCompleted: Nheko.reparent(roomMembersRoot)
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
sequence: StandardKey.Cancel
|
sequence: StandardKey.Cancel
|
||||||
|
@ -7,7 +7,7 @@ import Qt.labs.platform 1.1 as Platform
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.3
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
@ -15,14 +15,13 @@ ApplicationWindow {
|
|||||||
|
|
||||||
property var roomSettings
|
property var roomSettings
|
||||||
|
|
||||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
|
||||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
|
||||||
minimumWidth: 420
|
minimumWidth: 420
|
||||||
minimumHeight: 650
|
minimumHeight: 650
|
||||||
palette: Nheko.colors
|
palette: Nheko.colors
|
||||||
color: Nheko.colors.window
|
color: Nheko.colors.window
|
||||||
modality: Qt.NonModal
|
modality: Qt.NonModal
|
||||||
flags: Qt.Dialog
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
|
Component.onCompleted: Nheko.reparent(roomSettingsDialog)
|
||||||
title: qsTr("Room Settings")
|
title: qsTr("Room Settings")
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
@ -155,7 +154,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
columns: 2
|
columns: 2
|
||||||
rowSpacing: 10
|
rowSpacing: Nheko.paddingLarge
|
||||||
|
|
||||||
MatrixText {
|
MatrixText {
|
||||||
text: qsTr("SETTINGS")
|
text: qsTr("SETTINGS")
|
||||||
@ -181,7 +180,7 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MatrixText {
|
MatrixText {
|
||||||
text: "Room access"
|
text: qsTr("Room access")
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ import "./emoji"
|
|||||||
import "./voip"
|
import "./voip"
|
||||||
import Qt.labs.platform 1.1 as Platform
|
import Qt.labs.platform 1.1 as Platform
|
||||||
import QtGraphicalEffects 1.0
|
import QtGraphicalEffects 1.0
|
||||||
import QtQuick 2.9
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.5
|
import QtQuick.Controls 2.15
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.15
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
import im.nheko.EmojiModel 1.0
|
import im.nheko.EmojiModel 1.0
|
||||||
|
|
||||||
@ -96,6 +96,22 @@ Page {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: readReceiptsDialog
|
||||||
|
|
||||||
|
ReadReceipts {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: rawMessageDialog
|
||||||
|
|
||||||
|
RawMessageDialog {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
sequence: "Ctrl+K"
|
sequence: "Ctrl+K"
|
||||||
onActivated: {
|
onActivated: {
|
||||||
|
@ -23,6 +23,9 @@ MouseArea {
|
|||||||
// console.warn("Delta: ", wheel.pixelDelta.y);
|
// console.warn("Delta: ", wheel.pixelDelta.y);
|
||||||
// console.warn("Old position: ", flickable.contentY);
|
// console.warn("Old position: ", flickable.contentY);
|
||||||
// console.warn("New position: ", newPos);
|
// console.warn("New position: ", newPos);
|
||||||
|
// breaks ListView's with headers...
|
||||||
|
//if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
|
||||||
|
// minYExtent += flickableItem.headerItem.height;
|
||||||
|
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@ -55,9 +58,6 @@ MouseArea {
|
|||||||
|
|
||||||
var minYExtent = flickableItem.originY + flickableItem.topMargin;
|
var minYExtent = flickableItem.originY + flickableItem.topMargin;
|
||||||
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
|
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
|
||||||
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
|
|
||||||
minYExtent += flickableItem.headerItem.height;
|
|
||||||
|
|
||||||
//Avoid overscrolling
|
//Avoid overscrolling
|
||||||
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
|
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ ImageButton {
|
|||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (status == MtxEvent.Read)
|
if (status == MtxEvent.Read)
|
||||||
room.readReceiptsAction(eventId);
|
room.showReadReceipts(eventId);
|
||||||
|
|
||||||
}
|
}
|
||||||
image: {
|
image: {
|
||||||
|
@ -7,7 +7,7 @@ import "./emoji"
|
|||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@ -38,6 +38,7 @@ Item {
|
|||||||
required property string callType
|
required property string callType
|
||||||
required property var reactions
|
required property var reactions
|
||||||
required property int trustlevel
|
required property int trustlevel
|
||||||
|
required property int encryptionError
|
||||||
required property var timestamp
|
required property var timestamp
|
||||||
required property int status
|
required property int status
|
||||||
required property int relatedEventCacheBuster
|
required property int relatedEventCacheBuster
|
||||||
@ -110,6 +111,7 @@ Item {
|
|||||||
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
|
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
|
||||||
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
|
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
|
||||||
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
|
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
|
||||||
|
encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
|
||||||
relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
|
relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +138,7 @@ Item {
|
|||||||
roomTopic: r.roomTopic
|
roomTopic: r.roomTopic
|
||||||
roomName: r.roomName
|
roomName: r.roomName
|
||||||
callType: r.callType
|
callType: r.callType
|
||||||
|
encryptionError: r.encryptionError
|
||||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||||
isReply: false
|
isReply: false
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import QtGraphicalEffects 1.0
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.5
|
import QtQuick.Controls 2.5
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
import im.nheko.EmojiModel 1.0
|
import im.nheko.EmojiModel 1.0
|
||||||
|
|
||||||
@ -249,4 +249,23 @@ Item {
|
|||||||
roomid: room ? room.roomId : ""
|
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 "./device-verification"
|
||||||
import "./ui"
|
import "./ui"
|
||||||
import QtQuick 2.9
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.15
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.3
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
|
// this does not work in ApplicationWindow, just in Window
|
||||||
|
//transientParent: Nheko.mainwindow()
|
||||||
|
|
||||||
id: userProfileDialog
|
id: userProfileDialog
|
||||||
|
|
||||||
property var profile
|
property var profile
|
||||||
|
|
||||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
|
||||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
|
||||||
height: 650
|
height: 650
|
||||||
width: 420
|
width: 420
|
||||||
minimumHeight: 420
|
minimumHeight: 420
|
||||||
@ -24,7 +25,8 @@ ApplicationWindow {
|
|||||||
color: Nheko.colors.window
|
color: Nheko.colors.window
|
||||||
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
||||||
modality: Qt.NonModal
|
modality: Qt.NonModal
|
||||||
flags: Qt.Dialog
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
|
Component.onCompleted: Nheko.reparent(userProfileDialog)
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
sequence: StandardKey.Cancel
|
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 roomTopic
|
||||||
required property string roomName
|
required property string roomName
|
||||||
required property string callType
|
required property string callType
|
||||||
|
required property int encryptionError
|
||||||
required property int relatedEventCacheBuster
|
required property int relatedEventCacheBuster
|
||||||
|
|
||||||
height: chooser.childrenRect.height
|
height: chooser.childrenRect.height
|
||||||
@ -189,6 +190,16 @@ Item {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.Encrypted
|
||||||
|
|
||||||
|
Encrypted {
|
||||||
|
encryptionError: d.encryptionError
|
||||||
|
eventId: d.eventId
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.Name
|
roleValue: MtxEvent.Name
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@ -30,6 +30,7 @@ Item {
|
|||||||
property string roomTopic
|
property string roomTopic
|
||||||
property string roomName
|
property string roomName
|
||||||
property string callType
|
property string callType
|
||||||
|
property int encryptionError
|
||||||
property int relatedEventCacheBuster
|
property int relatedEventCacheBuster
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@ -97,6 +98,7 @@ Item {
|
|||||||
roomName: r.roomName
|
roomName: r.roomName
|
||||||
callType: r.callType
|
callType: r.callType
|
||||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||||
|
encryptionError: r.encryptionError
|
||||||
enabled: false
|
enabled: false
|
||||||
width: parent.width
|
width: parent.width
|
||||||
isReply: true
|
isReply: true
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Window 2.10
|
import QtQuick.Window 2.13
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
@ -14,13 +14,12 @@ ApplicationWindow {
|
|||||||
|
|
||||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||||
title: stack.currentItem.title
|
title: stack.currentItem.title
|
||||||
flags: Qt.Dialog
|
|
||||||
modality: Qt.NonModal
|
modality: Qt.NonModal
|
||||||
palette: Nheko.colors
|
palette: Nheko.colors
|
||||||
height: stack.implicitHeight
|
height: stack.implicitHeight
|
||||||
width: stack.implicitWidth
|
width: stack.implicitWidth
|
||||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
Component.onCompleted: Nheko.reparent(dialog)
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
id: stack
|
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
|
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
|
||||||
|
|
||||||
title: qsTr("Image pack settings")
|
title: qsTr("Image pack settings")
|
||||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
height: 600
|
||||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
width: 800
|
||||||
height: 400
|
|
||||||
width: 600
|
|
||||||
palette: Nheko.colors
|
palette: Nheko.colors
|
||||||
color: Nheko.colors.base
|
color: Nheko.colors.base
|
||||||
modality: Qt.NonModal
|
modality: Qt.NonModal
|
||||||
flags: Qt.Dialog
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||||
|
Component.onCompleted: Nheko.reparent(win)
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: packEditor
|
||||||
|
|
||||||
|
ImagePackEditorDialog {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
AdaptiveLayout {
|
AdaptiveLayout {
|
||||||
id: adaptiveView
|
id: adaptiveView
|
||||||
@ -55,7 +62,35 @@ ApplicationWindow {
|
|||||||
enabled: !Settings.mobileMode
|
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
|
id: packItem
|
||||||
|
|
||||||
property color background: Nheko.colors.window
|
property color background: Nheko.colors.window
|
||||||
@ -64,111 +99,11 @@ ApplicationWindow {
|
|||||||
property color bubbleBackground: Nheko.colors.highlight
|
property color bubbleBackground: Nheko.colors.highlight
|
||||||
property color bubbleText: Nheko.colors.highlightedText
|
property color bubbleText: Nheko.colors.highlightedText
|
||||||
required property string displayName
|
required property string displayName
|
||||||
required property string avatarUrl
|
|
||||||
required property bool fromAccountData
|
required property bool fromAccountData
|
||||||
required property bool fromCurrentRoom
|
required property bool fromCurrentRoom
|
||||||
required property int index
|
|
||||||
|
|
||||||
color: background
|
title: displayName
|
||||||
height: avatarSize + 2 * Nheko.paddingMedium
|
subtitle: {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
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)
|
if (fromAccountData)
|
||||||
return qsTr("Private pack");
|
return qsTr("Private pack");
|
||||||
else if (fromCurrentRoom)
|
else if (fromCurrentRoom)
|
||||||
@ -176,17 +111,10 @@ ApplicationWindow {
|
|||||||
else
|
else
|
||||||
return qsTr("Globally enabled pack");
|
return qsTr("Globally enabled pack");
|
||||||
}
|
}
|
||||||
textFormat: Text.PlainText
|
selectedIndex: currentPackIndex
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onSingleTapped: currentPackIndex = index
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -205,6 +133,7 @@ ApplicationWindow {
|
|||||||
id: packinfo
|
id: packinfo
|
||||||
|
|
||||||
property string packName: currentPack ? currentPack.packname : ""
|
property string packName: currentPack ? currentPack.packname : ""
|
||||||
|
property string attribution: currentPack ? currentPack.attribution : ""
|
||||||
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
|
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -222,8 +151,18 @@ ApplicationWindow {
|
|||||||
|
|
||||||
MatrixText {
|
MatrixText {
|
||||||
text: packinfo.packName
|
text: packinfo.packName
|
||||||
font.pixelSize: 24
|
font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1)
|
||||||
|
horizontalAlignment: TextEdit.AlignHCenter
|
||||||
Layout.alignment: Qt.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 {
|
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 {
|
GridView {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@ -267,7 +218,7 @@ ApplicationWindow {
|
|||||||
width: stickerDim
|
width: stickerDim
|
||||||
height: stickerDim
|
height: stickerDim
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
ToolTip.text: ":" + model.shortcode + ": - " + model.body
|
ToolTip.text: ":" + model.shortCode + ": - " + model.body
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
contentItem: Image {
|
contentItem: Image {
|
||||||
|
@ -16,6 +16,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
modality: Qt.NonModal
|
modality: Qt.NonModal
|
||||||
flags: Qt.Dialog
|
flags: Qt.Dialog
|
||||||
|
Component.onCompleted: Nheko.reparent(inputDialog)
|
||||||
width: 350
|
width: 350
|
||||||
height: fontMetrics.lineSpacing * 7
|
height: fontMetrics.lineSpacing * 7
|
||||||
|
|
||||||
|
@ -112,7 +112,6 @@
|
|||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>qtquickcontrols2.conf</file>
|
<file>qtquickcontrols2.conf</file>
|
||||||
|
|
||||||
<file>qml/Root.qml</file>
|
<file>qml/Root.qml</file>
|
||||||
<file>qml/ChatPage.qml</file>
|
<file>qml/ChatPage.qml</file>
|
||||||
<file>qml/CommunitiesList.qml</file>
|
<file>qml/CommunitiesList.qml</file>
|
||||||
@ -149,10 +148,16 @@
|
|||||||
<file>qml/delegates/NoticeMessage.qml</file>
|
<file>qml/delegates/NoticeMessage.qml</file>
|
||||||
<file>qml/delegates/ImageMessage.qml</file>
|
<file>qml/delegates/ImageMessage.qml</file>
|
||||||
<file>qml/delegates/PlayableMediaMessage.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/FileMessage.qml</file>
|
||||||
|
<file>qml/delegates/ImageMessage.qml</file>
|
||||||
|
<file>qml/delegates/NoticeMessage.qml</file>
|
||||||
<file>qml/delegates/Pill.qml</file>
|
<file>qml/delegates/Pill.qml</file>
|
||||||
<file>qml/delegates/Placeholder.qml</file>
|
<file>qml/delegates/Placeholder.qml</file>
|
||||||
|
<file>qml/delegates/PlayableMediaMessage.qml</file>
|
||||||
<file>qml/delegates/Reply.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/Waiting.qml</file>
|
||||||
<file>qml/device-verification/DeviceVerification.qml</file>
|
<file>qml/device-verification/DeviceVerification.qml</file>
|
||||||
<file>qml/device-verification/DigitVerification.qml</file>
|
<file>qml/device-verification/DigitVerification.qml</file>
|
||||||
@ -162,6 +167,7 @@
|
|||||||
<file>qml/device-verification/Success.qml</file>
|
<file>qml/device-verification/Success.qml</file>
|
||||||
<file>qml/dialogs/InputDialog.qml</file>
|
<file>qml/dialogs/InputDialog.qml</file>
|
||||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||||
|
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
|
||||||
<file>qml/ui/Ripple.qml</file>
|
<file>qml/ui/Ripple.qml</file>
|
||||||
<file>qml/ui/Spinner.qml</file>
|
<file>qml/ui/Spinner.qml</file>
|
||||||
<file>qml/ui/animations/BlinkAnimation.qml</file>
|
<file>qml/ui/animations/BlinkAnimation.qml</file>
|
||||||
@ -175,9 +181,12 @@
|
|||||||
<file>qml/voip/VideoCall.qml</file>
|
<file>qml/voip/VideoCall.qml</file>
|
||||||
<file>qml/components/AdaptiveLayout.qml</file>
|
<file>qml/components/AdaptiveLayout.qml</file>
|
||||||
<file>qml/components/AdaptiveLayoutElement.qml</file>
|
<file>qml/components/AdaptiveLayoutElement.qml</file>
|
||||||
|
<file>qml/components/AvatarListTile.qml</file>
|
||||||
<file>qml/components/FlatButton.qml</file>
|
<file>qml/components/FlatButton.qml</file>
|
||||||
<file>qml/RoomMembers.qml</file>
|
<file>qml/RoomMembers.qml</file>
|
||||||
<file>qml/InviteDialog.qml</file>
|
<file>qml/InviteDialog.qml</file>
|
||||||
|
<file>qml/ReadReceipts.qml</file>
|
||||||
|
<file>qml/RawMessageDialog.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/media">
|
<qresource prefix="/media">
|
||||||
<file>media/ring.ogg</file>
|
<file>media/ring.ogg</file>
|
||||||
|
@ -125,7 +125,7 @@ template<class T>
|
|||||||
bool
|
bool
|
||||||
containsStateUpdates(const T &e)
|
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
|
bool
|
||||||
@ -288,6 +288,9 @@ Cache::setup()
|
|||||||
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
|
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
|
||||||
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_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();
|
txn.commit();
|
||||||
|
|
||||||
databaseReady_ = true;
|
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);
|
nhlog::db()->info("mark room {} as encrypted", room_id);
|
||||||
|
|
||||||
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
|
encryptedRooms_.put(txn, room_id, "0");
|
||||||
db.put(txn, room_id, "0");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id)
|
|||||||
std::string_view unused;
|
std::string_view unused;
|
||||||
|
|
||||||
auto txn = ro_txn(env_);
|
auto txn = ro_txn(env_);
|
||||||
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
|
auto res = encryptedRooms_.get(txn, room_id, unused);
|
||||||
auto res = db.get(txn, room_id, unused);
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -3400,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
|
|||||||
info.pack.pack = pack.pack;
|
info.pack.pack = pack.pack;
|
||||||
|
|
||||||
for (const auto &img : pack.images) {
|
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()))
|
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -3541,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::map<std::string, std::optional<UserKeyCache>>
|
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;
|
std::string_view keys;
|
||||||
|
|
||||||
@ -3558,9 +3559,50 @@ Cache::getMembersWithKeys(const std::string &room_id)
|
|||||||
auto res = keysDb.get(txn, user_id, keys);
|
auto res = keysDb.get(txn, user_id, keys);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
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)] =
|
members[std::string(user_id)] =
|
||||||
json::parse(keys).get<UserKeyCache>();
|
std::move(keyCopy);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
members[std::string(user_id)] = std::move(k);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!verified_only)
|
||||||
members[std::string(user_id)] = {};
|
members[std::string(user_id)] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,8 @@ public:
|
|||||||
// user cache stores user keys
|
// user cache stores user keys
|
||||||
std::optional<UserKeyCache> userKeys(const std::string &user_id);
|
std::optional<UserKeyCache> userKeys(const std::string &user_id);
|
||||||
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
|
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,
|
void updateUserKeys(const std::string &sync_token,
|
||||||
const mtx::responses::QueryKeys &keyQuery);
|
const mtx::responses::QueryKeys &keyQuery);
|
||||||
void markUserKeysOutOfDate(lmdb::txn &txn,
|
void markUserKeysOutOfDate(lmdb::txn &txn,
|
||||||
@ -290,15 +291,9 @@ public:
|
|||||||
std::optional<std::string> secret(const std::string name);
|
std::optional<std::string> secret(const std::string name);
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
|
constexpr static bool isStateEvent_ =
|
||||||
{
|
std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
|
||||||
return true;
|
mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
|
||||||
}
|
|
||||||
template<class T>
|
|
||||||
static constexpr bool isStateEvent(const mtx::events::Event<T> &)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int compare_state_key(const MDB_val *a, const MDB_val *b)
|
static int compare_state_key(const MDB_val *a, const MDB_val *b)
|
||||||
{
|
{
|
||||||
@ -415,11 +410,27 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::visit(
|
std::visit(
|
||||||
[&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
|
[&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
|
||||||
if constexpr (isStateEvent(e)) {
|
if constexpr (isStateEvent_<decltype(e)>) {
|
||||||
eventsDb.put(txn, e.event_id, json(e).dump());
|
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())
|
if (e.state_key.empty())
|
||||||
statesdb.put(
|
statesdb.put(
|
||||||
txn, to_string(e.type), json(e).dump());
|
txn, to_string(e.type), json(e).dump());
|
||||||
@ -689,6 +700,8 @@ private:
|
|||||||
lmdb::dbi outboundMegolmSessionDb_;
|
lmdb::dbi outboundMegolmSessionDb_;
|
||||||
lmdb::dbi megolmSessionDataDb_;
|
lmdb::dbi megolmSessionDataDb_;
|
||||||
|
|
||||||
|
lmdb::dbi encryptedRooms_;
|
||||||
|
|
||||||
QString localUserId_;
|
QString localUserId_;
|
||||||
QString cacheDirectory_;
|
QString cacheDirectory_;
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
|
|
||||||
#include "notifications/Manager.h"
|
#include "notifications/Manager.h"
|
||||||
|
|
||||||
#include "dialogs/ReadReceipts.h"
|
|
||||||
#include "timeline/TimelineViewManager.h"
|
#include "timeline/TimelineViewManager.h"
|
||||||
|
|
||||||
#include "blurhash.hpp"
|
#include "blurhash.hpp"
|
||||||
|
@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row)
|
|||||||
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
|
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
|
||||||
return e;
|
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
|
class ImagePackListModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
|
||||||
public:
|
public:
|
||||||
enum Roles
|
enum Roles
|
||||||
{
|
{
|
||||||
@ -29,6 +30,9 @@ public:
|
|||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
Q_INVOKABLE SingleImagePackModel *packAt(int row);
|
Q_INVOKABLE SingleImagePackModel *packAt(int row);
|
||||||
|
Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
|
||||||
|
|
||||||
|
bool containsAccountPack() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string room_id;
|
std::string room_id;
|
||||||
|
@ -36,7 +36,6 @@
|
|||||||
#include "dialogs/JoinRoom.h"
|
#include "dialogs/JoinRoom.h"
|
||||||
#include "dialogs/LeaveRoom.h"
|
#include "dialogs/LeaveRoom.h"
|
||||||
#include "dialogs/Logout.h"
|
#include "dialogs/Logout.h"
|
||||||
#include "dialogs/ReadReceipts.h"
|
|
||||||
|
|
||||||
MainWindow *MainWindow::instance_ = nullptr;
|
MainWindow *MainWindow::instance_ = nullptr;
|
||||||
|
|
||||||
@ -398,27 +397,6 @@ MainWindow::openLogoutDialog()
|
|||||||
showDialog(dialog);
|
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
|
bool
|
||||||
MainWindow::hasActiveDialogs() const
|
MainWindow::hasActiveDialogs() const
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,6 @@ public:
|
|||||||
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
||||||
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
||||||
void openLogoutDialog();
|
void openLogoutDialog();
|
||||||
void openReadReceiptsDialog(const QString &event_id);
|
|
||||||
|
|
||||||
void hideOverlay();
|
void hideOverlay();
|
||||||
void showSolidOverlayModal(QWidget *content,
|
void showSolidOverlayModal(QWidget *content,
|
||||||
|
@ -2,16 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// 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 "MemberList.h"
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
@ -20,7 +10,6 @@
|
|||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "timeline/TimelineViewManager.h"
|
#include "timeline/TimelineViewManager.h"
|
||||||
#include "ui/Avatar.h"
|
|
||||||
|
|
||||||
MemberList::MemberList(const QString &room_id, QObject *parent)
|
MemberList::MemberList(const QString &room_id, QObject *parent)
|
||||||
: QAbstractListModel{parent}
|
: QAbstractListModel{parent}
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CacheStructs.h"
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
#include "CacheStructs.h"
|
||||||
|
|
||||||
class MemberList : public QAbstractListModel
|
class MemberList : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
|
|||||||
QQuickImageResponse *
|
QQuickImageResponse *
|
||||||
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
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);
|
pool.start(response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -36,20 +43,24 @@ void
|
|||||||
MxcImageResponse::run()
|
MxcImageResponse::run()
|
||||||
{
|
{
|
||||||
MxcImageProvider::download(
|
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()) {
|
if (image.isNull()) {
|
||||||
m_error = "Failed to download image.";
|
m_error = "Failed to download image.";
|
||||||
} else {
|
} else {
|
||||||
m_image = image;
|
m_image = image;
|
||||||
}
|
}
|
||||||
emit finished();
|
emit finished();
|
||||||
});
|
},
|
||||||
|
m_crop);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MxcImageProvider::download(const QString &id,
|
MxcImageProvider::download(const QString &id,
|
||||||
const QSize &requestedSize,
|
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;
|
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
|
||||||
auto temp = infos.find("mxc://" + id);
|
auto temp = infos.find("mxc://" + id);
|
||||||
@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
|
|||||||
|
|
||||||
if (requestedSize.isValid() && !encryptionInfo) {
|
if (requestedSize.isValid() && !encryptionInfo) {
|
||||||
QString fileName =
|
QString fileName =
|
||||||
QString("%1_%2x%3_crop")
|
QString("%1_%2x%3_%4")
|
||||||
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
|
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
|
||||||
QByteArray::OmitTrailingEquals)))
|
QByteArray::OmitTrailingEquals)))
|
||||||
.arg(requestedSize.width())
|
.arg(requestedSize.width())
|
||||||
.arg(requestedSize.height());
|
.arg(requestedSize.height())
|
||||||
|
.arg(crop ? "crop" : "scale");
|
||||||
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
"/media_cache",
|
"/media_cache",
|
||||||
fileName);
|
fileName);
|
||||||
@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
|
|||||||
opts.mxc_url = "mxc://" + id.toStdString();
|
opts.mxc_url = "mxc://" + id.toStdString();
|
||||||
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
|
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
|
||||||
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
|
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
|
||||||
opts.method = "crop";
|
opts.method = crop ? "crop" : "scale";
|
||||||
http::client()->get_thumbnail(
|
http::client()->get_thumbnail(
|
||||||
opts,
|
opts,
|
||||||
[fileInfo, requestedSize, then, id](const std::string &res,
|
[fileInfo, requestedSize, then, id](const std::string &res,
|
||||||
|
@ -19,9 +19,10 @@ class MxcImageResponse
|
|||||||
, public QRunnable
|
, public QRunnable
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MxcImageResponse(const QString &id, const QSize &requestedSize)
|
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
|
||||||
: m_id(id)
|
: m_id(id)
|
||||||
, m_requestedSize(requestedSize)
|
, m_requestedSize(requestedSize)
|
||||||
|
, m_crop(crop)
|
||||||
{
|
{
|
||||||
setAutoDelete(false);
|
setAutoDelete(false);
|
||||||
}
|
}
|
||||||
@ -37,6 +38,7 @@ public:
|
|||||||
QString m_id, m_error;
|
QString m_id, m_error;
|
||||||
QSize m_requestedSize;
|
QSize m_requestedSize;
|
||||||
QImage m_image;
|
QImage m_image;
|
||||||
|
bool m_crop;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MxcImageProvider
|
class MxcImageProvider
|
||||||
@ -51,7 +53,8 @@ public slots:
|
|||||||
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
|
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
|
||||||
static void download(const QString &id,
|
static void download(const QString &id,
|
||||||
const QSize &requestedSize,
|
const QSize &requestedSize,
|
||||||
std::function<void(QString, QSize, QImage, QString)> then);
|
std::function<void(QString, QSize, QImage, QString)> then,
|
||||||
|
bool crop = true);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QThreadPool pool;
|
QThreadPool pool;
|
||||||
|
35
src/Olm.cpp
35
src/Olm.cpp
@ -286,13 +286,19 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
|
|||||||
|
|
||||||
bool from_their_device = false;
|
bool from_their_device = false;
|
||||||
for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
|
for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
|
||||||
if (key.keys.at("curve25519:" + device_id) == msg.sender_key) {
|
auto c_key = key.keys.find("curve25519:" + device_id);
|
||||||
if (key.keys.at("ed25519:" + device_id) == sender_ed25519) {
|
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;
|
from_their_device = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!from_their_device) {
|
if (!from_their_device) {
|
||||||
nhlog::crypto()->warn("Decrypted event isn't sent from a device "
|
nhlog::crypto()->warn("Decrypted event isn't sent from a device "
|
||||||
"listed by that user! {}",
|
"listed by that user! {}",
|
||||||
@ -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 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;
|
std::map<std::string, std::vector<std::string>> sendSessionTo;
|
||||||
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
|
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
|
||||||
@ -1062,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index,
|
|||||||
mtx::events::collections::TimelineEvent te;
|
mtx::events::collections::TimelineEvent te;
|
||||||
mtx::events::collections::from_json(body, 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) {
|
} catch (std::exception &e) {
|
||||||
return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
|
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);
|
auto session = cache::getLatestOlmSession(device_curve);
|
||||||
if (!session || force_new_session) {
|
if (!session || force_new_session) {
|
||||||
claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519;
|
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].ed25519 = d.keys.at("ed25519:" + device);
|
||||||
pks[user][device].curve25519 = d.keys.at("curve25519:" + 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
|
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
|
||||||
|
|
||||||
namespace olm {
|
namespace olm {
|
||||||
|
Q_NAMESPACE
|
||||||
|
|
||||||
enum class DecryptionErrorCode
|
enum DecryptionErrorCode
|
||||||
{
|
{
|
||||||
|
NoError,
|
||||||
MissingSession, // Session was not found, retrieve from backup or request from other devices
|
MissingSession, // Session was not found, retrieve from backup or request from other devices
|
||||||
// and try again
|
// and try again
|
||||||
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
|
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
|
||||||
@ -25,14 +27,13 @@ enum class DecryptionErrorCode
|
|||||||
DecryptionFailed, // libolm error
|
DecryptionFailed, // libolm error
|
||||||
ParsingFailed, // Failed to parse the actual event
|
ParsingFailed, // Failed to parse the actual event
|
||||||
ReplayAttack, // Megolm index reused
|
ReplayAttack, // Megolm index reused
|
||||||
UnknownFingerprint, // Unknown device Fingerprint
|
|
||||||
};
|
};
|
||||||
|
Q_ENUM_NS(DecryptionErrorCode)
|
||||||
|
|
||||||
struct DecryptionResult
|
struct DecryptionResult
|
||||||
{
|
{
|
||||||
std::optional<DecryptionErrorCode> error;
|
DecryptionErrorCode error;
|
||||||
std::optional<std::string> error_message;
|
std::optional<std::string> error_message;
|
||||||
|
|
||||||
std::optional<mtx::events::collections::TimelineEvents> event;
|
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/register.hpp>
|
||||||
#include <mtx/responses/well-known.hpp>
|
#include <mtx/responses/well-known.hpp>
|
||||||
|
#include <mtxclient/http/client.hpp>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent)
|
|||||||
|
|
||||||
server_input_ = new TextField();
|
server_input_ = new TextField();
|
||||||
server_input_->setLabel(tr("Homeserver"));
|
server_input_->setLabel(tr("Homeserver"));
|
||||||
|
server_input_->setRegexp(QRegularExpression(".+"));
|
||||||
server_input_->setToolTip(
|
server_input_->setToolTip(
|
||||||
tr("A server that allows registration. Since matrix is decentralized, you need to first "
|
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."));
|
"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_->addLayout(button_layout_);
|
||||||
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
|
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
|
||||||
top_layout_->addStretch(1);
|
top_layout_->addStretch(1);
|
||||||
|
setLayout(top_layout_);
|
||||||
connect(
|
|
||||||
this,
|
|
||||||
&RegisterPage::versionErrorCb,
|
|
||||||
this,
|
|
||||||
[this](const QString &msg) {
|
|
||||||
error_server_label_->show();
|
|
||||||
server_input_->setValid(false);
|
|
||||||
showError(error_server_label_, msg);
|
|
||||||
},
|
|
||||||
Qt::QueuedConnection);
|
|
||||||
|
|
||||||
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
||||||
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
|
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
|
||||||
|
|
||||||
connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
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_, 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_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||||
connect(
|
connect(password_confirmation_,
|
||||||
password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
&TextField::editingFinished,
|
||||||
|
this,
|
||||||
|
&RegisterPage::checkPasswordConfirmation);
|
||||||
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||||
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
|
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
|
||||||
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(
|
connect(
|
||||||
this,
|
this,
|
||||||
&RegisterPage::registerAuth,
|
&RegisterPage::serverError,
|
||||||
this,
|
this,
|
||||||
[this](const std::string &user,
|
[this](const QString &msg) {
|
||||||
const std::string &pass,
|
server_input_->setValid(false);
|
||||||
const mtx::user_interactive::Auth &auth) {
|
showError(error_server_label_, msg);
|
||||||
http::client()->registration(
|
},
|
||||||
user,
|
Qt::QueuedConnection);
|
||||||
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);
|
|
||||||
|
|
||||||
emit registerOk();
|
connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
|
||||||
return;
|
connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
|
||||||
}
|
connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
|
||||||
|
connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA);
|
||||||
// The server requires registration flows.
|
connect(
|
||||||
if (err->status_code == 401) {
|
this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth);
|
||||||
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_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -345,107 +208,116 @@ RegisterPage::showError(QLabel *label, const QString &msg)
|
|||||||
int height = rect.height();
|
int height = rect.height();
|
||||||
label->setFixedHeight((int)qCeil(width / 200.0) * height);
|
label->setFixedHeight((int)qCeil(width / 200.0) * height);
|
||||||
label->setText(msg);
|
label->setText(msg);
|
||||||
|
label->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
|
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
|
||||||
{
|
{
|
||||||
if (t_field->isValid()) {
|
if (t_field->isValid()) {
|
||||||
label->setText("");
|
|
||||||
label->hide();
|
label->hide();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
label->show();
|
|
||||||
showError(label, msg);
|
showError(label, msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
RegisterPage::checkFields()
|
RegisterPage::checkUsername()
|
||||||
{
|
{
|
||||||
error_label_->setText("");
|
return checkOneField(error_username_label_,
|
||||||
error_username_label_->setText("");
|
|
||||||
error_password_label_->setText("");
|
|
||||||
error_password_confirmation_label_->setText("");
|
|
||||||
error_server_label_->setText("");
|
|
||||||
|
|
||||||
error_username_label_->hide();
|
|
||||||
error_password_label_->hide();
|
|
||||||
error_password_confirmation_label_->hide();
|
|
||||||
error_server_label_->hide();
|
|
||||||
|
|
||||||
password_confirmation_->setValid(true);
|
|
||||||
server_input_->setValid(true);
|
|
||||||
|
|
||||||
bool all_fields_good = true;
|
|
||||||
if (username_input_->isModified() &&
|
|
||||||
!checkOneField(error_username_label_,
|
|
||||||
username_input_,
|
username_input_,
|
||||||
tr("The username must not be empty, and must contain only the "
|
tr("The username must not be empty, and must contain only the "
|
||||||
"characters a-z, 0-9, ., _, =, -, and /."))) {
|
"characters a-z, 0-9, ., _, =, -, and /."));
|
||||||
all_fields_good = false;
|
}
|
||||||
} else if (password_input_->isModified() &&
|
|
||||||
!checkOneField(error_password_label_,
|
bool
|
||||||
password_input_,
|
RegisterPage::checkPassword()
|
||||||
tr("Password is not long enough (min 8 chars)"))) {
|
{
|
||||||
all_fields_good = false;
|
return checkOneField(
|
||||||
} else if (password_confirmation_->isModified() &&
|
error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
|
||||||
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"));
|
showError(error_password_confirmation_label_, tr("Passwords don't match"));
|
||||||
password_confirmation_->setValid(false);
|
password_confirmation_->setValid(false);
|
||||||
all_fields_good = false;
|
return 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;
|
|
||||||
}
|
}
|
||||||
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
|
void
|
||||||
RegisterPage::onRegisterButtonClicked()
|
RegisterPage::onRegisterButtonClicked()
|
||||||
{
|
{
|
||||||
if (!checkFields()) {
|
if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
|
||||||
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();
|
auto server = server_input_->text().toStdString();
|
||||||
|
|
||||||
http::client()->set_server(server);
|
http::client()->set_server(server);
|
||||||
http::client()->verify_certificates(
|
http::client()->verify_certificates(
|
||||||
!UserSettings::instance()->disableCertificateValidation());
|
!UserSettings::instance()->disableCertificateValidation());
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
emit wellKnownLookup();
|
||||||
|
|
||||||
|
emit registering();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RegisterPage::doWellKnownLookup()
|
||||||
|
{
|
||||||
http::client()->well_known(
|
http::client()->well_known(
|
||||||
[this, username, password](const mtx::responses::WellKnown &res,
|
[this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
|
||||||
mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err->status_code == 404) {
|
if (err->status_code == 404) {
|
||||||
nhlog::net()->info("Autodiscovery: No .well-known.");
|
nhlog::net()->info("Autodiscovery: No .well-known.");
|
||||||
checkVersionAndRegister(username, password);
|
// Check that the homeserver can be reached
|
||||||
|
emit versionsCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!err->parse_error.empty()) {
|
if (!err->parse_error.empty()) {
|
||||||
emit versionErrorCb(tr(
|
emit serverError(
|
||||||
"Autodiscovery failed. Received malformed response."));
|
tr("Autodiscovery failed. Received malformed response."));
|
||||||
nhlog::net()->error(
|
nhlog::net()->error(
|
||||||
"Autodiscovery failed. Received malformed response.");
|
"Autodiscovery failed. Received malformed response.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
|
emit serverError(tr("Autodiscovery failed. Unknown error when "
|
||||||
"requesting .well-known."));
|
"requesting .well-known."));
|
||||||
nhlog::net()->error("Autodiscovery failed. Unknown error when "
|
nhlog::net()->error("Autodiscovery failed. Unknown error when "
|
||||||
"requesting .well-known. {} {}",
|
"requesting .well-known. {} {}",
|
||||||
@ -454,48 +326,76 @@ RegisterPage::onRegisterButtonClicked()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::net()->info("Autodiscovery: Discovered '" +
|
nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
|
||||||
res.homeserver.base_url + "'");
|
|
||||||
http::client()->set_server(res.homeserver.base_url);
|
http::client()->set_server(res.homeserver.base_url);
|
||||||
checkVersionAndRegister(username, password);
|
// Check that the homeserver can be reached
|
||||||
|
emit versionsCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
emit registering();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password)
|
RegisterPage::doVersionsCheck()
|
||||||
{
|
{
|
||||||
|
// Make a request to /_matrix/client/versions to check the address
|
||||||
|
// given is a Matrix homeserver.
|
||||||
http::client()->versions(
|
http::client()->versions(
|
||||||
[this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) {
|
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err->status_code == 404) {
|
if (err->status_code == 404) {
|
||||||
emit versionErrorCb(tr("The required endpoints were not found. "
|
emit serverError(
|
||||||
"Possibly not a Matrix server."));
|
tr("The required endpoints were not found. Possibly "
|
||||||
|
"not a Matrix server."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!err->parse_error.empty()) {
|
if (!err->parse_error.empty()) {
|
||||||
emit versionErrorCb(tr("Received malformed response. Make sure "
|
emit serverError(
|
||||||
"the homeserver domain is valid."));
|
tr("Received malformed response. Make sure the homeserver "
|
||||||
|
"domain is valid."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit versionErrorCb(tr(
|
emit serverError(tr("An unknown error occured. Make sure the "
|
||||||
"An unknown error occured. Make sure the homeserver domain is valid."));
|
"homeserver domain is valid."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
http::client()->registration(
|
// Attempt registration without an `auth` dict
|
||||||
username,
|
emit registration();
|
||||||
password,
|
});
|
||||||
[this, username, password](const mtx::responses::Register &res,
|
}
|
||||||
mtx::http::RequestErr err) {
|
|
||||||
|
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) {
|
if (!err) {
|
||||||
http::client()->set_user(res.user_id);
|
http::client()->set_user(res.user_id);
|
||||||
http::client()->set_access_token(res.access_token);
|
http::client()->set_access_token(res.access_token);
|
||||||
|
|
||||||
emit registerOk();
|
emit registerOk();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -503,31 +403,101 @@ RegisterPage::checkVersionAndRegister(const std::string &username, const std::st
|
|||||||
// The server requires registration flows.
|
// The server requires registration flows.
|
||||||
if (err->status_code == 401) {
|
if (err->status_code == 401) {
|
||||||
if (err->matrix_error.unauthorized.flows.empty()) {
|
if (err->matrix_error.unauthorized.flows.empty()) {
|
||||||
nhlog::net()->warn(
|
nhlog::net()->warn("failed to retrieve registration flows: "
|
||||||
"failed to retrieve registration flows1: ({}) "
|
"status_code({}), matrix_error({}) ",
|
||||||
"{}",
|
|
||||||
static_cast<int>(err->status_code),
|
static_cast<int>(err->status_code),
|
||||||
err->matrix_error.error);
|
err->matrix_error.error);
|
||||||
emit errorOccurred();
|
showError(QString::fromStdString(err->matrix_error.error));
|
||||||
emit registerErrorCb(
|
|
||||||
QString::fromStdString(err->matrix_error.error));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit registrationFlow(
|
// Attempt to complete a UIA stage
|
||||||
username, password, err->matrix_error.unauthorized);
|
emit UIA(err->matrix_error.unauthorized);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::net()->error(
|
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
|
||||||
"failed to register: status_code ({}), matrix_error({})",
|
|
||||||
static_cast<int>(err->status_code),
|
static_cast<int>(err->status_code),
|
||||||
err->matrix_error.error);
|
err->matrix_error.error);
|
||||||
|
|
||||||
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
|
showError(QString::fromStdString(err->matrix_error.error));
|
||||||
emit errorOccurred();
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
void
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <mtx/user_interactive.hpp>
|
#include <mtx/user_interactive.hpp>
|
||||||
|
#include <mtxclient/http/client.hpp>
|
||||||
|
|
||||||
class FlatButton;
|
class FlatButton;
|
||||||
class RaisedButton;
|
class RaisedButton;
|
||||||
@ -33,17 +34,16 @@ signals:
|
|||||||
void errorOccurred();
|
void errorOccurred();
|
||||||
|
|
||||||
//! Used to trigger the corresponding slot outside of the main thread.
|
//! 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 registering();
|
||||||
void registerOk();
|
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:
|
private slots:
|
||||||
void onBackButtonClicked();
|
void onBackButtonClicked();
|
||||||
@ -51,12 +51,22 @@ private slots:
|
|||||||
|
|
||||||
// function for showing different errors
|
// function for showing different errors
|
||||||
void showError(const QString &msg);
|
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:
|
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_;
|
QVBoxLayout *top_layout_;
|
||||||
|
|
||||||
QHBoxLayout *back_layout_;
|
QHBoxLayout *back_layout_;
|
||||||
@ -69,6 +79,7 @@ private:
|
|||||||
QLabel *error_password_label_;
|
QLabel *error_password_label_;
|
||||||
QLabel *error_password_confirmation_label_;
|
QLabel *error_password_confirmation_label_;
|
||||||
QLabel *error_server_label_;
|
QLabel *error_server_label_;
|
||||||
|
QLabel *error_registration_token_label_;
|
||||||
|
|
||||||
FlatButton *back_button_;
|
FlatButton *back_button_;
|
||||||
RaisedButton *register_button_;
|
RaisedButton *register_button_;
|
||||||
@ -81,4 +92,5 @@ private:
|
|||||||
TextField *password_input_;
|
TextField *password_input_;
|
||||||
TextField *password_confirmation_;
|
TextField *password_confirmation_;
|
||||||
TextField *server_input_;
|
TextField *server_input_;
|
||||||
|
TextField *registration_token_input_;
|
||||||
};
|
};
|
||||||
|
@ -4,20 +4,35 @@
|
|||||||
|
|
||||||
#include "SingleImagePackModel.h"
|
#include "SingleImagePackModel.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
|
#include "Logging.h"
|
||||||
#include "MatrixClient.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)
|
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, roomid_(std::move(pack_.source_room))
|
, roomid_(std::move(pack_.source_room))
|
||||||
, statekey_(std::move(pack_.state_key))
|
, statekey_(std::move(pack_.state_key))
|
||||||
|
, old_statekey_(statekey_)
|
||||||
, pack(std::move(pack_.pack))
|
, pack(std::move(pack_.pack))
|
||||||
{
|
{
|
||||||
|
[[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
|
||||||
|
|
||||||
if (!pack.pack)
|
if (!pack.pack)
|
||||||
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
|
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
|
||||||
|
|
||||||
for (const auto &e : pack.images)
|
for (const auto &e : pack.images)
|
||||||
shortcodes.push_back(e.first);
|
shortcodes.push_back(e.first);
|
||||||
|
|
||||||
|
connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
@ -61,6 +76,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
|
|||||||
return {};
|
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
|
bool
|
||||||
SingleImagePackModel::isGloballyEnabled() const
|
SingleImagePackModel::isGloballyEnabled() const
|
||||||
{
|
{
|
||||||
@ -98,3 +180,171 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
|
|||||||
// emit this->globallyEnabledChanged();
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QList>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include <mtx/events/mscs/image_packs.hpp>
|
#include <mtx/events/mscs/image_packs.hpp>
|
||||||
|
|
||||||
@ -15,14 +17,18 @@ class SingleImagePackModel : public QAbstractListModel
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
Q_PROPERTY(QString roomid READ roomid CONSTANT)
|
Q_PROPERTY(QString roomid READ roomid CONSTANT)
|
||||||
Q_PROPERTY(QString statekey READ statekey CONSTANT)
|
Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
|
||||||
Q_PROPERTY(QString attribution READ statekey CONSTANT)
|
Q_PROPERTY(
|
||||||
Q_PROPERTY(QString packname READ packname CONSTANT)
|
QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
|
||||||
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
|
Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
|
||||||
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
|
Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
|
||||||
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
|
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
|
Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
|
||||||
globallyEnabledChanged)
|
globallyEnabledChanged)
|
||||||
|
Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Roles
|
enum Roles
|
||||||
{
|
{
|
||||||
@ -32,11 +38,15 @@ public:
|
|||||||
IsEmote,
|
IsEmote,
|
||||||
IsSticker,
|
IsSticker,
|
||||||
};
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
|
||||||
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
|
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role) 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 roomid() const { return QString::fromStdString(roomid_); }
|
||||||
QString statekey() const { return QString::fromStdString(statekey_); }
|
QString statekey() const { return QString::fromStdString(statekey_); }
|
||||||
@ -47,14 +57,36 @@ public:
|
|||||||
bool isEmotePack() const { return pack.pack->is_emoji(); }
|
bool isEmotePack() const { return pack.pack->is_emoji(); }
|
||||||
|
|
||||||
bool isGloballyEnabled() const;
|
bool isGloballyEnabled() const;
|
||||||
|
bool canEdit() const;
|
||||||
void setGloballyEnabled(bool enabled);
|
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:
|
signals:
|
||||||
void globallyEnabledChanged();
|
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:
|
private:
|
||||||
std::string roomid_;
|
std::string roomid_;
|
||||||
std::string statekey_;
|
std::string statekey_, old_statekey_;
|
||||||
|
|
||||||
mtx::events::msc2545::ImagePack pack;
|
mtx::events::msc2545::ImagePack pack;
|
||||||
std::vector<std::string> shortcodes;
|
std::vector<std::string> shortcodes;
|
||||||
|
@ -90,8 +90,6 @@ UserSettings::load(std::optional<QString> profile)
|
|||||||
decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool();
|
decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool();
|
||||||
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
|
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
|
||||||
privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
|
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();
|
mobileMode_ = settings.value("user/mobile_mode", false).toBool();
|
||||||
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
|
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
|
||||||
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
|
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
|
||||||
@ -123,6 +121,12 @@ UserSettings::load(std::optional<QString> profile)
|
|||||||
userId_ = settings.value(prefix + "auth/user_id", "").toString();
|
userId_ = settings.value(prefix + "auth/user_id", "").toString();
|
||||||
deviceId_ = settings.value(prefix + "auth/device_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_ =
|
disableCertificateValidation_ =
|
||||||
settings.value("disable_certificate_validation", false).toBool();
|
settings.value("disable_certificate_validation", false).toBool();
|
||||||
|
|
||||||
@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer)
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
|
||||||
|
{
|
||||||
|
if (shareKeys == onlyShareKeysWithVerifiedUsers_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
onlyShareKeysWithVerifiedUsers_ = shareKeys;
|
||||||
|
emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
|
UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
|
||||||
{
|
{
|
||||||
@ -610,8 +625,6 @@ UserSettings::save()
|
|||||||
settings.setValue("decrypt_sidebar", decryptSidebar_);
|
settings.setValue("decrypt_sidebar", decryptSidebar_);
|
||||||
settings.setValue("privacy_screen", privacyScreen_);
|
settings.setValue("privacy_screen", privacyScreen_);
|
||||||
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
|
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
|
||||||
settings.setValue("automatically_share_keys_with_trusted_users",
|
|
||||||
shareKeysWithTrustedUsers_);
|
|
||||||
settings.setValue("mobile_mode", mobileMode_);
|
settings.setValue("mobile_mode", mobileMode_);
|
||||||
settings.setValue("font_size", baseFontSize_);
|
settings.setValue("font_size", baseFontSize_);
|
||||||
settings.setValue("typing_notifications", typingNotifications_);
|
settings.setValue("typing_notifications", typingNotifications_);
|
||||||
@ -650,6 +663,11 @@ UserSettings::save()
|
|||||||
settings.setValue(prefix + "auth/user_id", userId_);
|
settings.setValue(prefix + "auth/user_id", userId_);
|
||||||
settings.setValue(prefix + "auth/device_id", deviceId_);
|
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.setValue("disable_certificate_validation", disableCertificateValidation_);
|
||||||
|
|
||||||
settings.sync();
|
settings.sync();
|
||||||
@ -708,6 +726,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||||||
avatarCircles_ = new Toggle{this};
|
avatarCircles_ = new Toggle{this};
|
||||||
decryptSidebar_ = new Toggle(this);
|
decryptSidebar_ = new Toggle(this);
|
||||||
privacyScreen_ = new Toggle{this};
|
privacyScreen_ = new Toggle{this};
|
||||||
|
onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
|
||||||
shareKeysWithTrustedUsers_ = new Toggle(this);
|
shareKeysWithTrustedUsers_ = new Toggle(this);
|
||||||
groupViewToggle_ = new Toggle{this};
|
groupViewToggle_ = new Toggle{this};
|
||||||
timelineButtonsToggle_ = new Toggle{this};
|
timelineButtonsToggle_ = new Toggle{this};
|
||||||
@ -738,6 +757,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||||||
avatarCircles_->setChecked(settings_->avatarCircles());
|
avatarCircles_->setChecked(settings_->avatarCircles());
|
||||||
decryptSidebar_->setChecked(settings_->decryptSidebar());
|
decryptSidebar_->setChecked(settings_->decryptSidebar());
|
||||||
privacyScreen_->setChecked(settings_->privacyScreen());
|
privacyScreen_->setChecked(settings_->privacyScreen());
|
||||||
|
onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
|
||||||
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
|
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
|
||||||
groupViewToggle_->setChecked(settings_->groupView());
|
groupViewToggle_->setChecked(settings_->groupView());
|
||||||
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
|
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
|
||||||
@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||||||
formLayout_->addRow(new HorizontalLine{this});
|
formLayout_->addRow(new HorizontalLine{this});
|
||||||
boxWrap(tr("Device ID"), deviceIdValue_);
|
boxWrap(tr("Device ID"), deviceIdValue_);
|
||||||
boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
|
boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
|
||||||
boxWrap(
|
boxWrap(tr("Send encrypted messages to verified users only"),
|
||||||
tr("Share keys with verified users and devices"),
|
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_,
|
shareKeysWithTrustedUsers_,
|
||||||
tr("Automatically replies to key requests from other users, if they are verified."));
|
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(new HorizontalLine{this});
|
||||||
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
|
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
|
||||||
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
|
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) {
|
connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
|
||||||
settings_->setShareKeysWithTrustedUsers(enabled);
|
settings_->setShareKeysWithTrustedUsers(enabled);
|
||||||
});
|
});
|
||||||
@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *)
|
|||||||
groupViewToggle_->setState(settings_->groupView());
|
groupViewToggle_->setState(settings_->groupView());
|
||||||
decryptSidebar_->setState(settings_->decryptSidebar());
|
decryptSidebar_->setState(settings_->decryptSidebar());
|
||||||
privacyScreen_->setState(settings_->privacyScreen());
|
privacyScreen_->setState(settings_->privacyScreen());
|
||||||
|
onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
|
||||||
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
|
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
|
||||||
avatarCircles_->setState(settings_->avatarCircles());
|
avatarCircles_->setState(settings_->avatarCircles());
|
||||||
typingNotifications_->setState(settings_->typingNotifications());
|
typingNotifications_->setState(settings_->typingNotifications());
|
||||||
|
@ -88,6 +88,8 @@ class UserSettings : public QObject
|
|||||||
setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
|
setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
|
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
|
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
|
||||||
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
|
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
|
||||||
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
|
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
|
||||||
@ -152,6 +154,7 @@ public:
|
|||||||
void setScreenShareRemoteVideo(bool state);
|
void setScreenShareRemoteVideo(bool state);
|
||||||
void setScreenShareHideCursor(bool state);
|
void setScreenShareHideCursor(bool state);
|
||||||
void setUseStunServer(bool state);
|
void setUseStunServer(bool state);
|
||||||
|
void setOnlyShareKeysWithVerifiedUsers(bool state);
|
||||||
void setShareKeysWithTrustedUsers(bool state);
|
void setShareKeysWithTrustedUsers(bool state);
|
||||||
void setProfile(QString profile);
|
void setProfile(QString profile);
|
||||||
void setUserId(QString userId);
|
void setUserId(QString userId);
|
||||||
@ -208,6 +211,7 @@ public:
|
|||||||
bool screenShareHideCursor() const { return screenShareHideCursor_; }
|
bool screenShareHideCursor() const { return screenShareHideCursor_; }
|
||||||
bool useStunServer() const { return useStunServer_; }
|
bool useStunServer() const { return useStunServer_; }
|
||||||
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
|
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
|
||||||
|
bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
|
||||||
QString profile() const { return profile_; }
|
QString profile() const { return profile_; }
|
||||||
QString userId() const { return userId_; }
|
QString userId() const { return userId_; }
|
||||||
QString accessToken() const { return accessToken_; }
|
QString accessToken() const { return accessToken_; }
|
||||||
@ -252,6 +256,7 @@ signals:
|
|||||||
void screenShareRemoteVideoChanged(bool state);
|
void screenShareRemoteVideoChanged(bool state);
|
||||||
void screenShareHideCursorChanged(bool state);
|
void screenShareHideCursorChanged(bool state);
|
||||||
void useStunServerChanged(bool state);
|
void useStunServerChanged(bool state);
|
||||||
|
void onlyShareKeysWithVerifiedUsersChanged(bool state);
|
||||||
void shareKeysWithTrustedUsersChanged(bool state);
|
void shareKeysWithTrustedUsersChanged(bool state);
|
||||||
void profileChanged(QString profile);
|
void profileChanged(QString profile);
|
||||||
void userIdChanged(QString userId);
|
void userIdChanged(QString userId);
|
||||||
@ -284,6 +289,7 @@ private:
|
|||||||
bool privacyScreen_;
|
bool privacyScreen_;
|
||||||
int privacyScreenTimeout_;
|
int privacyScreenTimeout_;
|
||||||
bool shareKeysWithTrustedUsers_;
|
bool shareKeysWithTrustedUsers_;
|
||||||
|
bool onlyShareKeysWithVerifiedUsers_;
|
||||||
bool mobileMode_;
|
bool mobileMode_;
|
||||||
int timelineMaxWidth_;
|
int timelineMaxWidth_;
|
||||||
int roomListWidth_;
|
int roomListWidth_;
|
||||||
@ -372,6 +378,7 @@ private:
|
|||||||
Toggle *privacyScreen_;
|
Toggle *privacyScreen_;
|
||||||
QSpinBox *privacyScreenTimeout_;
|
QSpinBox *privacyScreenTimeout_;
|
||||||
Toggle *shareKeysWithTrustedUsers_;
|
Toggle *shareKeysWithTrustedUsers_;
|
||||||
|
Toggle *onlyShareKeysWithVerifiedUsers_;
|
||||||
Toggle *mobileMode_;
|
Toggle *mobileMode_;
|
||||||
QLabel *deviceFingerprintValue_;
|
QLabel *deviceFingerprintValue_;
|
||||||
QLabel *deviceIdValue_;
|
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)
|
Q_DECLARE_METATYPE(Reaction)
|
||||||
|
|
||||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
|
QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
|
||||||
1000};
|
|
||||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
|
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
|
||||||
1000};
|
1000};
|
||||||
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{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>) {
|
mtx::events::msg::Encrypted>) {
|
||||||
auto event =
|
auto event =
|
||||||
decryptEvent({room_id_, e.event_id}, e);
|
decryptEvent({room_id_, e.event_id}, e);
|
||||||
if (auto dec =
|
if (event->event) {
|
||||||
std::get_if<mtx::events::RoomEvent<
|
if (auto dec = std::get_if<
|
||||||
|
mtx::events::RoomEvent<
|
||||||
mtx::events::msg::
|
mtx::events::msg::
|
||||||
KeyVerificationRequest>>(event)) {
|
KeyVerificationRequest>>(
|
||||||
|
&event->event.value())) {
|
||||||
emit updateFlowEventId(
|
emit updateFlowEventId(
|
||||||
event_id.event_id.to_string());
|
event_id.event_id
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
|
|||||||
if (auto encrypted =
|
if (auto encrypted =
|
||||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
&event)) {
|
&event)) {
|
||||||
mtx::events::collections::TimelineEvents *d_event =
|
auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||||
decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
if (d_event->event &&
|
||||||
if (std::visit(
|
std::visit(
|
||||||
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
|
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
|
||||||
*d_event)) {
|
*d_event->event)) {
|
||||||
handle_room_verification(*d_event);
|
handle_room_verification(*d_event->event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
|
|||||||
events_.insert(index, event_ptr);
|
events_.insert(index, event_ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decrypt)
|
if (decrypt) {
|
||||||
if (auto encrypted =
|
if (auto encrypted =
|
||||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
event_ptr))
|
event_ptr)) {
|
||||||
return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||||
|
if (decrypted->event)
|
||||||
|
return &*decrypted->event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return event_ptr;
|
return event_ptr;
|
||||||
}
|
}
|
||||||
@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
|
|||||||
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
|
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
mtx::events::collections::TimelineEvents *
|
olm::DecryptionResult *
|
||||||
EventStore::decryptEvent(const IdIndex &idx,
|
EventStore::decryptEvent(const IdIndex &idx,
|
||||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
|
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.session_id = e.content.session_id;
|
||||||
index.sender_key = e.content.sender_key;
|
index.sender_key = e.content.sender_key;
|
||||||
|
|
||||||
auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
|
auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
|
||||||
auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
|
auto event_ptr = new olm::DecryptionResult(std::move(event));
|
||||||
decryptedEvents_.insert(idx, event_ptr);
|
decryptedEvents_.insert(idx, event_ptr);
|
||||||
return event_ptr;
|
return event_ptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto decryptionResult = olm::decryptEvent(index, e);
|
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) {
|
if (decryptionResult.error) {
|
||||||
switch (*decryptionResult.error) {
|
switch (decryptionResult.error) {
|
||||||
case olm::DecryptionErrorCode::MissingSession:
|
case olm::DecryptionErrorCode::MissingSession:
|
||||||
case olm::DecryptionErrorCode::MissingSessionIndex: {
|
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 ({}, {}, {})",
|
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
|
||||||
index.room_id,
|
index.room_id,
|
||||||
index.session_id,
|
index.session_id,
|
||||||
e.sender);
|
e.sender);
|
||||||
// we may not want to request keys during initial sync and such
|
|
||||||
if (suppressKeyRequests)
|
requestSession(e, false);
|
||||||
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;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case olm::DecryptionErrorCode::DbError:
|
case olm::DecryptionErrorCode::DbError:
|
||||||
@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||||||
index.session_id,
|
index.session_id,
|
||||||
index.sender_key,
|
index.sender_key,
|
||||||
decryptionResult.error_message.value_or(""));
|
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;
|
break;
|
||||||
case olm::DecryptionErrorCode::DecryptionFailed:
|
case olm::DecryptionErrorCode::DecryptionFailed:
|
||||||
nhlog::crypto()->critical(
|
nhlog::crypto()->critical(
|
||||||
@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||||||
index.session_id,
|
index.session_id,
|
||||||
index.sender_key,
|
index.sender_key,
|
||||||
decryptionResult.error_message.value_or(""));
|
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;
|
break;
|
||||||
case olm::DecryptionErrorCode::ParsingFailed:
|
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;
|
break;
|
||||||
case olm::DecryptionErrorCode::ReplayAttack:
|
case olm::DecryptionErrorCode::ReplayAttack:
|
||||||
nhlog::crypto()->critical(
|
nhlog::crypto()->critical(
|
||||||
@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||||||
e.event_id,
|
e.event_id,
|
||||||
room_id_,
|
room_id_,
|
||||||
index.sender_key);
|
index.sender_key);
|
||||||
dummy.content.body =
|
|
||||||
tr("-- Replay attack! This message index was reused! --").toStdString();
|
|
||||||
break;
|
break;
|
||||||
case olm::DecryptionErrorCode::UnknownFingerprint:
|
case olm::DecryptionErrorCode::NoError:
|
||||||
// TODO: don't fail, just show in UI.
|
// unreachable
|
||||||
nhlog::crypto()->critical("Message by unverified fingerprint {}",
|
|
||||||
index.sender_key);
|
|
||||||
dummy.content.body =
|
|
||||||
tr("-- Message by unverified device! --").toStdString();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return asCacheEntry(std::move(dummy));
|
return asCacheEntry(std::move(decryptionResult));
|
||||||
}
|
|
||||||
|
|
||||||
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]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
||||||
if (encInfo)
|
if (encInfo)
|
||||||
emit newEncryptedImage(encInfo.value());
|
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
|
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);
|
events_by_id_.insert(index, event_ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decrypt)
|
if (decrypt) {
|
||||||
if (auto encrypted =
|
if (auto encrypted =
|
||||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
event_ptr))
|
event_ptr)) {
|
||||||
return decryptEvent(index, *encrypted);
|
auto decrypted = decryptEvent(index, *encrypted);
|
||||||
|
if (decrypted->event)
|
||||||
|
return &*decrypted->event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return event_ptr;
|
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
|
void
|
||||||
EventStore::fetchMore()
|
EventStore::fetchMore()
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
#include <mtx/responses/messages.hpp>
|
#include <mtx/responses/messages.hpp>
|
||||||
#include <mtx/responses/sync.hpp>
|
#include <mtx/responses/sync.hpp>
|
||||||
|
|
||||||
|
#include "Olm.h"
|
||||||
#include "Reaction.h"
|
#include "Reaction.h"
|
||||||
|
|
||||||
class EventStore : public QObject
|
class EventStore : public QObject
|
||||||
@ -78,6 +79,9 @@ public:
|
|||||||
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
|
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
|
||||||
|
|
||||||
QVariantList reactions(const std::string &event_id);
|
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
|
int size() const
|
||||||
{
|
{
|
||||||
@ -119,7 +123,7 @@ public slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
|
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
|
||||||
mtx::events::collections::TimelineEvents *decryptEvent(
|
olm::DecryptionResult *decryptEvent(
|
||||||
const IdIndex &idx,
|
const IdIndex &idx,
|
||||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
||||||
void handle_room_verification(mtx::events::collections::TimelineEvents event);
|
void handle_room_verification(mtx::events::collections::TimelineEvents event);
|
||||||
@ -129,7 +133,7 @@ private:
|
|||||||
uint64_t first = std::numeric_limits<uint64_t>::max(),
|
uint64_t first = std::numeric_limits<uint64_t>::max(),
|
||||||
last = 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<Index, mtx::events::collections::TimelineEvents> events_;
|
||||||
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
|
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
|
||||||
|
|
||||||
@ -137,6 +141,7 @@ private:
|
|||||||
{
|
{
|
||||||
std::string request_id;
|
std::string request_id;
|
||||||
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
|
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
|
||||||
|
qint64 requested_at;
|
||||||
};
|
};
|
||||||
std::map<std::string, PendingKeyRequests> pending_key_requests;
|
std::map<std::string, PendingKeyRequests> pending_key_requests;
|
||||||
|
|
||||||
|
@ -533,6 +533,8 @@ RoomlistModel::initializeRooms()
|
|||||||
for (const auto &id : cache::client()->roomIds())
|
for (const auto &id : cache::client()->roomIds())
|
||||||
addRoom(id, true);
|
addRoom(id, true);
|
||||||
|
|
||||||
|
nhlog::db()->info("Restored {} rooms from cache", rowCount());
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,9 +28,9 @@
|
|||||||
#include "MemberList.h"
|
#include "MemberList.h"
|
||||||
#include "MxcImageProvider.h"
|
#include "MxcImageProvider.h"
|
||||||
#include "Olm.h"
|
#include "Olm.h"
|
||||||
|
#include "ReadReceiptsModel.h"
|
||||||
#include "TimelineViewManager.h"
|
#include "TimelineViewManager.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "dialogs/RawMessage.h"
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(QModelIndex)
|
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::KeyVerificationDone:
|
||||||
case qml_mtx_events::KeyVerificationReady:
|
case qml_mtx_events::KeyVerificationReady:
|
||||||
return mtx::events::EventType::RoomMessage;
|
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:
|
default:
|
||||||
return mtx::events::EventType::Unsupported;
|
return mtx::events::EventType::Unsupported;
|
||||||
};
|
};
|
||||||
@ -443,6 +452,7 @@ TimelineModel::roleNames() const
|
|||||||
{IsEditable, "isEditable"},
|
{IsEditable, "isEditable"},
|
||||||
{IsEncrypted, "isEncrypted"},
|
{IsEncrypted, "isEncrypted"},
|
||||||
{Trustlevel, "trustlevel"},
|
{Trustlevel, "trustlevel"},
|
||||||
|
{EncryptionError, "encryptionError"},
|
||||||
{ReplyTo, "replyTo"},
|
{ReplyTo, "replyTo"},
|
||||||
{Reactions, "reactions"},
|
{Reactions, "reactions"},
|
||||||
{RoomId, "roomId"},
|
{RoomId, "roomId"},
|
||||||
@ -630,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||||||
return crypto::Trust::Unverified;
|
return crypto::Trust::Unverified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case EncryptionError:
|
||||||
|
return events.decryptionError(event_id(event));
|
||||||
|
|
||||||
case ReplyTo:
|
case ReplyTo:
|
||||||
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
|
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
|
||||||
case Reactions: {
|
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[RoomName], data(event, static_cast<int>(RoomName)));
|
||||||
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
|
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
|
||||||
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
|
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
|
||||||
|
m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
|
||||||
|
|
||||||
return QVariant(m);
|
return QVariant(m);
|
||||||
}
|
}
|
||||||
@ -1025,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::viewRawMessage(QString id) const
|
TimelineModel::viewRawMessage(QString id)
|
||||||
{
|
{
|
||||||
auto e = events.get(id.toStdString(), "", false);
|
auto e = events.get(id.toStdString(), "", false);
|
||||||
if (!e)
|
if (!e)
|
||||||
return;
|
return;
|
||||||
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
||||||
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
|
emit showRawMessageDialog(QString::fromStdString(ev));
|
||||||
Q_UNUSED(dialog);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -1046,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::viewDecryptedRawMessage(QString id) const
|
TimelineModel::viewDecryptedRawMessage(QString id)
|
||||||
{
|
{
|
||||||
auto e = events.get(id.toStdString(), "");
|
auto e = events.get(id.toStdString(), "");
|
||||||
if (!e)
|
if (!e)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
|
||||||
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
|
emit showRawMessageDialog(QString::fromStdString(ev));
|
||||||
Q_UNUSED(dialog);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -1089,9 +1101,9 @@ TimelineModel::relatedInfo(QString id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::readReceiptsAction(QString id) const
|
TimelineModel::showReadReceipts(QString id)
|
||||||
{
|
{
|
||||||
MainWindow::instance()->openReadReceiptsDialog(id);
|
emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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
|
void
|
||||||
TimelineModel::copyLinkToEvent(QString eventId) const
|
TimelineModel::copyLinkToEvent(QString eventId) const
|
||||||
{
|
{
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#include "InviteesModel.h"
|
#include "InviteesModel.h"
|
||||||
#include "MemberList.h"
|
#include "MemberList.h"
|
||||||
#include "Permissions.h"
|
#include "Permissions.h"
|
||||||
|
#include "ReadReceiptsModel.h"
|
||||||
#include "ui/RoomSettings.h"
|
#include "ui/RoomSettings.h"
|
||||||
#include "ui/UserProfile.h"
|
#include "ui/UserProfile.h"
|
||||||
|
|
||||||
@ -106,7 +107,13 @@ enum EventType
|
|||||||
KeyVerificationCancel,
|
KeyVerificationCancel,
|
||||||
KeyVerificationKey,
|
KeyVerificationKey,
|
||||||
KeyVerificationDone,
|
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)
|
Q_ENUM_NS(EventType)
|
||||||
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
|
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
|
||||||
@ -205,6 +212,7 @@ public:
|
|||||||
IsEditable,
|
IsEditable,
|
||||||
IsEncrypted,
|
IsEncrypted,
|
||||||
Trustlevel,
|
Trustlevel,
|
||||||
|
EncryptionError,
|
||||||
ReplyTo,
|
ReplyTo,
|
||||||
Reactions,
|
Reactions,
|
||||||
RoomId,
|
RoomId,
|
||||||
@ -235,13 +243,13 @@ public:
|
|||||||
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
|
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
|
||||||
Q_INVOKABLE QString formatPowerLevelEvent(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 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 openUserProfile(QString userid);
|
||||||
Q_INVOKABLE void editAction(QString id);
|
Q_INVOKABLE void editAction(QString id);
|
||||||
Q_INVOKABLE void replyAction(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 void redactEvent(QString id);
|
||||||
Q_INVOKABLE int idToIndex(QString id) const;
|
Q_INVOKABLE int idToIndex(QString id) const;
|
||||||
Q_INVOKABLE QString indexToId(int index) const;
|
Q_INVOKABLE QString indexToId(int index) const;
|
||||||
@ -257,6 +265,8 @@ public:
|
|||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Q_INVOKABLE void requestKeyForEvent(QString id);
|
||||||
|
|
||||||
std::vector<::Reaction> reactions(const std::string &event_id)
|
std::vector<::Reaction> reactions(const std::string &event_id)
|
||||||
{
|
{
|
||||||
auto list = events.reactions(event_id);
|
auto list = events.reactions(event_id);
|
||||||
@ -348,6 +358,8 @@ signals:
|
|||||||
void typingUsersChanged(std::vector<QString> users);
|
void typingUsersChanged(std::vector<QString> users);
|
||||||
void replyChanged(QString reply);
|
void replyChanged(QString reply);
|
||||||
void editChanged(QString reply);
|
void editChanged(QString reply);
|
||||||
|
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
|
||||||
|
void showRawMessageDialog(QString rawMessage);
|
||||||
void paginationInProgressChanged(const bool);
|
void paginationInProgressChanged(const bool);
|
||||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||||
void scrollToIndex(int index);
|
void scrollToIndex(int index);
|
||||||
|
@ -161,6 +161,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||||||
0,
|
0,
|
||||||
"MtxEvent",
|
"MtxEvent",
|
||||||
"Can't instantiate enum!");
|
"Can't instantiate enum!");
|
||||||
|
qmlRegisterUncreatableMetaObject(
|
||||||
|
olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
|
||||||
qmlRegisterUncreatableMetaObject(
|
qmlRegisterUncreatableMetaObject(
|
||||||
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
|
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
|
||||||
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
|
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
|
||||||
@ -210,6 +212,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||||||
0,
|
0,
|
||||||
"InviteesModel",
|
"InviteesModel",
|
||||||
"InviteesModel needs to be instantiated on the C++ side");
|
"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;
|
static auto self = this;
|
||||||
qmlRegisterSingletonType<MainWindow>(
|
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 <QDesktopServices>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const
|
|||||||
MainWindow::instance()->openJoinRoomDialog(
|
MainWindow::instance()->openJoinRoomDialog(
|
||||||
[](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
|
[](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
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFontDatabase>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
|
||||||
#include "Theme.h"
|
#include "Theme.h"
|
||||||
#include "UserProfile.h"
|
#include "UserProfile.h"
|
||||||
|
|
||||||
|
class QWindow;
|
||||||
|
|
||||||
class Nheko : public QObject
|
class Nheko : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -38,12 +41,17 @@ public:
|
|||||||
int paddingLarge() const { return 20; }
|
int paddingLarge() const { return 20; }
|
||||||
UserProfile *currentUser() const;
|
UserProfile *currentUser() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE QFont monospaceFont() const
|
||||||
|
{
|
||||||
|
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||||
|
}
|
||||||
Q_INVOKABLE void openLink(QString link) const;
|
Q_INVOKABLE void openLink(QString link) const;
|
||||||
Q_INVOKABLE void setStatusMessage(QString msg) const;
|
Q_INVOKABLE void setStatusMessage(QString msg) const;
|
||||||
Q_INVOKABLE void showUserSettingsPage() const;
|
Q_INVOKABLE void showUserSettingsPage() const;
|
||||||
Q_INVOKABLE void openLogoutDialog() const;
|
Q_INVOKABLE void openLogoutDialog() const;
|
||||||
Q_INVOKABLE void openCreateRoomDialog() const;
|
Q_INVOKABLE void openCreateRoomDialog() const;
|
||||||
Q_INVOKABLE void openJoinRoomDialog() const;
|
Q_INVOKABLE void openJoinRoomDialog() const;
|
||||||
|
Q_INVOKABLE void reparent(QWindow *win) const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updateUserProfile();
|
void updateUserProfile();
|
||||||
|
Loading…
Reference in New Issue
Block a user