Merge branch 'master' into nhekoRoomDirectory

This commit is contained in:
kamathmanu 2021-08-07 21:20:43 +00:00 committed by GitHub
commit 2dfccda73c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2099 additions and 1313 deletions

View File

@ -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

View File

@ -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,7 +569,8 @@ 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
)
# #
# Bundle translations. # Bundle translations.

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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 {

View File

@ -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 {

View 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()
}
}

View 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()
}
}

View File

@ -179,31 +179,38 @@ Component {
} }
] ]
TapHandler { // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that...
margin: -Nheko.paddingSmall Item {
acceptedButtons: Qt.RightButton anchors.fill: parent
onSingleTapped: { anchors.margins: 1
if (!TimelineManager.isInvite)
roomContextMenu.show(roomId, tags);
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: {
if (!TimelineManager.isInvite)
roomContextMenu.show(roomId, tags);
}
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
} }
gesturePolicy: TapHandler.ReleaseWithinBounds
}
TapHandler { TapHandler {
margin: -Nheko.paddingSmall margin: -Nheko.paddingSmall
onSingleTapped: Rooms.setCurrentRoom(roomId) onSingleTapped: Rooms.setCurrentRoom(roomId)
onLongPressed: { onLongPressed: {
if (!isInvite) if (!isInvite)
roomContextMenu.show(roomId, tags); roomContextMenu.show(roomId, tags);
}
} }
}
HoverHandler { HoverHandler {
id: hovered id: hovered
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
margin: -Nheko.paddingSmall
} }
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 {

View File

@ -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

View File

@ -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
} }

View File

@ -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: {

View File

@ -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));
} }

View File

@ -34,7 +34,7 @@ ImageButton {
} }
onClicked: { onClicked: {
if (status == MtxEvent.Read) if (status == MtxEvent.Read)
room.readReceiptsAction(eventId); room.showReadReceipts(eventId);
} }
image: { image: {

View File

@ -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
} }

View File

@ -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
}
} }

View File

@ -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

View 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
}
}
}
}
}

View 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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View 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();
}
}
}
}

View File

@ -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,131 +99,24 @@ 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 if (fromAccountData)
state: "normal" return qsTr("Private pack");
states: [ else if (fromCurrentRoom)
State { return qsTr("Pack from this room");
name: "highlight" else
when: hovered.hovered && !(index == currentPackIndex) return qsTr("Globally enabled pack");
}
PropertyChanges { selectedIndex: currentPackIndex
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 { TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: currentPackIndex = index onSingleTapped: currentPackIndex = index
} }
HoverHandler {
id: hovered
}
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
Avatar {
// In the future we could show an online indicator by setting the userid for the avatar
//userid: Nheko.currentUser.userid
id: avatar
enabled: false
Layout.alignment: Qt.AlignVCenter
height: avatarSize
width: avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: packItem.displayName
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: 100
width: parent.width - avatar.width
Layout.preferredWidth: parent.width - avatar.width
spacing: Nheko.paddingSmall
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
Layout.alignment: Qt.AlignBottom
color: packItem.importantText
elideWidth: textContent.width - Nheko.paddingMedium
fullText: displayName
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
color: packItem.unimportantText
font.pixelSize: fontMetrics.font.pixelSize * 0.9
elideWidth: textContent.width - Nheko.paddingSmall
fullText: {
if (fromAccountData)
return qsTr("Private pack");
else if (fromCurrentRoom)
return qsTr("Pack from this room");
else
return qsTr("Globally enabled pack");
}
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
}
}
} }
} }
@ -205,6 +133,7 @@ ApplicationWindow {
id: packinfo 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 {

View File

@ -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

View File

@ -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>
@ -144,15 +143,21 @@
<file>qml/emoji/StickerPicker.qml</file> <file>qml/emoji/StickerPicker.qml</file>
<file>qml/UserProfile.qml</file> <file>qml/UserProfile.qml</file>
<file>qml/RoomDirectory.qml</file> <file>qml/RoomDirectory.qml</file>
<file>qml/delegates/MessageDelegate.qml</file> <file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file> <file>qml/delegates/TextMessage.qml</file>
<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>

View 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,10 +3559,51 @@ 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) {
members[std::string(user_id)] = auto k = json::parse(keys).get<UserKeyCache>();
json::parse(keys).get<UserKeyCache>(); if (verified_only) {
auto verif = verificationStatus(std::string(user_id));
if (verif.user_verified == crypto::Trust::Verified ||
!verif.verified_devices.empty()) {
auto keyCopy = k;
keyCopy.device_keys.clear();
std::copy_if(
k.device_keys.begin(),
k.device_keys.end(),
std::inserter(keyCopy.device_keys,
keyCopy.device_keys.end()),
[&verif](const auto &key) {
auto curve25519 = key.second.keys.find(
"curve25519:" + key.first);
if (curve25519 == key.second.keys.end())
return false;
if (auto t =
verif.verified_device_keys.find(
curve25519->second);
t ==
verif.verified_device_keys.end() ||
t->second != crypto::Trust::Verified)
return false;
return key.first ==
key.second.device_id &&
std::find(
verif.verified_devices.begin(),
verif.verified_devices.end(),
key.first) !=
verif.verified_devices.end();
});
if (!keyCopy.device_keys.empty())
members[std::string(user_id)] =
std::move(keyCopy);
}
} else {
members[std::string(user_id)] = std::move(k);
}
} else { } else {
members[std::string(user_id)] = {}; if (!verified_only)
members[std::string(user_id)] = {};
} }
} }
cursor.close(); cursor.close();

View File

@ -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_;

View File

@ -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"

View File

@ -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;
}

View File

@ -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;

View File

@ -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
{ {

View File

@ -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,

View File

@ -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}

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -286,11 +286,17 @@ 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);
from_their_device = true;
break; if (c_key == key.keys.end() || e_key == key.keys.end()) {
} nhlog::crypto()->warn(
"Skipping device {} as we have no keys for it.",
device_id);
} else if (c_key->second == msg.sender_key &&
e_key->second == sender_ed25519) {
from_their_device = true;
break;
} }
} }
if (!from_their_device) { if (!from_their_device) {
@ -518,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
auto own_user_id = http::client()->user_id().to_string(); auto 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;
pks[user][device].ed25519 = d.keys.at("ed25519:" + device); auto currentTime = QDateTime::currentSecsSinceEpoch();
pks[user][device].curve25519 = d.keys.at("curve25519:" + device); if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
currentTime) {
claims.one_time_keys[user][device] =
mtx::crypto::SIGNED_CURVE25519;
pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
pks[user][device].curve25519 =
d.keys.at("curve25519:" + device);
rateLimit.insert(QPair(user, device), currentTime);
} else {
nhlog::crypto()->warn("Not creating new session with {}:{} "
"because of rate limit",
user,
device);
}
continue; continue;
} }

View File

@ -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
View 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
View 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

View File

@ -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,191 +208,298 @@ 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(""); username_input_,
error_password_label_->setText(""); tr("The username must not be empty, and must contain only the "
error_password_confirmation_label_->setText(""); "characters a-z, 0-9, ., _, =, -, and /."));
error_server_label_->setText(""); }
error_username_label_->hide(); bool
error_password_label_->hide(); RegisterPage::checkPassword()
error_password_confirmation_label_->hide(); {
error_server_label_->hide(); return checkOneField(
error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
}
password_confirmation_->setValid(true); bool
server_input_->setValid(true); RegisterPage::checkPasswordConfirmation()
{
bool all_fields_good = true; if (password_input_->text() == password_confirmation_->text()) {
if (username_input_->isModified() && error_password_confirmation_label_->hide();
!checkOneField(error_username_label_, password_confirmation_->setValid(true);
username_input_, return true;
tr("The username must not be empty, and must contain only the " } else {
"characters a-z, 0-9, ., _, =, -, and /."))) {
all_fields_good = false;
} else if (password_input_->isModified() &&
!checkOneField(error_password_label_,
password_input_,
tr("Password is not long enough (min 8 chars)"))) {
all_fields_good = false;
} else if (password_confirmation_->isModified() &&
password_input_->text() != password_confirmation_->text()) {
error_password_confirmation_label_->show();
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; bool
} RegisterPage::checkServer()
return all_fields_good; {
// 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_, auto server = server_input_->text().toStdString();
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();
http::client()->set_server(server); http::client()->set_server(server);
http::client()->verify_certificates( http::client()->verify_certificates(
!UserSettings::instance()->disableCertificateValidation()); !UserSettings::instance()->disableCertificateValidation());
http::client()->well_known( // This starts a chain of `emit`s which ends up doing the
[this, username, password](const mtx::responses::WellKnown &res, // registration. Signals are used rather than normal function
mtx::http::RequestErr err) { // calls so that the dialogs used in UIA work correctly.
if (err) { //
if (err->status_code == 404) { // The sequence of events looks something like this:
nhlog::net()->info("Autodiscovery: No .well-known."); //
checkVersionAndRegister(username, password); // dowellKnownLookup
return; // v
} // doVersionsCheck
// v
// doRegistration
// v
// doUIA <-----------------+
// v | More auth required
// doRegistrationWithAuth -+
// | Success
// v
// registering
if (!err->parse_error.empty()) { emit wellKnownLookup();
emit versionErrorCb(tr(
"Autodiscovery failed. Received malformed response."));
nhlog::net()->error(
"Autodiscovery failed. Received malformed response.");
return;
}
emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
"requesting .well-known."));
nhlog::net()->error("Autodiscovery failed. Unknown error when "
"requesting .well-known. {} {}",
err->status_code,
err->error_code);
return;
}
nhlog::net()->info("Autodiscovery: Discovered '" +
res.homeserver.base_url + "'");
http::client()->set_server(res.homeserver.base_url);
checkVersionAndRegister(username, password);
});
emit registering(); emit registering();
} }
} }
void void
RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password) RegisterPage::doWellKnownLookup()
{ {
http::client()->versions( http::client()->well_known(
[this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) { [this](const mtx::responses::WellKnown &res, 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. " nhlog::net()->info("Autodiscovery: No .well-known.");
"Possibly not a Matrix server.")); // Check that the homeserver can be reached
emit versionsCheck();
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("Autodiscovery failed. Received malformed response."));
nhlog::net()->error(
"Autodiscovery failed. Received malformed response.");
return; return;
} }
emit versionErrorCb(tr( emit serverError(tr("Autodiscovery failed. Unknown error when "
"An unknown error occured. Make sure the homeserver domain is valid.")); "requesting .well-known."));
nhlog::net()->error("Autodiscovery failed. Unknown error when "
"requesting .well-known. {} {}",
err->status_code,
err->error_code);
return; return;
} }
http::client()->registration( nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
username, http::client()->set_server(res.homeserver.base_url);
password, // Check that the homeserver can be reached
[this, username, password](const mtx::responses::Register &res, emit versionsCheck();
mtx::http::RequestErr err) {
if (!err) {
http::client()->set_user(res.user_id);
http::client()->set_access_token(res.access_token);
emit registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn(
"failed to retrieve registration flows1: ({}) "
"{}",
static_cast<int>(err->status_code),
err->matrix_error.error);
emit errorOccurred();
emit registerErrorCb(
QString::fromStdString(err->matrix_error.error));
return;
}
emit registrationFlow(
username, password, err->matrix_error.unauthorized);
return;
}
nhlog::net()->error(
"failed to register: status_code ({}), matrix_error({})",
static_cast<int>(err->status_code),
err->matrix_error.error);
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
});
}); });
} }
void
RegisterPage::doVersionsCheck()
{
// Make a request to /_matrix/client/versions to check the address
// given is a Matrix homeserver.
http::client()->versions(
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
emit serverError(
tr("The required endpoints were not found. Possibly "
"not a Matrix server."));
return;
}
if (!err->parse_error.empty()) {
emit serverError(
tr("Received malformed response. Make sure the homeserver "
"domain is valid."));
return;
}
emit serverError(tr("An unknown error occured. Make sure the "
"homeserver domain is valid."));
return;
}
// Attempt registration without an `auth` dict
emit registration();
});
}
void
RegisterPage::doRegistration()
{
// These inputs should still be alright, but check just in case
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
http::client()->registration(username, password, registrationCb());
}
}
void
RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
{
// These inputs should still be alright, but check just in case
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
http::client()->registration(username, password, auth, registrationCb());
}
}
mtx::http::Callback<mtx::responses::Register>
RegisterPage::registrationCb()
{
// Return a function to be used as the callback when an attempt at
// registration is made.
return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
if (!err) {
http::client()->set_user(res.user_id);
http::client()->set_access_token(res.access_token);
emit registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn("failed to retrieve registration flows: "
"status_code({}), matrix_error({}) ",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
return;
}
// Attempt to complete a UIA stage
emit UIA(err->matrix_error.unauthorized);
return;
}
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
};
}
void
RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
{
auto completed_stages = unauthorized.completed;
auto flows = unauthorized.flows;
auto session =
unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session;
nhlog::ui()->info("Completed stages: {}", completed_stages.size());
if (!completed_stages.empty()) {
// Get rid of all flows which don't start with the sequence of
// stages that have already been completed.
flows.erase(
std::remove_if(flows.begin(),
flows.end(),
[completed_stages](auto flow) {
if (completed_stages.size() > flow.stages.size())
return true;
for (size_t f = 0; f < completed_stages.size(); f++)
if (completed_stages[f] != flow.stages[f])
return true;
return false;
}),
flows.end());
}
if (flows.empty()) {
nhlog::ui()->error("No available registration flows!");
showError(tr("No supported registration flows!"));
return;
}
auto current_stage = flows.front().stages.at(completed_stages.size());
if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this);
connect(captchaDialog,
&dialogs::ReCaptcha::confirmation,
this,
[this, session, captchaDialog]() {
captchaDialog->close();
captchaDialog->deleteLater();
doRegistrationWithAuth(mtx::user_interactive::Auth{
session, mtx::user_interactive::auth::Fallback{}});
});
connect(
captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); });
} else if (current_stage == mtx::user_interactive::auth_types::dummy) {
doRegistrationWithAuth(
mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
} else {
// use fallback
auto dialog = new dialogs::FallbackAuth(
QString::fromStdString(current_stage), QString::fromStdString(session), this);
connect(
dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() {
dialog->close();
dialog->deleteLater();
emit registrationWithAuth(mtx::user_interactive::Auth{
session, mtx::user_interactive::auth::Fallback{}});
});
connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred);
dialog->show();
}
}
void void
RegisterPage::paintEvent(QPaintEvent *) RegisterPage::paintEvent(QPaintEvent *)
{ {

View File

@ -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_;
}; };

View File

@ -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();
}

View File

@ -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;

View File

@ -90,13 +90,11 @@ 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_ = mobileMode_ = settings.value("user/mobile_mode", false).toBool();
settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
mobileMode_ = settings.value("user/mobile_mode", false).toBool(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); auto tempPresence = settings.value("user/presence", "").toString().toStdString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
auto tempPresence = settings.value("user/presence", "").toString().toStdString();
auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
if (presenceValue < 0) if (presenceValue < 0)
presenceValue = 0; presenceValue = 0;
presence_ = static_cast<Presence>(presenceValue); presence_ = static_cast<Presence>(presenceValue);
@ -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();
@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
general_->setFont(font); general_->setFont(font);
trayToggle_ = new Toggle{this}; trayToggle_ = new Toggle{this};
startInTrayToggle_ = new Toggle{this}; startInTrayToggle_ = new Toggle{this};
avatarCircles_ = new Toggle{this}; avatarCircles_ = new Toggle{this};
decryptSidebar_ = new Toggle(this); decryptSidebar_ = new Toggle(this);
privacyScreen_ = new Toggle{this}; privacyScreen_ = new Toggle{this};
shareKeysWithTrustedUsers_ = new Toggle(this); onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
groupViewToggle_ = new Toggle{this}; shareKeysWithTrustedUsers_ = new Toggle(this);
timelineButtonsToggle_ = new Toggle{this}; groupViewToggle_ = new Toggle{this};
typingNotifications_ = new Toggle{this}; timelineButtonsToggle_ = new Toggle{this};
messageHoverHighlight_ = new Toggle{this}; typingNotifications_ = new Toggle{this};
enlargeEmojiOnlyMessages_ = new Toggle{this}; messageHoverHighlight_ = new Toggle{this};
sortByImportance_ = new Toggle{this}; enlargeEmojiOnlyMessages_ = new Toggle{this};
readReceipts_ = new Toggle{this}; sortByImportance_ = new Toggle{this};
markdown_ = new Toggle{this}; readReceipts_ = new Toggle{this};
desktopNotifications_ = new Toggle{this}; markdown_ = new Toggle{this};
alertOnNotification_ = new Toggle{this}; desktopNotifications_ = new Toggle{this};
useStunServer_ = new Toggle{this}; alertOnNotification_ = new Toggle{this};
mobileMode_ = new Toggle{this}; useStunServer_ = new Toggle{this};
scaleFactorCombo_ = new QComboBox{this}; mobileMode_ = new Toggle{this};
fontSizeCombo_ = new QComboBox{this}; scaleFactorCombo_ = new QComboBox{this};
fontSelectionCombo_ = new QFontComboBox{this}; fontSizeCombo_ = new QComboBox{this};
emojiFontSelectionCombo_ = new QComboBox{this}; fontSelectionCombo_ = new QFontComboBox{this};
ringtoneCombo_ = new QComboBox{this}; emojiFontSelectionCombo_ = new QComboBox{this};
microphoneCombo_ = new QComboBox{this}; ringtoneCombo_ = new QComboBox{this};
cameraCombo_ = new QComboBox{this}; microphoneCombo_ = new QComboBox{this};
cameraResolutionCombo_ = new QComboBox{this}; cameraCombo_ = new QComboBox{this};
cameraFrameRateCombo_ = new QComboBox{this}; cameraResolutionCombo_ = new QComboBox{this};
timelineMaxWidthSpin_ = new QSpinBox{this}; cameraFrameRateCombo_ = new QComboBox{this};
privacyScreenTimeout_ = new QSpinBox{this}; timelineMaxWidthSpin_ = new QSpinBox{this};
privacyScreenTimeout_ = new QSpinBox{this};
trayToggle_->setChecked(settings_->tray()); trayToggle_->setChecked(settings_->tray());
startInTrayToggle_->setChecked(settings_->startInTray()); startInTrayToggle_->setChecked(settings_->startInTray());
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_,
shareKeysWithTrustedUsers_, tr("Requires a user to be verified to send encrypted messages to them. This "
tr("Automatically replies to key requests from other users, if they are verified.")); "improves safety but makes E2EE more tedious."));
boxWrap(tr("Share keys with verified users and devices"),
shareKeysWithTrustedUsers_,
tr("Automatically replies to key requests from other users, if they are verified, "
"even if that device shouldn't have access to those keys otherwise."));
formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(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());

View File

@ -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_;

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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::msg:: mtx::events::RoomEvent<
KeyVerificationRequest>>(event)) { mtx::events::msg::
emit updateFlowEventId( KeyVerificationRequest>>(
event_id.event_id.to_string()); &event->event.value())) {
emit updateFlowEventId(
event_id.event_id
.to_string());
}
} }
} }
}); });
@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
if (auto encrypted = 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()
{ {

View File

@ -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;

View File

@ -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();
} }

View File

@ -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
{ {

View File

@ -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);

View File

@ -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>(

View File

@ -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;
}
}

View File

@ -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_;
};

View File

@ -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());
}

View File

@ -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();