Page MenuHomePhorge

No OneTemporary

Size
515 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8a5441e..0c4612d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,311 +1,314 @@
function(kazv_add_qml_files)
set(options "")
set(oneValueArgs BASE_DIRECTORY)
set(multiValueArgs SOURCES)
cmake_parse_arguments(PARSE_ARGV 0 kazv_add_qml_files "${options}" "${oneValueArgs}" "${multiValueArgs}")
set(target "${kazv_add_qml_files_UNPARSED_ARGUMENTS}")
set(allSources)
foreach(source ${kazv_add_qml_files_SOURCES})
message(STATUS "Adding source: ${kazv_add_qml_files_BASE_DIRECTORY} / ${source}")
set_source_files_properties(
"${kazv_add_qml_files_BASE_DIRECTORY}/${source}"
PROPERTIES
QT_RESOURCE_ALIAS "${source}"
)
list(APPEND allSources "${kazv_add_qml_files_BASE_DIRECTORY}/${source}")
endforeach()
qt_target_qml_sources(
"${target}"
QML_FILES ${allSources}
)
endfunction()
if(WIN32)
set(KAZV_IS_WINDOWS 1)
else()
set(KAZV_IS_WINDOWS 0)
endif()
configure_file(kazv-version.cpp.in kazv-version.cpp)
configure_file(kazv-platform.hpp.in kazv-platform.hpp)
configure_file(kazv-defs.hpp.in kazv-defs.hpp)
set(kazvqmlmodule_SRCS
qt-job-handler.cpp
qt-job.cpp
${CMAKE_CURRENT_BINARY_DIR}/kazv-version.cpp
+ constants.cpp
matrix-sdk.cpp
+ matrix-session.cpp
+ matrix-session-controller.cpp
sso-login-process.cpp
kazv-session-lock-guard.cpp
matrix-room.cpp
matrix-room-list.cpp
matrix-room-timeline.cpp
matrix-room-pinned-events-timeline.cpp
matrix-room-member.cpp
matrix-room-member-list-model.cpp
matrix-room-state.cpp
matrix-event-reader.cpp
matrix-event-reader-list-model.cpp
matrix-event-reaction-list-model.cpp
matrix-event-reaction.cpp
matrix-event.cpp
matrix-event-list.cpp
matrix-link.cpp
meta-types.cpp
l10n-provider.cpp
qt-rand-adapter.cpp
helper.cpp
matrix-promise.cpp
kazv-util.cpp
matrix-sticker-pack.cpp
matrix-sticker.cpp
matrix-sticker-pack-list.cpp
matrix-sticker-pack-source.cpp
matrix-user-given-attrs-map.cpp
matrix-verification.cpp
matrix-verification-list.cpp
kazv-abstract-list-model.cpp
qt-promise-handler.cpp
device-mgmt/matrix-device.cpp
device-mgmt/matrix-device-list.cpp
kazv-config.cpp
kazv-path-config.cpp
kazv-io-manager.cpp
kazv-io-job.cpp
kazv-file.cpp
upload-job-model.cpp
kazv-markdown.cpp
kazv-clipboard.cpp
db-store.cpp
)
ecm_qt_declare_logging_category(kazvqmlmodule_SRCS
HEADER kazv-log.hpp
IDENTIFIER kazvLog
CATEGORY_NAME moe.kazv.mxc.kazv
)
qt_add_qml_module(kazvqmlmodule
STATIC
URI moe.kazv.mxc.kazv
VERSION 0.0
SOURCES ${kazvqmlmodule_SRCS}
)
target_link_libraries(kazvqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Network
Threads::Threads
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
Qt${QT_MAJOR_VERSION}::Quick
Qt${QT_MAJOR_VERSION}::QuickControls2
Qt${QT_MAJOR_VERSION}::Svg
Qt${QT_MAJOR_VERSION}::Concurrent
Qt${QT_MAJOR_VERSION}::Widgets
Qt${QT_MAJOR_VERSION}::Sql
Qt${QT_MAJOR_VERSION}::HttpServer
KF${KF_MAJOR_VERSION}::ConfigCore KF${KF_MAJOR_VERSION}::KIOCore
KF${KF_MAJOR_VERSION}::Notifications
KF${KF_MAJOR_VERSION}::CoreAddons
QCoro::Core
${CMARK_TARGET_NAME}
)
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/device-mgmt)
qt_add_qml_module(kazvshortcutsqmlmodule
STATIC
URI moe.kazv.mxc.kazvshortcuts
VERSION 0.0
SOURCES
shortcuts/shortcut-util.cpp
)
target_link_libraries(kazvshortcutsqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
)
target_include_directories(kazvshortcutsqmlmodule
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/shortcuts
)
add_library(kazvprivlib INTERFACE)
target_link_libraries(kazvprivlib
INTERFACE
kazvqmlmodule
kazvqmlmoduleplugin
kazvshortcutsqmlmodule
kazvshortcutsqmlmoduleplugin
)
qt_add_qml_module(kazvqmlqmlmodule
STATIC
URI moe.kazv.mxc.kazvqml
VERSION 0.0
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY contents/ui
SOURCES
Main.qml
About.qml
PageManager.qml
LoginPage.qml
LoginFlows.qml
MainPage.qml
TabView.qml
Tab.qml
ClosableScrollablePage.qml
ClosableScrollableOverlay.qml
SelfDestroyableOverlaySheet.qml
RoomListView.qml
RoomListViewItemDelegate.qml
RoomPage.qml
RoomPinnedEventsPage.qml
RoomTimelineView.qml
SendMessageBox.qml
EventView.qml
EventViewWrapper.qml
EventViewCompact.qml
EventSourceView.qml
EventHistoryView.qml
EventReactions.qml
Bubble.qml
MediaFileMenu.qml
KazvIOMenu.qml
ConfirmUploadPopup.qml
StickerPicker.qml
AddStickerPopup.qml
StickerPackNameProvider.qml
ConfirmationOverlay.qml
AcceptRejectNoopOverlay.qml
VerificationsPage.qml
SasCodes.qml
event-types/Simple.qml
event-types/Text.qml
event-types/Emote.qml
event-types/Notice.qml
event-types/State.qml
event-types/TextTemplate.qml
event-types/Image.qml
event-types/File.qml
event-types/Video.qml
event-types/Audio.qml
event-types/MediaBubble.qml
event-types/Redacted.qml
event-types/Reaction.qml
event-types/Fallback.qml
TypingIndicator.qml
EventReadIndicator.qml
SelectableText.qml
ReactToEventPopup.qml
FileHandler.qml
ActionSettingsPage.qml
CreateRoomPage.qml
JoinRoomPage.qml
RoomMemberListView.qml
RoomMemberListViewItemDelegate.qml
UserNameProvider.qml
RoomNameProvider.qml
Completion.qml
AsyncHandler.qml
UploadFileHelper.qml
SettingsPage.qml
settings/ProfileSettings.qml
settings/CacheSettings.qml
settings/SecuritySettings.qml
room-settings/RoomSettingsPage.qml
room-settings/RoomTagHandler.qml
room-settings/RoomMemberListPage.qml
room-settings/RoomInvitePage.qml
room-settings/RoomStickerPacksPage.qml
room-settings/RoomStickerPackItemDelegate.qml
room-settings/RoomStatePage.qml
device-mgmt/Device.qml
device-mgmt/DeviceList.qml
UserPage.qml
shortcuts/ActionCollection.qml
shortcuts/ActionItem.qml
shortcuts/ActionSettings.qml
shortcuts/ShortcutInput.qml
MessageNotification.qml
Notifier.qml
NotificationAction.qml
AvatarAdapter.qml
ImageAdapter.qml
FileDialogAdapter.qml
FolderDialogAdapter.qml
ScrollablePageAdapter.qml
emoji/Activities.qml
emoji/Flags.qml
emoji/Objects.qml
emoji/SmileysAndEmotion.qml
emoji/TravelAndPlaces.qml
emoji/AnimalsAndNature.qml
emoji/FoodAndDrink.qml
emoji/PeopleAndBody.qml
emoji/Symbols.qml
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js
SOURCES
l10n.js
global-this.js
matrix-helpers.js
sas-emoji-defs.js
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js/transformed-libs
SOURCES
fluent-bundle.js
fluent-sequence.js
fluent-langneg.js
bundled-deps.js
)
target_link_libraries(kazvqmlqmlmodule PUBLIC kazvprivlib)
set(kazv_SRCS
main.cpp
l10n/translations.qrc
)
add_executable(kazv ${kazv_SRCS})
target_link_libraries(kazv
PRIVATE
kazvqmlqmlmodule
kazvqmlqmlmoduleplugin
kazvprivlib
)
if(kazv_LINK_BREEZE_ICONS)
target_link_libraries(kazv PRIVATE KF${KF_MAJOR_VERSION}::BreezeIcons)
endif()
set(kazv_INSTALL_ARGS ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
if(NOT kazv_INSTALL_ARGS)
set(kazv_INSTALL_ARGS ${KF${KF_MAJOR_VERSION}_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
install(TARGETS kazv ${kazv_INSTALL_ARGS})
add_subdirectory(tests)
diff --git a/src/constants.cpp b/src/constants.cpp
new file mode 100644
index 0000000..001c634
--- /dev/null
+++ b/src/constants.cpp
@@ -0,0 +1,7 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "constants.hpp"
diff --git a/src/constants.hpp b/src/constants.hpp
new file mode 100644
index 0000000..e30d4f9
--- /dev/null
+++ b/src/constants.hpp
@@ -0,0 +1,53 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <client.hpp>
+#include <QObject>
+#include <QQmlEngine>
+
+class Constants : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+public:
+ enum LoadSessionResult {
+ /// Successfully loaded the session
+ SessionLoadSuccess,
+ /// There is no store file
+ SessionNotFound,
+ /// The format of the store file is not supported
+ SessionFormatUnknown,
+ /// The store file cannot be backed up
+ SessionCannotBackup,
+ /// Cannot grab the lock on the session file
+ SessionLockFailed,
+ /// Cannot open store file
+ SessionCannotOpenFile,
+ /// Cannot deserialize the store file
+ SessionDeserializeFailed,
+ };
+
+ Q_ENUM(LoadSessionResult);
+
+ enum CreateRoomPreset {
+ PrivateChat = Kazv::CreateRoomPreset::PrivateChat,
+ PublicChat = Kazv::CreateRoomPreset::PublicChat,
+ TrustedPrivateChat = Kazv::CreateRoomPreset::TrustedPrivateChat,
+ };
+
+ Q_ENUM(CreateRoomPreset);
+
+ enum MediaDownloadEndpointVersion {
+ UnauthenticatedMediaV3, // Classical endpoint series
+ AuthenticatedMediaV1, // Endpoints that needs authorization
+ };
+
+ Q_ENUM(MediaDownloadEndpointVersion);
+};
diff --git a/src/contents/ui/AddStickerPopup.qml b/src/contents/ui/AddStickerPopup.qml
index 91672b3..336c093 100644
--- a/src/contents/ui/AddStickerPopup.qml
+++ b/src/contents/ui/AddStickerPopup.qml
@@ -1,102 +1,102 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.20 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
Kazv.SelfDestroyableOverlaySheet {
id: addStickerPopup
property var stickerPackList
property var event
readonly property var availablePacks: stickerPackList.packs
readonly property var packNameProvider: Kazv.StickerPackNameProvider {
}
readonly property var availablePackNames: availablePacks.map(k => packNameProvider.getName(k))
readonly property alias packIndex: packChooser.currentIndex
property var currentPack: stickerPackList.at(packIndex)
property var stickerSize: Kirigami.Units.iconSizes.enormous
property var addingSticker: false
title: l10n.get('add-sticker-popup-title')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
Kirigami.Icon {
Layout.preferredHeight: addStickerPopup.stickerSize
Layout.preferredWidth: addStickerPopup.stickerSize
height: addStickerPopup.stickerSize
width: addStickerPopup.stickerSize
- source: matrixSdk.mxcUriToHttp(event.content.url)
+ source: matrixSession.mxcUriToHttp(event.content.url)
}
ComboBox {
id: packChooser
objectName: 'packChooser'
Layout.fillWidth: true
model: addStickerPopup.availablePackNames
currentIndex: 0
Kirigami.FormData.label: l10n.get('add-sticker-popup-pack-prompt')
}
TextField {
Layout.fillWidth: true
id: shortCodeInput
objectName: 'shortCodeInput'
readOnly: addStickerPopup.addingSticker
Kirigami.FormData.label: l10n.get('add-sticker-popup-short-code-prompt')
}
Label {
objectName: 'shortCodeExistsWarning'
visible: currentPack.hasShortCode(shortCodeInput.text)
text: l10n.get('add-sticker-popup-short-code-exists-warning')
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Button {
objectName: 'addStickerButton'
text: l10n.get('add-sticker-popup-add-sticker-button')
onClicked: addStickerPopup.updateStickerPack.call()
enabled: !addStickerPopup.addingSticker
}
Button {
objectName: 'cancelButton'
text: l10n.get('add-sticker-popup-cancel-button')
onClicked: addStickerPopup.close()
}
}
}
property var updateStickerPack: Kazv.AsyncHandler {
trigger: () => {
addStickerPopup.addingSticker = true;
- return matrixSdk.updateStickerPack(
+ return matrixSession.updateStickerPack(
addStickerPopup.currentPack.addSticker(shortCodeInput.text, addStickerPopup.event)
);
}
onResolved: (success, data) => {
addStickerPopup.addingSticker = false;
if (!success) {
showPassiveNotification(l10n.get('add-sticker-popup-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
addStickerPopup.close();
}
}
}
}
diff --git a/src/contents/ui/AvatarAdapter.qml b/src/contents/ui/AvatarAdapter.qml
index 5e900a6..f6e45d8 100644
--- a/src/contents/ui/AvatarAdapter.qml
+++ b/src/contents/ui/AvatarAdapter.qml
@@ -1,37 +1,37 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import org.kde.kirigamiaddons.components 1.0 as KAC
import '.' as Kazv
KAC.Avatar {
id: avatar
property string mxcUri
readonly property var manager: kazvIOManager
- readonly property var sdk: matrixSdk
+ readonly property var session: matrixSession
source: fileHandler?.localFile || ''
property var fileHandlerComp: Component {
Kazv.FileHandler {
autoCache: true
kazvIOManager: avatar.manager
- matrixSdk: avatar.sdk
+ matrixSession: avatar.session
}
}
property var fileHandler: null
onMxcUriChanged: {
if (fileHandler) {
fileHandler.destroy();
fileHandler = null;
}
fileHandler = fileHandlerComp.createObject(avatar, {
eventContent: ({ url: avatar.mxcUri }),
});
}
}
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
index 1e41dca..4cf2a40 100644
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -1,269 +1,269 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
import 'matrix-helpers.js' as Helpers
ItemDelegate {
id: upper
objectName: 'bubble'
default property var children
property var currentEvent: event
property var menuContent: []
property var shouldDisplayTime: displayTime || (!compactMode && !event.isLocalEcho)
property var eventPinned: event.eventId && pinnedEvents.eventIds.hasOwnProperty(event.eventId)
readonly property var eventReactions: event.reactions()
topPadding: 0
bottomPadding: 0
readonly property var bubbleSpacing: leftPadding + rightPadding
readonly property double replyMaxHeight: Kirigami.Units.gridUnit * 7
Layout.fillWidth: true
property var backgroundItem: Rectangle {
anchors.fill: parent
color: (hovered || isSelected) ? Kirigami.Theme.activeBackgroundColor : Kirigami.Theme.backgroundColor
}
background: compactMode ? null : backgroundItem
property var eventSourcePopupComp: Component {
Kazv.SelfDestroyableOverlaySheet {
shouldSelfDestroy: true
title: l10n.get('event-source-popup-title')
Kazv.EventSourceView {
Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
event: upper.currentEvent
}
}
}
property var reactionPopupComp: Component {
Kazv.ReactToEventPopup {
shouldSelfDestroy: true
onAccepted: eventView.reactWith(text)
}
}
function getIsEditable(event) {
- return event.sender === matrixSdk.userId
+ return event.sender === matrixSession.userId
&& event.type === 'm.room.message'
&& event.content.msgtype === 'm.text';
}
property var menuComp: Component {
Menu {
objectName: 'bubbleContextMenu'
property list<QtObject> staticItems: [
Kirigami.Action {
objectName: 'replyToMenuItem'
text: l10n.get('event-reply-action')
onTriggered: setDraftRelation('m.in_reply_to', currentEvent.eventId)
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'editMenuItem'
text: l10n.get('event-edit-action')
onTriggered: {
setDraftRelation('m.replace', currentEvent.eventId, Helpers.getEventBodyForEditing(event));
}
enabled: event && !event.redacted && !event.isLocalEcho && getIsEditable(event)
},
Kirigami.Action {
objectName: 'deleteMenuItem'
text: l10n.get('event-delete')
onTriggered: deleteEventRequested(event.isLocalEcho ? event.txnId : event.eventId, event.isLocalEcho)
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'reactMenuItem'
text: l10n.get('event-react-action')
onTriggered: reactionPopupComp.createObject(applicationWindow().overlay).open()
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'pinUnpinMenuItem'
enabled: !event.isLocalEcho
text: eventPinned ? l10n.get('event-unpin-action') : l10n.get('event-pin-action')
onTriggered: eventPinned ? unpinEventRequested(event.eventId) : pinEventRequested(event.eventId)
},
MenuSeparator {},
Kirigami.Action {
text: l10n.get('event-view-source')
onTriggered: eventSourcePopupComp.createObject(applicationWindow().overlay).open()
},
Kirigami.Action {
objectName: 'viewHistoryMenuItem'
text: l10n.get('event-view-history')
enabled: event && !!event.isEdited
onTriggered: viewEventHistoryRequested(event.eventId)
}
]
contentData: Array.from(staticItems).concat(upper.menuContent)
onClosed: destroy()
}
}
function shouldPopupMenu() {
return !compactMode
}
function maybePopupMenu() {
if (shouldPopupMenu()) {
menuComp.createObject(parent).popup(parent)
}
}
contentItem: RowLayout {
id: bubbleRootLayout
property var replyToOrAnnotatedEventId: currentEvent.replyingToEventId || (currentEvent.relationType === 'm.annotation' ? currentEvent.relatedEventId : '')
ColumnLayout {
Layout.fillWidth: true
MouseArea {
visible: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
Layout.fillWidth: true
Layout.margins: 0
Layout.preferredHeight: inReplyToLayout.implicitHeight
Layout.maximumHeight: upper.replyMaxHeight
clip: true
onClicked: eventListView.goToEvent(bubbleRootLayout.replyToOrAnnotatedEventId)
RowLayout {
id: inReplyToLayout
anchors {
top: parent.top
left: parent.left
right: parent.right
}
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: Kirigami.Units.smallSpacing
color: Kirigami.Theme.activeTextColor
clip: true
}
Loader {
id: inReplyToLoader
clip: true
source: Qt.resolvedUrl('EventViewCompact.qml')
active: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
asynchronous: true
Layout.fillWidth: true
property var event: room.messageById(bubbleRootLayout.replyToOrAnnotatedEventId)
property var props: ({
event,
- sender: room.member(event.sender || matrixSdk.userId),
+ sender: room.member(event.sender || matrixSession.userId),
stateKeyUser: event.stateKey ? room.member(event.stateKey) : {},
isGapped: timeline.gaps.hasOwnProperty(event.eventId),
})
}
}
}
ColumnLayout {
id: children
data: upper.children
}
Kazv.EventReactions {
objectName: 'eventReactions'
visible: !compactMode
Layout.fillWidth: true
reactions: upper.eventReactions
}
}
property var popupAction: Kirigami.Action {
id: popupAction
text: l10n.get('event-popup-action')
icon.name: 'view-more-horizontal-symbolic'
onTriggered: maybePopupMenu()
}
RowLayout {
Layout.alignment: Qt.AlignBottom
Kirigami.Icon {
source: 'emblem-encrypted-locked'
visible: !compactMode && event && event.encrypted
Layout.preferredHeight: inlineBadgeSize
Layout.preferredWidth: inlineBadgeSize
Accessible.role: Accessible.StaticText
Accessible.name: l10n.get('event-encrypted')
ToolTip.text: l10n.get('event-encrypted')
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hoverHandlerEncIcon.hovered
HoverHandler { id: hoverHandlerEncIcon }
}
ToolButton {
objectName: 'localEchoIndicator'
visible: !!event.isLocalEcho
hoverEnabled: true
enabled: event.isFailed
icon.name: event.isSending ? 'state-sync' : 'state-warning'
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Accessible.role: event.isSending ? Accessible.StaticText : Accessible.Button
Accessible.name: event.isSending ? l10n.get('event-sending') : l10n.get('event-resend')
ToolTip.text: event.isSending ? l10n.get('event-sending') : l10n.get('event-send-failed')
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hovered
onClicked: {
room.resendMessage(event.txnId);
}
}
Label {
objectName: 'timeIndicator'
visible: shouldDisplayTime
text: event.formattedTime
ToolTip.text: event.formattedDateTime
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hoverHandlerTime.hovered
HoverHandler { id: hoverHandlerTime }
font: Kirigami.Theme.smallFont
}
Label {
objectName: 'editedIndicator'
visible: !compactMode && event.isEdited
text: l10n.get('event-edited-indicator')
font: Kirigami.Theme.smallFont
}
Kazv.EventReadIndicator {
objectName: 'eventReadIndicator'
shouldShow: !compactMode && !event.isLocalEcho
model: event.readers()
}
ToolButton {
id: moreButton
objectName: 'moreButton'
visible: !compactMode
action: popupAction
display: AbstractButton.IconOnly
}
}
}
}
diff --git a/src/contents/ui/CreateRoomPage.qml b/src/contents/ui/CreateRoomPage.qml
index bb323c4..2f7ce5d 100644
--- a/src/contents/ui/CreateRoomPage.qml
+++ b/src/contents/ui/CreateRoomPage.qml
@@ -1,201 +1,201 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
import './matrix-helpers.js' as Helpers
import moe.kazv.mxc.kazv 0.0 as MK
Kazv.ClosableScrollableOverlay {
id: createRoomPage
title: l10n.get('create-room-page-title')
property var creatingRoom: false
property var inviteUserIds: []
property var encryptedChangedOnce: false
ButtonGroup {
id: roomTypeButtons
}
Kirigami.FormLayout {
anchors.fill: parent
ColumnLayout {
Kirigami.FormData.label: l10n.get('create-room-page-type-prompt')
RadioButton {
id: typePublic
objectName: 'typePublicRadio'
checked: true
onCheckedChanged: {
if (!createRoomPage.encryptedChangedOnce) {
encrypted.checked = !checked;
}
}
text: l10n.get('create-room-page-type-public')
ButtonGroup.group: roomTypeButtons
}
RadioButton {
id: typePrivate
objectName: 'typePrivateRadio'
text: l10n.get('create-room-page-type-private')
ButtonGroup.group: roomTypeButtons
}
RadioButton {
id: typeDirect
objectName: 'typeDirectRadio'
text: l10n.get('create-room-page-type-direct')
ButtonGroup.group: roomTypeButtons
}
}
TextField {
id: roomName
objectName: 'roomNameInput'
Kirigami.FormData.label: l10n.get('create-room-page-name-prompt')
placeholderText: l10n.get('create-room-page-name-placeholder')
Layout.fillWidth: true
}
TextField {
id: roomAlias
objectName: 'roomAliasInput'
Kirigami.FormData.label: l10n.get('create-room-page-alias-prompt')
placeholderText: l10n.get('create-room-page-alias-placeholder')
Layout.fillWidth: true
}
TextField {
id: roomTopic
objectName: 'roomTopicInput'
Kirigami.FormData.label: l10n.get('create-room-page-topic-prompt')
placeholderText: l10n.get('create-room-page-topic-placeholder')
Layout.fillWidth: true
}
CheckBox {
id: allowFederate
checked: true
text: l10n.get('create-room-page-allow-federate-prompt')
Layout.fillWidth: true
}
CheckBox {
id: encrypted
objectName: 'encryptedInput'
text: l10n.get('create-room-page-encrypted-prompt')
Layout.fillWidth: true
onToggled: createRoomPage.encryptedChangedOnce = true
}
Item {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: l10n.get('create-room-invite-userids-prompt')
}
ColumnLayout {
Repeater {
model: createRoomPage.inviteUserIds.length
delegate: Kirigami.SwipeListItem {
objectName: 'inviteUserItem_' + index
Layout.fillWidth: true
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
contentItem: RowLayout {
Label {
Layout.fillWidth: true
objectName: 'inviteUserIdLabel'
text: createRoomPage.inviteUserIds[index]
}
}
actions: [
Kirigami.Action {
objectName: 'removeInviteAction'
icon.name: 'list-remove-symbolic'
text: l10n.get('create-room-page-action-remove-invite')
onTriggered: {
createRoomPage.inviteUserIds.splice(index, 1);
// trigger reactivity
createRoomPage.inviteUserIds = createRoomPage.inviteUserIds;
}
}
]
}
}
}
TextField {
id: newInviteUserId
Kirigami.FormData.label: l10n.get('create-room-page-add-invite-prompt')
placeholderText: l10n.get('create-room-page-add-invite-placeholder')
objectName: 'newInviteUserInput'
text: ''
Layout.fillWidth: true
}
Button {
id: addInviteUserButton
objectName: 'addInviteUserButton'
text: l10n.get('create-room-page-action-add-invite')
Layout.fillWidth: true
onClicked: {
createRoomPage.inviteUserIds.push(newInviteUserId.text);
// trigger reactivity
createRoomPage.inviteUserIds = createRoomPage.inviteUserIds;
newInviteUserId.text = '';
}
}
Button {
Layout.fillWidth: true
objectName: 'createRoomButton'
text: l10n.get('create-room-page-action-create-room')
onClicked: createRoomPage.createRoom.call()
}
}
property var createRoom: Kazv.AsyncHandler {
- trigger: () => matrixSdk.createRoom(
+ trigger: () => matrixSession.createRoom(
/* isPrivate = */ typePrivate.checked || typeDirect.checked,
roomName.text,
roomAlias.text,
createRoomPage.inviteUserIds,
/* isDirect = */ typeDirect.checked && createRoomPage.inviteUserIds.length === 1,
/* allowFederate = */ allowFederate.checked,
roomTopic.text,
/* powerLevelContentOverride = */ {},
/* preset = */ typeDirect.checked
- ? MK.MatrixSdk.TrustedPrivateChat
+ ? MK.Constants.TrustedPrivateChat
: typePrivate.checked
- ? MK.MatrixSdk.PrivateChat
- : MK.MatrixSdk.PublicChat,
+ ? MK.Constants.PrivateChat
+ : MK.Constants.PublicChat,
/* encrypted = */ encrypted.checked,
)
onResolved: {
if (success) {
if (typeDirect.checked && createRoomPage.inviteUserIds.length === 1) {
- matrixSdk.addDirectRoom(createRoomPage.inviteUserIds[0], data.roomId);
+ matrixSession.addDirectRoom(createRoomPage.inviteUserIds[0], data.roomId);
}
showPassiveNotification(l10n.get('create-room-page-success-prompt'));
pageStack.removePage(createRoomPage);
} else {
showPassiveNotification(l10n.get('create-room-page-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function presetPage(userId) {
typeDirect.checked = true;
newInviteUserId.text = userId;
addInviteUserButton.click();
}
}
diff --git a/src/contents/ui/EventReactions.qml b/src/contents/ui/EventReactions.qml
index a173fa9..66bc55f 100644
--- a/src/contents/ui/EventReactions.qml
+++ b/src/contents/ui/EventReactions.qml
@@ -1,89 +1,89 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
Flow {
required property var reactions
Repeater {
model: reactions
delegate: RowLayout {
id: reaction
objectName: `reaction${index}`
spacing: -1
Kirigami.Theme.colorSet: Kirigami.Theme.Button
required property int index
readonly property var item: reactions.at(index)
readonly property string key: item.key
readonly property double commonHeight: Math.max(content.implicitHeight, count.implicitHeight)
readonly property string selfReactEventId: getSelfReactEventId()
function getSelfReactEventId() {
for (let i = 0; i < item.count; ++i) {
const e = item.at(i);
- if (e.sender === matrixSdk.userId) {
+ if (e.sender === matrixSession.userId) {
return e.eventId;
}
}
return '';
}
ItemDelegate {
id: content
objectName: 'reactionContent'
Layout.preferredHeight: reaction.commonHeight
rightInset: 0
contentItem: Text {
objectName: 'reactionContentText'
textFormat: Qt.RichText
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
// See the comments in event-types/Reaction.qml
text: '<span style="font-family: emoji, sans-serif;">' + MK.KazvUtil.escapeHtml(key) + '</span>'
Accessible.description: l10n.get('event-react-button-description', { key })
color: content.checked ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor
}
checked: !!reaction.selfReactEventId
onClicked: reaction.selfReactEventId ? deleteEventRequestedNoConfirm(reaction.selfReactEventId, /* isLocalEcho = */ false) : eventView.reactWith(key)
background: Rectangle {
topLeftRadius: Kirigami.Units.cornerRadius
bottomLeftRadius: Kirigami.Units.cornerRadius
border.color: Kirigami.Theme.highlightColor
color: content.hovered
? Kirigami.Theme.hoverColor
: content.checked
? Kirigami.Theme.highlightColor
: 'transparent'
}
}
ItemDelegate {
id: count
objectName: 'reactionCount'
Layout.preferredHeight: reaction.commonHeight
leftInset: 0
contentItem: Text {
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
text: `${reaction.item.count}`
color: Kirigami.Theme.textColor
}
background: Rectangle {
topRightRadius: Kirigami.Units.cornerRadius
bottomRightRadius: Kirigami.Units.cornerRadius
border.color: Kirigami.Theme.highlightColor
color: count.hovered
? Kirigami.Theme.hoverColor
: 'transparent'
}
}
}
}
}
diff --git a/src/contents/ui/EventViewWrapper.qml b/src/contents/ui/EventViewWrapper.qml
index 63007a7..5921ca9 100644
--- a/src/contents/ui/EventViewWrapper.qml
+++ b/src/contents/ui/EventViewWrapper.qml
@@ -1,19 +1,19 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
Kazv.EventView {
- sender: room.member(event.sender || matrixSdk.userId)
+ sender: room.member(event.sender || matrixSession.userId)
stateKeyUser: event.stateKey ? room.member(event.stateKey) : {}
isGapped: timeline?.gaps?.hasOwnProperty(event.eventId)
}
diff --git a/src/contents/ui/FileHandler.qml b/src/contents/ui/FileHandler.qml
index 6ba00a7..6dc8424 100644
--- a/src/contents/ui/FileHandler.qml
+++ b/src/contents/ui/FileHandler.qml
@@ -1,172 +1,172 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import moe.kazv.mxc.kazv 0.0 as MK
import QtQml 2.15
import QtQuick 2.15
/**
* Used to handle files described in event, such as caching(if autoCache is true) and download.
* It is either used for caching or downloading. These two operations can be performed in same FileHandler.(Set autoCache to true and then call downloadFile)
* But one Filehandler can only track one download/cache job.
*/
QtObject {
id: fileHandler
/**
* Just set these four properties, any other properties will be automatically evaluated.
* The eventContent can be modified at any time.
* Modifying other properties can lead to unpredictable behavior.
*/
required property var eventContent
required property bool autoCache // thumbnail(if exists) will be downloaded if set to true
required property var kazvIOManager
- required property var matrixSdk
+ required property var matrixSession
- readonly property var httpVer: matrixSdk.checkSpecVersion("v1.11") ? MK.MatrixSdk.AuthenticatedMediaV1 : MK.MatrixSdk.UnauthenticatedMediaV3
- readonly property var token: (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) ? matrixSdk.token : ""
+ readonly property var httpVer: matrixSession.checkSpecVersion("v1.11") ? MK.Constants.AuthenticatedMediaV1 : MK.Constants.UnauthenticatedMediaV3
+ readonly property var token: (httpVer === MK.Constants.AuthenticatedMediaV1) ? matrixSession.token : ""
property var kazvIOJob
readonly property url localFile: kazvIOJob ? '' : cachedFile
readonly property real percent: fileHandler.kazvIOJob ? fileHandler.kazvIOJob.progress : 0.0 // from 0 to 1
property var result
readonly property bool success: fileHandler.result == MK.KazvIOBaseJob.NoError
/**
* This attribute is only used after result is set (that is, after signal resultChanged is emitted),
* and only when success is true, this attribute has a correct and valid value.
*/
property url cachedFile
/**
* Emit when start download, not emit when start cache.
* Can be used to indicate progress bar visible
*/
signal downloadStarted
function getMediaId(mxcUri) {
const temp = mxcUri.toString().split('/')
return temp[2] + '_' + temp[3]
}
function downloadFile(saveFileUrl) {
fileHandler.result = undefined;
const file = getFile();
if (!file) {
return;
}
const mediaId = getMediaId(file.mxcUri);
kazvIOManager.startNewDownloadJob(mxcToHttp(file.mxcUri), saveFileUrl, mediaId,
file.sha256, token, file.key, file.iv);
fileHandler.updateKazvIOJob(mediaId, false);
fileHandler.downloadStarted();
}
function cacheFile() {
fileHandler.result = undefined;
const file = getThumbnail() || getFile();
if (!file) {
console.error('No valid file to cache');
return;
}
const mediaId = getMediaId(file.mxcUri);
const cachedFile = kazvIOManager.cacheFile(
mxcToHttp(file.mxcUri), mediaId,
file.sha256, token, file.key, file.iv);
fileHandler.cachedFile = cachedFile;
fileHandler.updateKazvIOJob(mediaId, true);
return cachedFile;
}
function updateKazvIOJob(mediaId, isCache) {
fileHandler.kazvIOJob = null
if (isCache) {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getCacheJob(mediaId)
} else {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getDownloadJob(mediaId)
}
if (kazvIOJob) {
fileHandler.kazvIOJob.result.connect(function (ec) {
fileHandler.result = ec
})
}
}
function mxcToHttp(mxcUri) {
if (!mxcUri) {
return "";
}
- if (httpVer === MK.MatrixSdk.UnauthenticatedMediaV3) {
- return matrixSdk.mxcUriToHttp(mxcUri);
- } else if (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) {
- return matrixSdk.mxcUriToHttpAuthenticatedV1(mxcUri);
+ if (httpVer === MK.Constants.UnauthenticatedMediaV3) {
+ return matrixSession.mxcUriToHttp(mxcUri);
+ } else if (httpVer === MK.Constants.AuthenticatedMediaV1) {
+ return matrixSession.mxcUriToHttpAuthenticatedV1(mxcUri);
} else {
return "";
}
}
function getThumbnail() {
return getFileOrThumbnail(eventContent?.info?.thumbnail_file, eventContent?.info?.thumbnail_url);
}
function getFile() {
return getFileOrThumbnail(eventContent?.file, eventContent?.url);
}
// https://spec.matrix.org/v1.16/client-server-api/#mfile
function getFileOrThumbnail(encryptedFile, unencryptedMxcUri) {
const encryptedMxcUri = encryptedFile?.url;
const encryptedSha256 = encryptedFile?.hashes?.sha256;
const encryptedKey = encryptedFile?.key?.k;
const encryptedIv = encryptedFile?.iv;
const isEncrypted = !!(encryptedMxcUri && encryptedSha256 && encryptedKey && encryptedIv);
const hasFile = !!(isEncrypted || unencryptedMxcUri);
if (!hasFile) {
return undefined;
}
return ({
mxcUri: isEncrypted ? encryptedMxcUri : unencryptedMxcUri,
sha256: isEncrypted ? encryptedSha256 : "",
key: isEncrypted ? encryptedKey : "",
iv: isEncrypted ? encryptedIv : ""
})
}
// Cache file and then open it
function openFile() {
const kazvIOManager = fileHandler.kazvIOManager;
const filePath = this.cacheFile();
if (kazvIOManager.fileExists(filePath)) {
Qt.openUrlExternally(filePath);
return;
}
this.kazvIOJob.result.connect((ec, data) => {
if (ec == MK.KazvIOBaseJob.NoError) {
Qt.openUrlExternally(filePath);
} else {
showPassiveNotification(kazvIOManager.jobResultL10nMsg(ec, false));
}
});
}
onEventContentChanged: {
if (fileHandler.autoCache) {
fileHandler.cacheFile();
} else {
// Check if the file is downloading
const mediaId = getMediaId(getFile()?.mxcUri || "");
fileHandler.updateKazvIOJob(mediaId, autoCache);
fileHandler.onSuccessChanged.connect(function () {
fileHandler.kazvIOManager.deleteDownloadJob(mediaId);
});
}
}
}
diff --git a/src/contents/ui/ImageAdapter.qml b/src/contents/ui/ImageAdapter.qml
index f8707d4..d302e6d 100644
--- a/src/contents/ui/ImageAdapter.qml
+++ b/src/contents/ui/ImageAdapter.qml
@@ -1,36 +1,36 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import '.' as Kazv
Image {
id: image
property string mxcUri
readonly property var manager: kazvIOManager
- readonly property var sdk: matrixSdk
+ readonly property var session: matrixSession
source: fileHandler?.localFile || ''
property var fileHandlerComp: Component {
Kazv.FileHandler {
autoCache: true
kazvIOManager: image.manager
- matrixSdk: image.sdk
+ matrixSession: image.session
}
}
property var fileHandler: null
onMxcUriChanged: {
if (fileHandler) {
fileHandler.destroy();
fileHandler = null;
}
fileHandler = fileHandlerComp.createObject(image, {
eventContent: ({ url: image.mxcUri }),
});
}
}
diff --git a/src/contents/ui/JoinRoomPage.qml b/src/contents/ui/JoinRoomPage.qml
index 69d58e0..0201409 100644
--- a/src/contents/ui/JoinRoomPage.qml
+++ b/src/contents/ui/JoinRoomPage.qml
@@ -1,80 +1,80 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.1
import org.kde.kirigami 2.4 as Kirigami
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.1
import '.' as Kazv
Kazv.ClosableScrollableOverlay {
id: joinRoomPage
title: l10n.get('join-room-page-title')
ButtonGroup {
id: visibilityButtons
}
ColumnLayout {
width: parent.width
spacing: Kirigami.Units.largeSpacing
GridLayout {
columns: 2
Label {
text: l10n.get('join-room-page-id-or-alias-prompt')
}
TextField {
id: idOrAlias
objectName: 'idOrAliasInput'
placeholderText: l10n.get('join-room-page-id-or-alias-placeholder')
Layout.fillWidth: true
}
Label {
text: l10n.get('join-room-page-servers-prompt')
}
TextArea {
id: servers
objectName: 'serversInput'
placeholderText: l10n.get('join-room-page-servers-placeholder')
Layout.fillWidth: true
}
}
Button {
objectName: 'joinRoomButton'
text: l10n.get('join-room-page-action-join-room')
onClicked: joinRoomPage.joinRoom()
}
}
property var joinRoomHandler: Kazv.AsyncHandler {
property var serverNames: servers.text.split('\n').map(k => k.trim()).filter(k => k)
property var room: idOrAlias.text
- trigger: () => matrixSdk.joinRoom(room, serverNames)
+ trigger: () => matrixSession.joinRoom(room, serverNames)
onResolved: {
if (success) {
showPassiveNotification(l10n.get('join-room-page-success-prompt', { room }));
pageStack.removePage(joinRoomPage);
} else {
showPassiveNotification(l10n.get('join-room-page-failed-prompt', { room, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function joinRoom()
{
joinRoomHandler.call();
}
function presetPage(roomIdOrAlias, routingServers) {
idOrAlias.text = roomIdOrAlias;
servers.text = routingServers.join('\n');
}
}
diff --git a/src/contents/ui/LoginFlows.qml b/src/contents/ui/LoginFlows.qml
index 4384732..069bb29 100644
--- a/src/contents/ui/LoginFlows.qml
+++ b/src/contents/ui/LoginFlows.qml
@@ -1,211 +1,217 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import '.' as Kazv
StackView {
id: loginFlows
initialItem: userIdScreen
implicitHeight: currentItem.childrenRect.height
property bool parentLoading
property bool isSwitchingAccount
property bool ownLoading: false
property bool loading: parentLoading || ownLoading
property string discoveredServerUrl: ''
property var availableFlows: []
property var userIdScreen: Kirigami.FormLayout {
width: parent.width
TextField {
id: userIdField
objectName: 'userIdField'
Layout.fillWidth: true
placeholderText: l10n.get('login-page-userid-input-placeholder')
Kirigami.FormData.label: l10n.get('login-page-userid-prompt')
enabled: !loginFlows.loading
}
TextField {
id: serverUrlField
objectName: 'serverUrlField'
Layout.fillWidth: true
placeholderText: l10n.get('login-page-server-url-placeholder')
Kirigami.FormData.label: l10n.get('login-page-server-url-prompt')
enabled: !loginFlows.loading
}
Button {
id: userIdNextButton
objectName: 'userIdNextButton'
text: l10n.get('login-page-next-action')
enabled: !loginFlows.loading
Layout.fillWidth: true
onClicked: () => {
loginFlows.ownLoading = true;
- if (loginFlows.isSwitchingAccount) {
- matrixSdk.startNewSession();
- }
- matrixSdk.discoverAndGetLoginFlows(userIdField.text, serverUrlField.text);
+ matrixSdk.startNewSession();
+ // go to sdkConns.onSessionChanged
}
}
}
property var loginTypeScreen: ColumnLayout {
width: parent.width
id: loginTypeScreen
property var serverUrl: serverUrlField.text || loginFlows.discoveredServerUrl
property var ssoRedirectUrl: ''
Label {
Layout.fillWidth: true
text: l10n.get('login-page-server-label', { serverUrl: loginTypeScreen.serverUrl })
}
Button {
Layout.fillWidth: true
text: l10n.get('login-page-change-server-button')
onClicked: () => {
loginFlows.discoveredServerUrl = '';
loginFlows.ownLoading = false;
loginFlows.availableFlows = [];
loginFlows.pop();
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('login-page-login-flow-choice')
}
ColumnLayout {
RadioButton {
id: passwordRadio
objectName: 'passwordRadio'
Layout.fillWidth: true
text: l10n.get('login-page-login-flow-password')
enabled: loginFlows.supportsPassword()
}
RadioButton {
id: ssoRadio
objectName: 'ssoRadio'
Layout.fillWidth: true
text: l10n.get('login-page-login-flow-sso')
enabled: loginFlows.supportsSso()
}
}
Label {
objectName: 'noFlowIndicator'
Layout.fillWidth: true
visible: !loginFlows.supportsAnyLoginFlow()
text: l10n.get('login-page-no-flow-supported')
}
Label {
Layout.fillWidth: true
visible: passwordRadio.checked
wrapMode: Text.Wrap
text: l10n.get('login-page-password-prompt')
}
Kirigami.PasswordField {
id: passwordField
objectName: 'passwordField'
Layout.fillWidth: true
visible: passwordRadio.checked
enabled: !loginPage.loading
}
Button {
id: loginButton
objectName: 'passwordLoginButton'
Layout.fillWidth: true
text: l10n.get('login-page-login-button')
visible: passwordRadio.checked
enabled: !loginPage.loading
onClicked: {
loginFlows.ownLoading = true;
- matrixSdk.login(userIdField.text, passwordField.text, loginTypeScreen.serverUrl);
+ matrixSession.login(userIdField.text, passwordField.text, loginTypeScreen.serverUrl);
}
}
Button {
id: ssoButton
objectName: 'ssoButton'
Layout.fillWidth: true
text: l10n.get('login-page-login-with-sso-button')
visible: ssoRadio.checked
enabled: !loginFlows.loading
onClicked: {
loginFlows.ownLoading = true;
- loginTypeScreen.ssoRedirectUrl = matrixSdk.ssoLoginStart(loginTypeScreen.serverUrl);
+ loginTypeScreen.ssoRedirectUrl = matrixSession.ssoLoginStart(loginTypeScreen.serverUrl);
if (!inTest) {
Qt.openUrlExternally(loginTypeScreen.ssoRedirectUrl);
}
}
}
Label {
visible: !!loginTypeScreen.ssoRedirectUrl
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('login-page-login-sso-redirect-url-label')
}
Kazv.SelectableText {
objectName: 'ssoRedirectUrlLabel'
visible: !!loginTypeScreen.ssoRedirectUrl
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
text: loginTypeScreen.ssoRedirectUrl
}
}
- property var conns: Connections {
+ property var sdkConns: Connections {
target: matrixSdk
+ function onSessionChanged() {
+ if (loginFlows.ownLoading) {
+ matrixSession.discoverAndGetLoginFlows(userIdField.text, serverUrlField.text);
+ }
+ }
+ }
+ property var sessionConns: Connections {
+ target: matrixSession
function onDiscoverFailed(errorCode, errorMsg) {
showPassiveNotification(l10n.get('login-page-discover-failed-enter-prompt', { errorCode, errorMsg }));
loginFlows.discoveredServerUrl = '';
loginFlows.ownLoading = false;
}
function onDiscoverSuccessful(serverUrl) {
loginFlows.discoveredServerUrl = serverUrl;
}
function onGetLoginFlowsFailed(errorCode, errorMsg) {
showPassiveNotification(l10n.get('login-page-get-login-flows-failed-prompt', { errorCode, errorMsg }));
loginFlows.ownLoading = false;
loginFlows.availableFlows = [];
}
function onGetLoginFlowsSuccessful(flows) {
loginFlows.availableFlows = flows;
loginFlows.push(loginTypeScreen);
loginFlows.ownLoading = false;
}
function onLoginFailed() {
loginFlows.ownLoading = false;
}
}
function supportsPassword() {
return availableFlows.some((flow) => flow.type === 'm.login.password');
}
function supportsSso() {
return availableFlows.some((flow) => flow.type === 'm.login.sso');
}
function supportsAnyLoginFlow() {
return supportsPassword() || supportsSso();
}
}
diff --git a/src/contents/ui/LoginPage.qml b/src/contents/ui/LoginPage.qml
index f50006e..9caec57 100644
--- a/src/contents/ui/LoginPage.qml
+++ b/src/contents/ui/LoginPage.qml
@@ -1,78 +1,78 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.15
import org.kde.kirigami 2.8 as Kirigami
import '.' as Kazv
Kirigami.Page {
id: loginPage
title: l10n.get('login-page-title')
property var isSwitchingAccount: false
property bool loading: loadingSession
property bool inTest: false
ColumnLayout {
width: parent.width
spacing: Kirigami.Units.largeSpacing
ColumnLayout {
id: restoreSessionPart
width: parent.width
spacing: Kirigami.Units.largeSpacing
- property var sessions: matrixSdk.allSessions().filter(s => s !== sessionNameFor(matrixSdk.userId, matrixSdk.deviceId))
+ property var sessions: matrixSdk.allSessions().filter(s => s !== sessionNameFor(matrixSession?.userId, matrixSession?.deviceId))
Label {
text: l10n.get('login-page-existing-sessions-prompt')
wrapMode: Text.Wrap
Layout.fillWidth: true
}
ComboBox {
id: sessionChooser
Layout.fillWidth: true
model: restoreSessionPart.sessions
currentIndex: -1
enabled: !loginPage.loading
}
Button {
id: restoreButton
text: l10n.get('login-page-restore-session-button')
Layout.fillWidth: true
enabled: sessionChooser.currentIndex != -1 && !loginPage.loading
onClicked: {
const sess = restoreSessionPart.sessions[sessionChooser.currentIndex];
console.log('Selected session ', sess);
loadSession(sess);
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('login-page-alternative-password-login-prompt')
}
visible: sessions.length > 0
}
Kazv.LoginFlows {
objectName: 'loginFlows'
Layout.preferredWidth: parent.width
Layout.maximumWidth: parent.width
parentLoading: loginPage.loading
isSwitchingAccount: loginPage.isSwitchingAccount
}
ProgressBar {
visible: loginPage.loading
indeterminate: true
Layout.fillWidth: true
}
}
}
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
index ac76cc6..27227dd 100644
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -1,404 +1,414 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import org.kde.kirigami as Kirigami
import QtQuick.Layouts
import QtQuick.Controls
import moe.kazv.mxc.kazv as MK
import '.' as Kazv
import 'shortcuts' as KazvShortcuts
import 'l10n.js' as L10n
Kirigami.ApplicationWindow {
id: root
property var kazvIOManager: MK.KazvIOManager {
cacheDirectory: kazvConfig.cacheDirectory
function jobResultL10nMsg(ec: MK.KazvIOBaseJob.ErrorCode, isUpload: bool): string {
const resultL10nId = isUpload ? 'kazv-io-upload-failure-prompt' : 'kazv-io-download-failure-prompt'
const detailL10nId = new Map([
[MK.KazvIOBaseJob.UserCancel, 'kazv-io-failure-detail-user-cancel'],
[MK.KazvIOBaseJob.OpenFileError, 'kazv-io-failure-detail-open-file-error'],
[MK.KazvIOBaseJob.WriteFileError, 'kazv-io-failure-detail-write-file-error'],
[MK.KazvIOBaseJob.KIOError, 'kazv-io-failure-detail-network-error'],
[MK.KazvIOBaseJob.HashError, 'kazv-io-failure-detail-hash-error'],
[MK.KazvIOBaseJob.ResponseError, 'kazv-io-failure-detail-response-error'],
[MK.KazvIOBaseJob.KazvError, 'kazv-io-failure-detail-kazv-error']
]);
return l10n.get(resultL10nId, {detail: l10n.get(detailL10nId.get(ec))});
}
}
property bool loadingSession: false
property var matrixSdk: MK.MatrixSdk {
- onLoginSuccessful: {
+ onSessionChanged: {
+ console.log('session changed');
+ reloadSdkVariables();
+ root.purgeTimer.start();
+ }
+
+ onLoadSessionFinished: (sessionName, res) => {
+ root.loadingSession = false;
+ if (res === MK.Constants.SessionLoadSuccess) {
+ console.log('load session successful');
+ switchToMainPage();
+ recordLastSession();
+ matrixSession.getSpecVersions();
+ } else {
+ showPassiveNotification(getSessionLoadError(res, sessionName));
+ }
+ }
+ }
+
+ property var matrixSession: matrixSdk.session()
+ Connections {
+ target: matrixSession
+ function onLoginSuccessful() {
console.log('Login successful');
switchToMainPage();
recordLastSession();
root.purgeTimer.start();
}
- onLoginFailed: {
+ function onLoginFailed() {
console.log("Login Failed");
showPassiveNotification(l10n.get('login-page-request-failed-prompt', { errorCode, errorMsg }));
}
- onSessionChanged: {
- console.log('session changed');
- reloadSdkVariables();
- root.purgeTimer.start();
- }
- onLogoutSuccessful: {
+
+ function onLogoutSuccessful() {
root.loggedIn = false;
pageStack.clear();
pushLoginPage();
}
- onLogoutFailed: {
+
+ function onLogoutFailed() {
console.warn('Logout failed');
showPassiveNotification(l10n.get('logout-failed-prompt', { errorCode, errorMsg }));
}
- onLoadSessionFinished: (sessionName, res) => {
- root.loadingSession = false;
- if (res === MK.MatrixSdk.SessionLoadSuccess) {
- console.log('load session successful');
- switchToMainPage();
- recordLastSession();
- matrixSdk.getSpecVersions();
- } else {
- showPassiveNotification(getSessionLoadError(res, sessionName));
- }
- }
}
- property var loggedIn: !!matrixSdk.token
+ property var loggedIn: !!matrixSession?.token
property var sdkVars: QtObject {
- property var roomList: matrixSdk.roomList()
- property var userGivenNicknameMap: matrixSdk.userGivenNicknameMap()
+ property var roomList: matrixSession?.roomList()
+ property var userGivenNicknameMap: matrixSession?.userGivenNicknameMap()
property string currentRoomId: ''
}
property var purgeTimer: Timer {
interval: 1000 * 60 * 5
repeat: true
triggeredOnStart: true
onTriggered: () => {
- matrixSdk.purgeEventsExceptRooms(
+ matrixSession?.purgeEventsExceptRooms(
sdkVars.currentRoomId ? [sdkVars.currentRoomId] : []);
}
}
property var l10nProvider: MK.L10nProvider {
}
property var kazvConfig: MK.KazvConfig {
}
function initializeL10n () {
const availableLocales = l10nProvider.availableLocaleCodes();
console.log('available locales:', availableLocales);
const desiredLanguages = [Qt.uiLanguage];
console.log('ui language is: ', desiredLanguages);
const defaultLocale = 'en';
const wantedLocales = L10n.negotiateLanguages(desiredLanguages, availableLocales, defaultLocale);
console.log('wanted locales:', wantedLocales);
const bundles = L10n.generateBundles(wantedLocales, l10nProvider.getFtlData(wantedLocales));
const provider = new L10n.FluentProvider(bundles);
return provider;
}
property var l10n: initializeL10n();
property var confirmLogoutPopup: Kirigami.OverlaySheet {
id: confirmLogoutPopup
title: l10n.get('confirm-logout-popup-title')
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('confirm-logout-popup-prompt')
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
Button {
text: l10n.get('confirm-logout-popup-accept-button')
onClicked: {
hardLogout();
confirmLogoutPopup.close();
}
}
Button {
text: l10n.get('confirm-logout-popup-cancel-button')
onClicked: confirmLogoutPopup.close();
}
}
}
}
- title: matrixSdk.userId ? l10n.get('app-title-with-user-id', { userId: matrixSdk.userId }) : l10n.get('app-title-not-logged-in')
+ title: matrixSession?.userId ? l10n.get('app-title-with-user-id', { userId: matrixSession.userId }) : l10n.get('app-title-not-logged-in')
globalDrawer: Kirigami.GlobalDrawer {
title: l10n.get('global-drawer-title')
titleIcon: "applications-graphics"
property var actionGroupName: 'global-drawer-actions'
actions: [
Kirigami.Action {
objectName: 'switch-account'
text: l10n.get('global-drawer-action-switch-account')
icon.name: 'system-switch-user'
onTriggered: switchAccount()
},
Kirigami.Action {
objectName: 'hard-logout'
text: l10n.get('global-drawer-action-hard-logout')
icon.name: 'application-exit'
onTriggered: confirmLogoutPopup.open()
},
Kirigami.Action {
objectName: 'save'
text: l10n.get('global-drawer-action-save-session')
icon.name: 'document-save'
onTriggered: {
recordLastSession()
matrixSdk.serializeToFile()
}
},
Kirigami.Action {
objectName: 'configure-shortcuts'
text: l10n.get('global-drawer-action-configure-shortcuts')
icon.name: 'configure-shortcuts'
onTriggered: {
pushActionSettingsPage();
}
},
Kirigami.Action {
objectName: 'settings'
text: l10n.get('global-drawer-action-settings')
icon.name: 'configure'
onTriggered: {
pushSettingsPage();
}
},
Kirigami.Action {
objectName: 'create-room'
enabled: root.loggedIn
icon.name: 'contact-new'
text: l10n.get('global-drawer-action-create-room')
onTriggered: {
pushCreateRoomPage();
}
},
Kirigami.Action {
objectName: 'join-room'
enabled: root.loggedIn
text: l10n.get('global-drawer-action-join-room')
icon.name: 'list-add-user'
onTriggered: {
pushJoinRoomPage();
}
},
Kirigami.Action {
objectName: 'verifications'
enabled: root.loggedIn
icon.name: 'checkmark'
text: l10n.get('global-drawer-action-verifications')
onTriggered: {
pushVerificationsPage();
}
},
Kirigami.Action {
objectName: 'aboutAction'
text: l10n.get('global-drawer-action-about')
icon.name: 'help-about'
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('About.qml'))
}
]
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
property var actionCollection: KazvShortcuts.ActionCollection {
id: globalActionCollection
children: [
root.globalDrawer,
root.contextDrawer,
]
shortcutsConfig: kazvConfig
}
property var notifier: Kazv.Notifier {
}
pageStack.initialPage: Qt.resolvedUrl("LoginPage.qml")
property var emptyPage: Kirigami.Page {
title: l10n.get('empty-room-page-title')
Label {
text: l10n.get('empty-room-page-description')
}
}
signal switchToRoomRequested(string roomId)
property var pageManager: Kazv.PageManager {
pageStack: root.pageStack
sdkVars: root.sdkVars
main: root
}
function activateRoomSettingsPage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomSettingsPage.qml"), {
room
});
}
function activateRoomPinnedEventsPage(room) {
pageStack.push(Qt.resolvedUrl('RoomPinnedEventsPage.qml'), {
room
});
}
function activateRoomInvitePage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomInvitePage.qml"), {
room
});
}
function activateRoomStickerPacksPage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomStickerPacksPage.qml"), {
room
});
}
function activateRoomStatePage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomStatePage.qml"), {
room
});
}
function activateUserPage(user, room, userId) {
pageStack.layers.push(Qt.resolvedUrl("UserPage.qml"), {
userId: userId || user.userId,
user,
room
});
}
function switchToMainPage() {
pageStack.clear();
pageStack.layers.clear();
pageStack.replace([Qt.resolvedUrl('MainPage.qml')]);
pageStack.currentIndex = 0;
}
function switchToLoginPage() {
pageStack.replace([Qt.resolvedUrl("LoginPage.qml")])
pageStack.currentIndex = 0
}
function pushLoginPage() {
pageStack.layers.push(Qt.resolvedUrl("LoginPage.qml"), {
isSwitchingAccount: true,
});
}
function pushActionSettingsPage() {
pageStack.layers.push(Qt.resolvedUrl("ActionSettingsPage.qml"), {
actionsToConfigure: globalActionCollection.allActions(),
});
}
function pushSettingsPage() {
pageStack.layers.push(Qt.resolvedUrl("SettingsPage.qml"));
}
function pushVerificationsPage() {
pageStack.layers.push(Qt.resolvedUrl("VerificationsPage.qml"));
}
function pushCreateRoomPage() {
pageStack.layers.push(Qt.resolvedUrl("CreateRoomPage.qml"), {});
}
function pushJoinRoomPage() {
pageStack.layers.push(Qt.resolvedUrl("JoinRoomPage.qml"), {});
}
function sessionNameFor(userId, deviceId) {
return userId + '/' + deviceId;
}
function getSessionLoadError(res, sessionName) {
switch (res) {
- case MK.MatrixSdk.SessionNotFound:
+ case MK.Constants.SessionNotFound:
return l10n.get('session-load-failure-not-found', { sessionName });
- case MK.MatrixSdk.SessionFormatUnknown:
+ case MK.Constants.SessionFormatUnknown:
return l10n.get('session-load-failure-format-unknown', { sessionName });
- case MK.MatrixSdk.SessionCannotBackup:
+ case MK.Constants.SessionCannotBackup:
return l10n.get('session-load-failure-cannot-backup', { sessionName });
- case MK.MatrixSdk.SessionLockFailed:
+ case MK.Constants.SessionLockFailed:
return l10n.get('session-load-failure-lock-failed', { sessionName });
- case MK.MatrixSdk.SessionCannotOpenFile:
+ case MK.Constants.SessionCannotOpenFile:
return l10n.get('session-load-failure-cannot-open-file', { sessionName });
- case MK.MatrixSdk.SessionDeserializeFailed:
+ case MK.Constants.SessionDeserializeFailed:
return l10n.get('session-load-failure-deserialize-failed', { sessionName });
default:
console.error('loadSession: Encountered an unknown error code');
}
}
function loadSession(sessionName) {
root.loadingSession = true;
matrixSdk.loadSession(sessionName);
}
function loadLastSession() {
console.log('last session is:', kazvConfig.lastSession);
return loadSession(kazvConfig.lastSession);
}
function recordLastSession() {
- kazvConfig.lastSession = sessionNameFor(matrixSdk.userId, matrixSdk.deviceId);
+ kazvConfig.lastSession = sessionNameFor(matrixSession?.userId, matrixSession?.deviceId);
}
function reloadSdkVariables() {
- sdkVars.roomList = matrixSdk.roomList();
- sdkVars.userGivenNicknameMap = matrixSdk.userGivenNicknameMap();
+ matrixSession = matrixSdk.session();
+ sdkVars.roomList = matrixSession?.roomList();
+ sdkVars.userGivenNicknameMap = matrixSession?.userGivenNicknameMap();
sdkVars.currentRoomId = '';
}
function switchAccount() {
pushLoginPage();
}
function hardLogout() {
- matrixSdk.logout();
+ matrixSession?.logout();
}
Component.onCompleted: {
actionCollection.setupShortcuts();
+ matrixSdk.startThread();
loadLastSession();
}
onClosing: {
kazvIOManager.clearJobs()
}
}
diff --git a/src/contents/ui/MainPage.qml b/src/contents/ui/MainPage.qml
index 362953f..4731013 100644
--- a/src/contents/ui/MainPage.qml
+++ b/src/contents/ui/MainPage.qml
@@ -1,57 +1,57 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 6.6
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
Kirigami.ScrollablePage {
- title: l10n.get('main-page-title', { userId: matrixSdk.userId })
+ title: l10n.get('main-page-title', { userId: matrixSession.userId })
property var currentTagId: ''
onCurrentTagIdChanged: sdkVars.roomList.setTagId(currentTagId)
header: ColumnLayout {
spacing: 0
TextField {
text: sdkVars.roomList.filter
onTextChanged: sdkVars.roomList.filter = text
placeholderText: l10n.get('main-page-room-filter-prompt')
Layout.fillWidth: true
}
TabBar {
Layout.fillWidth: true
TabButton {
icon.name: 'clock'
text: l10n.get('main-page-recent-tab-title')
onClicked: currentTagId = ''
checkable: true
checked: currentTagId === ''
}
TabButton {
icon.name: 'non-starred-symbolic'
text: l10n.get('main-page-favourites-tab-title')
onClicked: currentTagId = 'm.favourite'
checkable: true
checked: currentTagId === 'm.favourite'
}
}
}
Kazv.RoomListView {
roomList: sdkVars.roomList
anchors.fill: parent
}
}
diff --git a/src/contents/ui/MediaFileMenu.qml b/src/contents/ui/MediaFileMenu.qml
index 8630d89..bed99eb 100644
--- a/src/contents/ui/MediaFileMenu.qml
+++ b/src/contents/ui/MediaFileMenu.qml
@@ -1,62 +1,62 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
QtObject {
id: mediaFileMenu
required property var eventContent
required property var eventId
property var jobManager: kazvIOManager
- property var mtxSdk: matrixSdk
+ property var session: matrixSession
signal downloadStarted
property var fileHandler: FileHandler {
id: fileHandler
autoCache: false
eventContent: mediaFileMenu.eventContent
kazvIOManager: mediaFileMenu.jobManager
- matrixSdk: mediaFileMenu.mtxSdk
+ matrixSession: mediaFileMenu.session
}
property var viewAction: Kirigami.Action {
text: l10n.get('media-file-menu-option-view')
onTriggered: {
fileHandler.openFile();
}
}
property var saveAction: Kirigami.Action {
text: l10n.get('media-file-menu-option-save-as')
onTriggered: {
fileDialog.saveAs(mediaFileMenu.eventContent, mediaFileMenu.eventId);
}
}
property var optionMenu: [viewAction, saveAction]
Component.onCompleted: {
mediaFileMenu.fileHandler.downloadStarted.connect(mediaFileMenu.downloadStarted)
eventView.downloadStarted.connect(() => {
const file = mediaFileMenu.fileHandler.getFile();
if (file) {
const mediaId = mediaFileMenu.fileHandler.getMediaId(file.mxcUri);
mediaFileMenu.fileHandler.updateKazvIOJob(mediaId, false);
}
mediaFileMenu.downloadStarted();
});
}
}
diff --git a/src/contents/ui/Notifier.qml b/src/contents/ui/Notifier.qml
index 65779b1..f95f90a 100644
--- a/src/contents/ui/Notifier.qml
+++ b/src/contents/ui/Notifier.qml
@@ -1,50 +1,50 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import 'matrix-helpers.js' as Helpers
import '.' as Kazv
QtObject {
id: notifier
property var notificationComp: Component {
Kazv.MessageNotification {
autoDelete: true
}
}
property var nameProvider: Kazv.UserNameProvider {}
property var roomNameProvider: Kazv.RoomNameProvider {}
property var conn: Connections {
- target: matrixSdk
+ target: matrixSession
function onReceivedMessage(roomId, eventId) {
- const roomList = matrixSdk.roomList();
+ const roomList = matrixSession.roomList();
const room = roomList.room(roomId);
const event = room.messageById(eventId);
const sender = room.member(event.sender);
const senderName = nameProvider.getName(sender);
- if (matrixSdk.shouldNotify(event)) {
+ if (matrixSession.shouldNotify(event)) {
console.debug('Push rules say we should notify this');
const notification = notificationComp.createObject(
notifier,
{
roomId,
});
- notification.eventId = matrixSdk.shouldPlaySound(event) ? 'message' : 'messageWithoutSound';
+ notification.eventId = matrixSession.shouldPlaySound(event) ? 'message' : 'messageWithoutSound';
notification.title = roomNameProvider.getName(room);
const message = event.content.body;
notification.text = message
? l10n.get('notification-message', { user: senderName, message })
: l10n.get('notification-message-no-content', { user: senderName });
notification.sendEvent();
} else {
console.debug('Push rules say we should not notify this');
}
}
}
}
diff --git a/src/contents/ui/RoomListView.qml b/src/contents/ui/RoomListView.qml
index 186ea6c..f552e4c 100644
--- a/src/contents/ui/RoomListView.qml
+++ b/src/contents/ui/RoomListView.qml
@@ -1,38 +1,36 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import 'matrix-helpers.js' as Helpers
import '.' as Kazv
import 'room-settings' as RoomSettings
ListView {
id: roomListView
property var roomList
property var roomIds: roomList.roomIds
property var iconSize: Kirigami.Units.iconSizes.large
- //matrixSdk.currentRoomId
-
Layout.fillHeight: true
model: roomList
Layout.minimumHeight: childrenRect.height
currentIndex: roomIds.indexOf(sdkVars.currentRoomId)
delegate: Kazv.RoomListViewItemDelegate {
required property var index
item: roomList.at(index)
iconSize: roomListView.iconSize
}
}
diff --git a/src/contents/ui/RoomListViewItemDelegate.qml b/src/contents/ui/RoomListViewItemDelegate.qml
index 0ac39df..2dd71f4 100644
--- a/src/contents/ui/RoomListViewItemDelegate.qml
+++ b/src/contents/ui/RoomListViewItemDelegate.qml
@@ -1,171 +1,171 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import 'matrix-helpers.js' as Helpers
import 'room-settings' as RoomSettings
import '.' as Kazv
Kirigami.SwipeListItem {
id: upper
property var item
property var iconSize: 1
property var isFavourite: item.tagIds.includes('m.favourite')
property var roomNameProvider: Kazv.RoomNameProvider {
room: upper.item
}
property var roomDisplayName: roomNameProvider.name
property var roomTimeline: item.timeline()
property var lastUnreadMessage: getLastUnreadMessage(roomTimeline, roomTimeline.count, item)
property var lastUnreadMessageReaders: lastUnreadMessage ? lastUnreadMessage.readers() : null
// The readers count is to make hasUnreadMessages prop reactive
// against our newly-posted receipts
property var hasUnreadMessages: getHasUnreadMessages(item, roomTimeline, roomTimeline.count, lastUnreadMessageReaders ? lastUnreadMessageReaders.count : 0)
onClicked: switchToRoomRequested(item.roomId)
checkable: true
checked: sdkVars.currentRoomId == item.roomId
autoExclusive: true
Kirigami.Theme.colorSet: !checked ? Kirigami.Theme.View : Kirigami.Theme.Selection
Kirigami.Theme.inherit: false
background: Rectangle {
anchors.fill: parent
color: hovered ? Kirigami.Theme.hoverColor : Kirigami.Theme.backgroundColor
}
contentItem: RowLayout {
Kazv.AvatarAdapter {
mxcUri: item.roomOrHeroAvatarMxcUri
name: Helpers.roomAvatarPlaceholderName(item)
}
Label {
objectName: 'tombstoneIndicator'
visible: item.tombstoned === true
text: l10n.get('room-list-view-room-item-tombstone')
background: Rectangle {
color: Kirigami.Theme.negativeBackgroundColor
}
color: Kirigami.Theme.negativeTextColor
}
Label {
objectName: 'inviteIndicator'
visible: item.membership === MK.MatrixRoom.Invite
text: l10n.get('room-list-view-room-item-invited')
background: Rectangle {
color: Kirigami.Theme.activeBackgroundColor
}
color: Kirigami.Theme.activeTextColor
}
Label {
objectName: 'leaveIndicator'
visible: item.membership === MK.MatrixRoom.Leave
text: l10n.get('room-list-view-room-item-left')
color: Kirigami.Theme.disabledTextColor
}
Label {
objectName: 'roomDisplayNameLabel'
text: roomDisplayName
Layout.fillWidth: true
elide: Text.ElideRight
color: item.membership === MK.MatrixRoom.Leave ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
}
Rectangle {
objectName: 'unreadNotificationCount'
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.preferredWidth: Math.max(unreadNotificationCountLabel.implicitWidth, Kirigami.Units.gridUnit)
Layout.preferredHeight: Kirigami.Units.gridUnit
radius: Kirigami.Units.gridUnit / 2
color: Kirigami.Theme.activeBackgroundColor
visible: !!item.unreadNotificationCount
RowLayout {
anchors.fill: parent
Label {
id: unreadNotificationCountLabel
Layout.alignment: Qt.AlignHCenter
color: Kirigami.Theme.activeTextColor
text: l10n.get('room-list-view-room-item-unread-notification-count', { count: item.unreadNotificationCount })
Accessible.ignored: true
}
}
Accessible.role: Accessible.Indicator
Accessible.name: l10n.get('room-list-view-room-item-unread-notification-count-text', { count: item.unreadNotificationCount })
}
Rectangle {
objectName: 'unreadIndicator'
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
radius: Kirigami.Units.iconSizes.small / 2
color: Kirigami.Theme.activeTextColor
visible: !item.unreadNotificationCount && hasUnreadMessages
Accessible.role: Accessible.Indicator
Accessible.name: l10n.get('room-list-view-room-item-unread-indicator')
}
}
actions: [
Kirigami.Action {
text: l10n.get("room-list-view-room-item-fav-action")
checkable: true
checked: isFavourite
icon.name: isFavourite ? 'starred-symbolic' : 'non-starred-symbolic'
onTriggered: toggleFavourite()
},
Kirigami.Action {
text: l10n.get('room-list-view-room-item-more-action')
icon.name: 'view-more-horizontal-symbolic'
onTriggered: activateRoomSettingsPage(item)
}
]
function toggleFavourite() {
favouriteToggler.toggleTag('m.favourite');
}
property var favouriteToggler: RoomSettings.RoomTagHandler {
room: item
}
function getHasUnreadMessages(room, timeline, timelineCount, readersCount) {
return !!room.unreadNotificationCount || !!getLastUnreadMessage(timeline, timelineCount, room);
}
function getLastUnreadMessage(timeline, timelineCount, room) {
for (let i = 0; i < timelineCount; ++i) {
const event = timeline.at(i);
if (event.isLocalEcho) {
// Skip local echoes
} else if (room.localReadMarker === event.eventId) {
// This message is read locally
return undefined;
- } else if (event.sender === matrixSdk.userId) {
+ } else if (event.sender === matrixSession.userId) {
// Own message here
return undefined;
- } else if (Helpers.isEventReadBy(event, matrixSdk.userId)) {
+ } else if (Helpers.isEventReadBy(event, matrixSession.userId)) {
// This message is read
return undefined;
} else {
// The latest message is not read and not sent by the current user
return event;
}
}
}
}
diff --git a/src/contents/ui/RoomPage.qml b/src/contents/ui/RoomPage.qml
index 2eb6b64..06c35d4 100644
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -1,339 +1,339 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import Qt.labs.platform as Platform
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
import 'matrix-helpers.js' as Helpers
Kazv.ScrollablePageAdapter {
id: roomPage
property string roomId: ''
property var room: sdkVars.roomList.room(roomId)
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomPage.room
}
property var roomTimeline: room.timeline()
property var pinnedEvents: room.pinnedEventsTimeline()
property var lastReceiptableEventId: getLastReceiptableEventId(roomTimeline, roomTimeline.count)
property var paginationRequests: ({})
property var refreshRoomStateRequest: null
property var fileDialog: Kazv.FileDialogAdapter {
id: fileDialog
property var eventContent
property var eventId
property var jobManager: kazvIOManager
property var mtxSdk: matrixSdk
property var fileHandler: Kazv.FileHandler {
id: fileHandler
autoCache: false
eventContent: fileDialog.eventContent
kazvIOManager: fileDialog.jobManager
matrixSdk: fileDialog.mtxSdk
}
function saveAs(eventContent, eventId) {
fileDialog.eventContent = eventContent;
fileDialog.eventId = eventId;
fileDialog.open();
}
folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DownloadLocation)
onAccepted: {
fileDialog.fileHandler.downloadFile(fileUrl);
// A newly created eventView will automatically look for its KazvIOJob.
// Here we just need to find those eventViews that have been created and notify them that the download has started.
for (const eventView of roomTimelineView.contentItem.children) {
if (eventView.event.eventId === fileDialog.eventId) {
eventView.downloadStarted();
}
}
}
Component.onCompleted: {
if (MK.KazvUtil.kfQtMajorVersion === 5) {
fileDialog.selectExisting = false;
} else {
fileDialog.fileMode = Kazv.FileDialogAdapter.SaveFile;
}
}
}
signal mentionUserRequested(string userId)
signal replaceDraftRequested(string newDraft)
signal paginateBackRequested(string eventId)
signal backfillRequested(string eventId)
signal viewEventHistoryRequested(string eventId)
title: roomNameProvider.name
padding: 0
property var isInvite: room.membership === MK.MatrixRoom.Invite
property var isJoin: room.membership === MK.MatrixRoom.Join
property var eventHistoryPopupComp: Component {
Kazv.SelfDestroyableOverlaySheet {
id: historyPopup
property var eventId
property var event: room.messageById(eventId)
shouldSelfDestroy: true
title: l10n.get('event-history-popup-title')
Kazv.EventHistoryView {
Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
history: historyPopup.event.history()
}
}
}
contextualActions: [
Kirigami.Action {
id: acceptInviteAction
icon.name: 'checkmark'
text: l10n.get('room-invite-accept-action')
visible: isInvite
onTriggered: joinRoomHandler.call()
},
Kirigami.Action {
id: rejectInviteAction
icon.name: 'im-ban-kick-user'
text: l10n.get('room-invite-reject-action')
visible: isInvite
onTriggered: leaveRoomHandler.call()
},
Kirigami.Action {
id: pinnedEventsAction
objectName: 'pinnedEventsAction'
icon.name: 'pin'
visible: roomPage.pinnedEvents.count
enabled: roomPage.pinnedEvents.count
text: l10n.get('room-pinned-events-action', { count: roomPage.pinnedEvents.count })
onTriggered: activateRoomPinnedEventsPage(room)
},
Kirigami.Action {
id: scrollToLatestAction
objectName: 'scrollToLatestAction'
icon.name: "go-down"
enabled: !roomTimelineView.atYEnd
text: l10n.get("room-timeline-scroll-to-latest-action")
onTriggered: {
roomTimelineView.positionViewAtBeginning();
}
},
Kirigami.Action {
id: roomSettingsAction
icon.name: 'settings-configure'
text: l10n.get('room-settings-action')
onTriggered: activateRoomSettingsPage(room)
}
]
property var inviteOverlay: Kirigami.OverlaySheet {
id: inviteOverlay
title: l10n.get('room-invite-popup-title')
parent: roomPage.overlay
- property var self: room.member(matrixSdk.userId)
+ property var self: room.member(matrixSession.userId)
property var inviteEvent: self.toEvent()
property var inviter: room.member(inviteEvent.sender)
property var inviterName: Helpers.userNameWithId(inviter, l10n)
ColumnLayout {
Label {
text: {
inviteOverlay.inviterName
? l10n.get('room-invite-popup-text-with-inviter', { inviterName: inviteOverlay.inviterName })
: l10n.get('room-invite-popup-text')
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Button {
action: acceptInviteAction
}
Button {
action: rejectInviteAction
}
}
}
}
Component.onCompleted: {
if (isInvite) {
inviteOverlay.open();
}
}
RoomTimelineView {
id: roomTimelineView
objectName: 'roomTimelineView'
timeline: roomPage.roomTimeline
}
onRoomChanged: {
kazvIOManager.deleteModelIfEmpty(roomId)
uploadList.model = kazvIOManager.getUploadJobs(roomId)
}
footer: Item {
height: childrenRect.height
width: parent.width
ListView {
id: uploadList
width: parent.width
height: contentHeight
model: kazvIOManager.getUploadJobs(roomId)
delegate: Kazv.KazvIOMenu {
width: parent.width
jobId: roomId
isUpload: true
}
}
Kazv.TypingIndicator {
id: typingIndicator
typingUsers: roomPage.room.typingUsers()
width: parent.width
anchors.top: uploadList.bottom
}
Kazv.SendMessageBox {
id: sendMessageBox
objectName: 'sendMessageBox'
width: parent.width
room: roomPage.room
anchors.top: typingIndicator.bottom
enabled: isJoin
}
}
function setDraftRelation(relType, eventId, newDraft) {
sendMessageBox.setDraftRelation(relType, eventId, newDraft);
}
property var joinRoomHandler: Kazv.AsyncHandler {
- trigger: () => matrixSdk.joinRoom(roomId, [])
+ trigger: () => matrixSession.joinRoom(roomId, [])
onResolved: {
if (success) {
showPassiveNotification(l10n.get('join-room-page-success-prompt', { room: roomId }));
inviteOverlay.close();
} else {
showPassiveNotification(l10n.get('join-room-page-failed-prompt', { room: roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var leaveRoomHandler: Kazv.AsyncHandler {
trigger: () => room.leaveRoom()
onResolved: {
if (success) {
showPassiveNotification(l10n.get('leave-room-success-prompt', { room: roomId }));
inviteOverlay.close();
} else {
showPassiveNotification(l10n.get('leave-room-failed-prompt', { room: roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
onMentionUserRequested: (userId) => {
sendMessageBox.mentionUser(userId);
sendMessageBox.focusInput();
}
onReplaceDraftRequested: (newDraft) => {
sendMessageBox.replaceDraft(newDraft);
}
function getLastReceiptableEventId(timeline, timelineCount) {
for (let i = 0; i < timelineCount; ++i) {
const event = timeline.at(i);
- if (!event.isLocalEcho && event.sender !== matrixSdk.userId) {
+ if (!event.isLocalEcho && event.sender !== matrixSession.userId) {
return event.eventId;
}
}
}
onLastReceiptableEventIdChanged: maybePostReadReceipt()
Window.onActiveChanged: maybePostReadReceipt()
onVisibleChanged: maybePostReadReceipt()
function maybePostReadReceipt() {
if (!Window.active || !roomPage.visible) {
return;
}
const eventId = roomPage.lastReceiptableEventId;
if (!eventId) {
return;
}
const event = roomPage.room.messageById(eventId);
- const readByMe = Helpers.isEventReadBy(event, matrixSdk.userId);
+ const readByMe = Helpers.isEventReadBy(event, matrixSession.userId);
if (!readByMe) {
roomPage.room.postReadReceipt(eventId);
}
}
onPaginateBackRequested: (eventId) => {
if (!paginationRequests[eventId]) {
console.debug('paginating back from', eventId);
paginationRequests[eventId] = room.paginateBackFrom(eventId);
paginationRequests[eventId].resolved.connect(() => {
console.debug('finished paginating back from', eventId);
delete paginationRequests[eventId];
});
} else {
console.debug('pagination request of', eventId, 'is already under way');
}
}
onBackfillRequested: (eventId) => {
if (!paginationRequests[eventId]) {
- paginationRequests[eventId] = matrixSdk.backfillRoomFromEvent(roomId, eventId);
+ paginationRequests[eventId] = matrixSession.backfillRoomFromEvent(roomId, eventId);
paginationRequests[eventId].resolved.connect((isSuccess, data) => {
console.debug(
'finished backfill from', eventId,
'success=', isSuccess, JSON.stringify(data));
delete paginationRequests[eventId];
});
}
}
onViewEventHistoryRequested: (eventId) => {
eventHistoryPopupComp.createObject(applicationWindow().overlay, { eventId }).open();
}
Timer {
id: goToEventTimer
property var eventId
onTriggered: {
console.log('goto event');
roomTimelineView.goToEvent(eventId);
}
}
function delayGoToEvent(eventId, time) {
goToEventTimer.interval = time;
goToEventTimer.eventId = eventId
goToEventTimer.start();
}
}
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index b090882..f931471 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,495 +1,495 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
ColumnLayout {
id: sendMessageBox
objectName: 'sendMessageBox'
property var room
property var draftRelType: room.draftRelType
property var draftRelatedTo: room.draftRelTo
property var timeline: room.timeline()
property var members: room.members()
property var changeDraftRelTypes: ['m.replace']
property var confirmSaveDraftOverlay: Kazv.AcceptRejectNoopOverlay {
id: confirmSaveDraftOverlay
objectName: 'confirmSaveDraftOverlay'
parent: Overlay.overlay
title: l10n.get('save-draft-confirmation-title')
message: l10n.get('save-draft-confirmation')
confirmButtonText: l10n.get('save-draft-confirm-action')
onConfirmButtonClicked: function() {
pushLocalDraft.call();
}
rejectButtonText: l10n.get('save-draft-discard-action')
onRejectButtonClicked: function() {
confirmSaveDraftOverlay.setLocalDraft.call();
}
noopButtonText: l10n.get('save-draft-cancel-action')
property var relType
property var relTo
property var newDraft
property var pushLocalDraft: Kazv.AsyncHandler {
trigger: () => room.pushLocalDraft(
confirmSaveDraftOverlay.newDraft,
confirmSaveDraftOverlay.relType,
confirmSaveDraftOverlay.relTo)
onResolved: (success) => {
if (success) {
textArea.changeText(confirmSaveDraftOverlay.newDraft);
}
}
}
property var setLocalDraft: Kazv.AsyncHandler {
trigger: () => room.setLocalDraft(
confirmSaveDraftOverlay.newDraft,
confirmSaveDraftOverlay.relType,
confirmSaveDraftOverlay.relTo)
onResolved: (success) => {
if (success) {
textArea.changeText(confirmSaveDraftOverlay.newDraft);
}
}
}
}
property var confirmDiscardDraftOverlay: Kazv.AcceptRejectNoopOverlay {
id: confirmDiscardDraftOverlay
objectName: 'confirmDiscardDraftOverlay'
parent: Overlay.overlay
title: l10n.get('discard-draft-confirmation-title')
message: l10n.get('discard-draft-confirmation')
confirmButtonText: l10n.get('discard-draft-confirm-action')
onConfirmButtonClicked: function() {
sendMessageBox.popLocalDraft.prevDraft = sendMessageBox.room.prevDraft;
sendMessageBox.popLocalDraft.call();
}
rejectButtonText: l10n.get('discard-draft-keep-action')
onRejectButtonClicked: function() {
sendMessageBox.room.setLocalDraftRel("", "");
}
noopButtonText: l10n.get('discard-draft-cancel-action')
}
property var popLocalDraft: Kazv.AsyncHandler {
trigger: () => sendMessageBox.room.popLocalDraft()
property var prevDraft
onResolved: (success) => {
if (success) {
textArea.changeText(prevDraft);
}
prevDraft = "";
}
}
function getRelationPrompt(draftRelType) {
if (draftRelType === 'm.in_reply_to') {
return l10n.get('send-message-box-reply-to');
} else if (draftRelType === 'm.replace') {
return l10n.get('send-message-box-edit');
}
}
function getCancelRelationPrompt(draftRelType) {
if (draftRelType === 'm.in_reply_to') {
return l10n.get('send-message-box-remove-reply-to-action');
} else if (draftRelType === 'm.replace') {
return l10n.get('send-message-box-remove-replace-action');
}
}
function replaceDraft(newDraft) {
textArea.changeText(newDraft, /* inhibitTyping = */ true);
}
function setDraftRelation(relType, eventId, newDraft) {
if (!changeDraftRelTypes.includes(relType)) {
room.setLocalDraftRel(relType, eventId);
return;
}
if (textArea.text === '') {
room.setLocalDraft(newDraft, relType, eventId);
textArea.changeText(newDraft);
return;
}
confirmSaveDraftOverlay.relType = relType;
confirmSaveDraftOverlay.relTo = eventId;
confirmSaveDraftOverlay.newDraft = newDraft;
confirmSaveDraftOverlay.open();
}
function resetDraftRelation() {
popLocalDraft.prevDraft = room.prevDraft;
popLocalDraft.call();
}
onRoomChanged: {
textArea.changeText(room.localDraft, true);
}
Popup {
id: completionPopup
objectName: 'completionPopup'
x: 1
y: -height
width: parent.width - 1
contentItem: Kazv.Completion {
id: completion
members: sendMessageBox.members
onMentionUserRequested: (userId) => {
textArea.removePartialMention();
sendMessageBox.mentionUser(userId);
}
}
}
Control {
visible: !!sendMessageBox.draftRelatedTo
Layout.fillWidth: true
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
}
contentItem: RowLayout {
spacing: 0
ColumnLayout {
Layout.fillWidth: true
Label {
id: replyLabel
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
text: getRelationPrompt(sendMessageBox.draftRelType)
}
Item {
id: replyItem
Layout.minimumHeight: Math.min(replyToEventView.implicitHeight, Kirigami.Units.gridUnit * 5)
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
Layout.fillWidth: true
clip: true
Kazv.EventViewWrapper {
id: replyToEventView
anchors {
top: parent.top
left: parent.left
right: parent.right
}
event: room.messageById(sendMessageBox.draftRelatedTo)
compactMode: true
}
}
}
ToolButton {
objectName: 'removeRelatedToButton'
Layout.alignment: Qt.AlignTop
action: removeRelatedToAction
display: AbstractButton.IconOnly
}
}
}
RowLayout {
ScrollView {
Layout.fillWidth: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
Layout.preferredHeight: textArea.implicitHeight
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
TextArea {
id: textArea
objectName: 'draftMessage'
property var shortcutList: ["Ctrl+Return", "Ctrl+Enter"]
property var inhibitTyping: false
placeholderText: l10n.get('send-message-box-input-placeholder')
property string filter: getFilter()
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
persistentSelection: true
onTextChanged: {
room.setLocalDraftText(text);
if (!inhibitTyping) {
room.setTyping(true);
}
inhibitTyping = false;
}
function changeText(newText, inhibitTyping) {
textArea.inhibitTyping = inhibitTyping;
textArea.text = newText;
}
onVisibleChanged: {
if (!visible) {
room.updateLocalDraftNow();
}
}
function getFilter() {
const atPos = text.lastIndexOf('@', cursorPosition);
if (atPos < 0) {
return '';
}
const textInBetween = text.slice(atPos, cursorPosition);
const hasSpaces = /\s/.test(textInBetween);
return hasSpaces ? '' : textInBetween;
}
function removePartialMention() {
const atPos = text.lastIndexOf('@', cursorPosition);
text = text.slice(0, atPos) + text.slice(cursorPosition);
cursorPosition = atPos;
}
onFilterChanged: {
sendMessageBox.members.filter = filter.slice(1);
if (filter && sendMessageBox.members.count > 0) {
completionPopup.open();
} else {
completionPopup.close();
}
}
// Shortcut keys for sending messages (TODO: Shortcut keys customized by the user)
Shortcut {
sequences: textArea.shortcutList
onActivated: {
sendAction.trigger()
}
}
DropArea {
anchors.fill: parent
onDropped: (drag) => {
if (drag.hasUrls) {
confirmUploadPopup.call(drag.urls);
}
}
}
function handlePaste() {
if (MK.KazvClipboard.hasUrls) {
confirmUploadPopup.call(MK.KazvClipboard.urls());
} else if (MK.KazvClipboard.hasImage) {
confirmUploadPopup.call([ MK.KazvClipboard.saveClipboardImage() ]);
} else {
textArea.paste();
}
}
Keys.onPressed: (event) => {
if (event.matches(StandardKey.Paste)) {
textArea.handlePaste();
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
preventStealing: true
cursorShape: Qt.IBeamCursor
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton)
textAreaMenu.popup();
}
Menu {
id: textAreaMenu
MenuItem {
text: l10n.get("action-undo")
icon.name: "edit-undo"
onTriggered: textArea.undo()
enabled: textArea.canUndo
}
MenuItem {
text: l10n.get("action-redo")
icon.name: "edit-redo"
onTriggered: textArea.redo()
enabled: textArea.canRedo
}
MenuSeparator {}
MenuItem {
text: l10n.get("action-cut")
icon.name: "edit-cut"
onTriggered: textArea.cut()
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-copy")
icon.name: "edit-copy"
onTriggered: textArea.copy()
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-paste")
icon.name: "edit-paste"
onTriggered: textArea.handlePaste()
enabled: textArea.canPaste || MK.KazvClipboard.hasImage || MK.KazvClipboard.hasUrls
}
MenuItem {
text: l10n.get("action-delete")
icon.name: "edit-delete"
onTriggered: textArea.remove(textArea.selectionStart, textArea.selectionEnd)
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-select-all")
icon.name: "edit-select-all"
onTriggered: textArea.selectAll()
}
}
}
}
}
ToolButton {
action: sendMediaFileAction
objectName: 'sendMediaFileButton'
display: AbstractButton.IconOnly
}
ToolButton {
action: stickersAction
display: AbstractButton.IconOnly
}
ToolButton {
objectName: 'sendButton'
action: sendAction
display: AbstractButton.IconOnly
}
}
function mentionUser(userId) {
const prefix = textArea.text.slice(0, textArea.cursorPosition);
const suffix = textArea.text.slice(textArea.cursorPosition);
let insertion = userId;
// If we are not at the beginning and the prev char is not a space,
// insert a space there.
if (prefix && prefix.slice(prefix.length - 1).trim() !== '') {
insertion = ' ' + insertion;
}
let postPadding = 0;
// If we are at the end or the next char is not a space,
// insert a space there.
if (!suffix || suffix.slice(0, 1).trim() !== '') {
insertion = insertion + ' ';
} else if (suffix) {
// If there is a suffix, and space is not inserted
// this means the next character is a space,
// move the cursor to that space.
postPadding = 1;
}
textArea.text = prefix + insertion + suffix;
textArea.cursorPosition = prefix.length + insertion.length + postPadding;
}
function focusInput() {
textArea.forceActiveFocus();
}
Kirigami.Action {
id: removeRelatedToAction
icon.name: 'window-close-symbolic'
text: getCancelRelationPrompt(sendMessageBox.draftRelType)
onTriggered: {
if (textArea.text) {
confirmDiscardDraftOverlay.open();
return;
}
sendMessageBox.resetDraftRelation();
}
}
Kirigami.Action {
id: sendAction
icon.name: "document-send"
text: l10n.get("send-message-box-send")
onTriggered: {
room.setTyping(false);
room.sendTextMessage(textArea.text, draftRelType, draftRelatedTo);
textArea.changeText("", true);
sendMessageBox.resetDraftRelation();
}
enabled: textArea.text !== ''
}
Kirigami.Action {
id: sendMediaFileAction
icon.name: "document-open-data"
text: l10n.get("send-message-box-send-file")
onTriggered: {
fileDialog.open()
}
}
property var confirmUploadPopup: ConfirmUploadPopup {
id: confirmUploadPopup
parent: applicationWindow().overlay
onUploadRequested: (url) => {
sendMessageBox.uploadFile(url);
}
}
property var fileDialog: Kazv.FileDialogAdapter {
objectName: 'fileDialog'
onAccepted: {
sendMessageBox.uploadFile(fileUrl);
}
}
function uploadFile(fileUrl) {
kazvIOManager.startNewUploadJob(
- matrixSdk.serverUrl, fileUrl, matrixSdk.token,
+ matrixSession.serverUrl, fileUrl, matrixSession.token,
room.roomId, sdkVars.roomList, room.encrypted,
draftRelType, draftRelatedTo
);
sendMessageBox.resetDraftRelation();
}
property var stickerPopup: Kirigami.OverlaySheet {
id: stickerPopup
parent: applicationWindow().overlay
title: l10n.get('send-message-box-stickers-popup-title')
Kazv.StickerPicker {
Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
- stickerPackList: matrixSdk.stickerPackList()
+ stickerPackList: matrixSession.stickerPackList()
onSendMessageRequested: eventJson => {
console.log(JSON.stringify(eventJson));
room.sendMessage(eventJson, draftRelType, draftRelatedTo);
sendMessageBox.resetDraftRelation();
stickerPopup.close();
}
}
}
Kirigami.Action {
id: stickersAction
icon.name: 'smiley'
text: l10n.get('send-message-box-stickers')
onTriggered: stickerPopup.open()
}
}
diff --git a/src/contents/ui/UploadFileHelper.qml b/src/contents/ui/UploadFileHelper.qml
index a33fbc0..4e6c630 100644
--- a/src/contents/ui/UploadFileHelper.qml
+++ b/src/contents/ui/UploadFileHelper.qml
@@ -1,51 +1,51 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
QtObject {
id: uploadFileHelper
function chooseFileAndUpload () {
fileDialog.open();
}
property var job: null
property var available: !!job
property var fileDialog: Kazv.FileDialogAdapter {
onAccepted: {
- uploadFileHelper.job = kazvIOManager.startNewRoomlessUploadJob(matrixSdk.serverUrl, fileUrl, matrixSdk.token)
+ uploadFileHelper.job = kazvIOManager.startNewRoomlessUploadJob(matrixSession.serverUrl, fileUrl, matrixSession.token)
}
}
signal uploaded(string mxcUri)
signal failed(int ec)
property var conn: Connections {
target: uploadFileHelper.job
function onResult(ec, mxcUri) {
if (ec === MK.KazvIOBaseJob.NoError) {
uploadFileHelper.uploaded(mxcUri);
} else {
uploadFileHelper.failed(ec);
}
}
}
function cleanup() {
if (uploadFileHelper.job) {
kazvIOManager.deleteRoomlessUploadJob(uploadFileHelper.job);
uploadFileHelper.job = null;
}
}
}
diff --git a/src/contents/ui/UserPage.qml b/src/contents/ui/UserPage.qml
index 132cc48..49ad437 100644
--- a/src/contents/ui/UserPage.qml
+++ b/src/contents/ui/UserPage.qml
@@ -1,394 +1,394 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import moe.kazv.mxc.kazv as MK
import '.' as Kazv
import 'device-mgmt' as KazvDM
import 'matrix-helpers.js' as Helpers
Kazv.ClosableScrollableOverlay {
id: userPage
property string userId: ''
property var user: ({})
property var nameProvider: Kazv.UserNameProvider {
user: userPage.user
}
property var room
property var roomNameProvider: Kazv.RoomNameProvider {
room: userPage.room
}
property var roomName: roomNameProvider.name
property var editingPowerLevel: false
property var submittingPowerLevel: false
property var powerLevelsLoaded: false
property var userPowerLevel: room.userPowerLevel(userId)
property var kickingUser: false
property var canKickUser: !(user.membership === 'ban' || user.membership === 'leave')
property var banningUser: false
property var unbanningUser: false
Connections {
target: room
function onPowerLevelsChanged() {
userPage.userPowerLevel = room.userPowerLevel(userPage.userId);
}
}
title: nameProvider.name
property var ensureMemberEvent: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.member', userId)
}
property var ensurePowerLevels: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.power_levels', '')
onResolved: (success, data) => {
if (success) {
userPage.powerLevelsLoaded = true;
}
}
}
property var updatingNameOverride: false
property var updateNameOverride: Kazv.AsyncHandler {
trigger: () => {
userPage.updatingNameOverride = true;
return sdkVars.userGivenNicknameMap.setAndUpload(
userPage.userId, nameOverrideInput.text || null);
}
onResolved: (success, data) => {
userPage.updatingNameOverride = false;
if (!success) {
showPassiveNotification(l10n.get('user-page-update-name-override-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property bool updatingSelfName: false
property var updateSelfName: Kazv.AsyncHandler {
trigger: () => {
userPage.updatingSelfName = true;
return userPage.room.setSelfName(selfNameInput.text);
}
onResolved: (success, data) => {
userPage.updatingSelfName = false;
if (!success) {
showPassiveNotification(l10n.get('user-page-update-self-name-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
Component.onCompleted: {
userPage.ensureMemberEvent.call();
userPage.ensurePowerLevels.call();
}
property var setPowerLevel: Kazv.AsyncHandler {
trigger: () => {
userPage.submittingPowerLevel = true;
return room.setUserPowerLevel(userPage.userId, parseInt(newPowerLevel.text));
}
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('user-page-set-power-level-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
userPage.editingPowerLevel = false;
}
userPage.submittingPowerLevel = false;
}
}
property var kickUser: Kazv.AsyncHandler {
trigger: () => {
userPage.kickingUser = true;
return room.kickUser(userPage.userId, kickUserReasonInput.text);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-kick-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
kickUserReasonInput.text = '';
}
userPage.kickingUser = false;
}
}
property var banUser: Kazv.AsyncHandler {
trigger: () => {
userPage.banningUser = true;
return room.banUser(userPage.userId, banUserReasonInput.text);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-ban-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
banUserReasonInput.text = '';
}
userPage.banningUser = false;
}
}
property var unbanUser: Kazv.AsyncHandler {
trigger: () => {
userPage.unbanningUser = true;
return room.unbanUser(userPage.userId);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-unban-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
userPage.unbanningUser = false;
}
}
ColumnLayout {
Kazv.AvatarAdapter {
id: avatar
objectName: 'avatar'
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: Kirigami.Units.iconSizes.enormous
Layout.preferredWidth: Kirigami.Units.iconSizes.enormous
mxcUri: userPage.user.avatarMxcUri
name: nameProvider.name
}
ColumnLayout {
Layout.preferredWidth: this.parent.width
Label {
objectName: 'userNameLabel'
Layout.alignment: Qt.AlignHCenter
text: !!userPage.user.name ? userPage.user.name : userPage.userId
}
Label {
objectName: 'userIdLabel'
Layout.alignment: Qt.AlignHCenter
font: Kirigami.Theme.smallFont
visible: !!userPage.user.name
text: userPage.userId
}
}
RowLayout {
- visible: room.membership === MK.MatrixRoom.Join && userPage.userId === matrixSdk.userId
+ visible: room.membership === MK.MatrixRoom.Join && userPage.userId === matrixSession.userId
TextField {
id: selfNameInput
objectName: 'selfNameInput'
placeholderText: l10n.get('user-page-self-name-placeholder')
Layout.fillWidth: true
Accessible.name: l10n.get('user-page-self-name-prompt')
enabled: !userPage.updatingSelfName
text: userPage.user.name
}
Button {
objectName: 'saveSelfNameButton'
text: l10n.get('user-page-save-self-name-action')
enabled: !userPage.updatingSelfName
onClicked: userPage.updateSelfName.call()
}
}
RowLayout {
// Do not allow user to set a name override for themselves
- visible: userPage.userId !== matrixSdk.userId
+ visible: userPage.userId !== matrixSession.userId
TextField {
id: nameOverrideInput
objectName: 'nameOverrideInput'
placeholderText: l10n.get('user-page-overrided-name-placeholder')
Layout.fillWidth: true
enabled: !userPage.updatingNameOverride
text: nameProvider.overridedName
}
Button {
objectName: 'saveNameOverrideButton'
text: l10n.get('user-page-save-name-override-action')
enabled: !userPage.updatingNameOverride
onClicked: updateNameOverride.call()
}
}
RowLayout {
visible: !userPage.editingPowerLevel
Label {
objectName: 'powerLevelLabel'
Layout.fillWidth: true
text: l10n.get('user-page-power-level', { powerLevel: userPage.userPowerLevel })
}
Button {
objectName: 'editPowerLevelButton'
text: l10n.get('user-page-edit-power-level-action')
enabled: userPage.powerLevelsLoaded
onClicked: {
userPage.editingPowerLevel = true;
}
}
}
RowLayout {
visible: userPage.editingPowerLevel
TextField {
objectName: 'newPowerLevelInput'
id: newPowerLevel
Layout.fillWidth: true
text: `${userPage.userPowerLevel}`
readOnly: userPage.submittingPowerLevel
}
Button {
objectName: 'savePowerLevelButton'
text: l10n.get('user-page-save-power-level-action')
onClicked: {
setPowerLevel.call();
}
enabled: !userPage.submittingPowerLevel
}
Button {
objectName: 'discardPowerLevelButton'
text: l10n.get('user-page-discard-power-level-action')
enabled: !userPage.submittingPowerLevel
onClicked: {
userPage.editingPowerLevel = false;
newPowerLevel.text = `${userPage.userPowerLevel}`;
}
}
}
ColumnLayout {
Kirigami.PromptDialog {
id: kickUserReasonDialog
objectName: 'kickUserReasonDialog'
title: l10n.get('user-page-kick-user-confirm-dialog-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
userPage.kickUser.call()
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('user-page-kick-user-confirm-dialog-content', {
userId: userPage.user.userId,
name: nameProvider.name,
roomName: userPage.roomName,
})
wrapMode: Text.Wrap
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('user-page-kick-user-confirm-dialog-content', {
userId: userPage.user.userId,
name: userPage.user.name,
roomName: userPage.roomName,
})
wrapMode: Text.Wrap
}
Kirigami.FormLayout {
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
id: kickUserReasonInput
objectName: 'kickUserReasonInput'
readOnly: userPage.kickingUser
Kirigami.FormData.label: l10n.get('user-page-kick-user-reason-prompt')
}
}
}
}
}
Button {
objectName: 'kickUserButton'
icon.name: 'im-kick-user'
Layout.fillWidth: true
enabled: !userPage.kickingUser
text: l10n.get('user-page-kick-user-action')
visible: userPage.canKickUser
onClicked: {
kickUserReasonDialog.open()
}
}
}
RowLayout {
Kirigami.PromptDialog {
id: banUserReasonDialog
objectName: 'banUserReasonDialog'
title: l10n.get('user-page-ban-user-confirm-dialog-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
userPage.banUser.call()
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('user-page-ban-user-confirm-dialog-content', {
userId: userPage.user.userId,
name: nameProvider.name,
roomName: userPage.roomName,
})
wrapMode: Text.Wrap
}
Kirigami.FormLayout {
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
id: banUserReasonInput
objectName: 'banUserReasonInput'
readOnly: userPage.banningUser
Kirigami.FormData.label: l10n.get('user-page-ban-user-reason-prompt')
}
}
}
}
Button {
objectName: 'banUserButton'
Layout.fillWidth: true
icon.name: 'im-ban-kick-user'
enabled: !userPage.banningUser
text: l10n.get('user-page-ban-user-action')
visible: !unbanUserButton.visible
onClicked: {
banUserReasonDialog.open()
}
}
Button {
id: unbanUserButton
objectName: 'unbanUserButton'
Layout.fillWidth: true
icon.name: 'im-user-online'
enabled: !userPage.unbanningUser
text: l10n.get('user-page-unban-user-action')
visible: user.membership === "ban"
onClicked: {
userPage.unbanUser.call()
}
}
}
KazvDM.DeviceList {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: childrenRect.height
userId: userPage.userId
- devices: matrixSdk.devicesOfUser(userId)
+ devices: matrixSession.devicesOfUser(userId)
}
}
}
diff --git a/src/contents/ui/VerificationsPage.qml b/src/contents/ui/VerificationsPage.qml
index ffdbcee..a154b46 100644
--- a/src/contents/ui/VerificationsPage.qml
+++ b/src/contents/ui/VerificationsPage.qml
@@ -1,106 +1,106 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import '.' as Kazv
import moe.kazv.mxc.kazv as MK
Kazv.ClosableScrollableOverlay {
id: verificationsPage
title: l10n.get('verifications-page-title')
- property var verificationList: matrixSdk.verificationList()
+ property var verificationList: matrixSession.verificationList()
Kirigami.CardsListView {
objectName: 'verificationsView'
model: verificationsPage.verificationList
delegate: Kirigami.AbstractCard {
id: c
required property int index
property var verification: verificationsPage.verificationList.at(index)
// From https://develop.kde.org/docs/getting-started/kirigami/components-card/#cardslistview
// NOTE: never put a Layout as contentItem as it will cause binding loops
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
ColumnLayout {
id: delegateLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
// IMPORTANT: never put the bottom margin
}
Label {
text: `${verification.userId} ${verification.deviceId}`
wrapMode: Text.Wrap
Layout.fillWidth: true
}
Label {
text: c.getStateText()
wrapMode: Text.Wrap
Layout.fillWidth: true
}
Kazv.SasCodes {
visible: c.verification.stateType == MK.MatrixVerification.CodeDisplayed
emojiIndices: c.verification.emojiIndices
decimalCodes: c.verification.decimalCodes
Layout.fillWidth: true
}
RowLayout {
Button {
objectName: 'readyButton'
visible: c.verification.stateType == MK.MatrixVerification.TheyRequested
text: l10n.get('verification-action-ready')
onClicked: c.verification.ready()
}
Button {
objectName: 'confirmSasButton'
visible: c.verification.stateType == MK.MatrixVerification.CodeDisplayed
text: l10n.get('verification-action-confirm-sas-match')
onClicked: c.verification.confirmSasMatch()
}
Button {
objectName: 'denySasButton'
visible: c.verification.stateType == MK.MatrixVerification.CodeDisplayed
text: l10n.get('verification-action-deny-sas-match')
onClicked: c.verification.denySasMatch()
}
Button {
objectName: 'cancelButton'
text: l10n.get('verification-action-cancel')
visible: c.verification.stateType != MK.MatrixVerification.Cancelled && c.verification.stateType != MK.MatrixVerification.Done
onClicked: c.verification.cancel()
}
}
}
}
function getStateText() {
switch (verification.stateType) {
case MK.MatrixVerification.TheyRequested:
return l10n.get('verification-state-they-requested');
case MK.MatrixVerification.Waiting:
return l10n.get('verification-state-waiting');
case MK.MatrixVerification.VerifiedThem:
return l10n.get('verification-state-waiting-after-verified');
case MK.MatrixVerification.CodeDisplayed:
return l10n.get('verification-state-confirm-code');
case MK.MatrixVerification.Done:
return l10n.get('verification-state-done');
case MK.MatrixVerification.Cancelled:
return l10n.get('verification-state-cancelled');
}
return '';
}
Component.onCompleted: {
console.log('verification:', verification.userId, verification.deviceId, verification.stateType, verification.emojiIndices, verification.decimalCodes);
}
}
}
}
diff --git a/src/contents/ui/device-mgmt/Device.qml b/src/contents/ui/device-mgmt/Device.qml
index c99797f..ceab9e5 100644
--- a/src/contents/ui/device-mgmt/Device.qml
+++ b/src/contents/ui/device-mgmt/Device.qml
@@ -1,183 +1,183 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '..' as Kazv
Kirigami.SwipeListItem {
id: device
property var item
property var userId
property bool sendingVerification: false
contentItem: RowLayout {
ColumnLayout {
Label {
objectName: 'deviceIdLabel'
text: item.deviceId
}
Label {
objectName: 'displayNameLabel'
text: item.displayName
font: Kirigami.Theme.smallFont
visible: !!item.displayName
}
Layout.fillWidth: true
}
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: device.getTrustLevelIcon(item.trustLevel)
Layout.preferredHeight: Kirigami.Units.iconSizes.sizeForLabels
Layout.preferredWidth: Kirigami.Units.iconSizes.sizeForLabels
Accessible.ignored: true
}
Label {
objectName: 'trustLevelLabel'
text: l10n.get(`device-trust-level-${item.trustLevel}`)
}
}
}
property var sheet: Kirigami.OverlaySheet {
id: popup
title: l10n.get('device-set-trust-level-dialog-title')
parent: applicationWindow().overlay
property var setTrustLevelPromise: null
property var conn: Connections {
target: popup.setTrustLevelPromise
function onResolved(success, data) {
if (!success) {
console.log('Error setting trust level:', JSON.stringify(data));
}
popup.setTrustLevelPromise = null;
popup.close();
}
}
ColumnLayout {
Label {
text: l10n.get('device-set-trust-level-dialog-name-label', { name: item.displayName })
}
Label {
text: l10n.get('device-set-trust-level-dialog-id-label', { id: item.deviceId })
}
Label {
text: l10n.get('device-set-trust-level-dialog-ed25519-key-label', { key: item.ed25519Key })
}
Label {
text: l10n.get('device-set-trust-level-dialog-curve25519-key-label', { key: item.curve25519Key })
}
ButtonGroup {
buttons: trustLevelChoices.children
}
ColumnLayout {
id: trustLevelChoices
objectName: 'trustLevelChoices'
Layout.fillHeight: true
Repeater {
model: ['blocked', 'unseen', 'seen', 'verified']
delegate: RadioButton {
objectName: 'trustLevelChoice_' + modelData
property var level: modelData
autoExclusive: true
checked: item.trustLevel === modelData
text: l10n.get(`device-trust-level-${modelData}`)
icon.name: device.getTrustLevelIcon(modelData)
}
}
}
RowLayout {
Button {
text: l10n.get('device-set-trust-level-dialog-save')
onClicked: {
const current = popup.getNewTrustLevel();
- popup.setTrustLevelPromise = matrixSdk.setDeviceTrustLevel(userId, item.deviceId, current);
+ popup.setTrustLevelPromise = matrixSession.setDeviceTrustLevel(userId, item.deviceId, current);
}
}
Button {
text: l10n.get('device-set-trust-level-dialog-cancel')
onClicked: {
popup.close();
}
}
}
}
function getNewTrustLevel() {
for (let i = 0; i < trustLevelChoices.children.length; ++i) {
const curItem = trustLevelChoices.children[i];
if (curItem.checked) {
return curItem.level;
}
}
return 'unknown';
}
}
actions: [
Kirigami.Action {
text: l10n.get('device-set-trust-level')
icon.name: 'settings-configure'
onTriggered: {
popup.open();
}
},
Kirigami.Action {
objectName: 'requestVerifyAction'
text: l10n.get('device-verify-action')
enabled: !device.sendingVerification
icon.name: 'emblem-locked'
onTriggered: {
requestVerify.call();
}
}
]
property var requestVerify: Kazv.AsyncHandler {
trigger: () => {
device.sendingVerification = true;
- return matrixSdk.requestVerifyDevice(userId, item.deviceId);
+ return matrixSession.requestVerifyDevice(userId, item.deviceId);
}
onResolved: (success, data) => {
device.sendingVerification = false;
if (success) {
pushVerificationsPage();
} else {
showPassiveNotification(l10n.get('device-verification-request-failed'));
}
}
}
function getTrustLevelIcon(level)
{
switch (level) {
case 'blocked':
return 'emblem-error';
case 'unseen':
return 'emblem-warning';
case 'seen':
return 'emblem-added';
case 'verified':
return 'emblem-checked';
default:
return 'emblem-question';
}
}
}
diff --git a/src/contents/ui/event-types/Audio.qml b/src/contents/ui/event-types/Audio.qml
index 18264f7..0cd136a 100644
--- a/src/contents/ui/event-types/Audio.qml
+++ b/src/contents/ui/event-types/Audio.qml
@@ -1,48 +1,48 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
import '..' as Kazv
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
- property var audioUri: matrixSdk.mxcUriToHttp(mxcUri)
+ property var audioUri: matrixSession.mxcUriToHttp(mxcUri)
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventContent: event.content
eventId: event.eventId
ColumnLayout {
property var msgLabel: Kazv.SelectableText {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('event-message-audio-sent', { gender, body })
}
data: [
msgLabel,
bubble.mediaFileMenu,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/File.qml b/src/contents/ui/event-types/File.qml
index aeef790..217be8e 100644
--- a/src/contents/ui/event-types/File.qml
+++ b/src/contents/ui/event-types/File.qml
@@ -1,48 +1,48 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
import '..' as Kazv
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
- property var fileUri: matrixSdk.mxcUriToHttp(mxcUri)
+ property var fileUri: matrixSession.mxcUriToHttp(mxcUri)
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventContent: event.content
eventId: event.eventId
ColumnLayout {
Layout.fillWidth: true
property var msgLabel: Kazv.SelectableText {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('event-message-file-sent', { gender, body })
}
data: [
msgLabel,
bubble.mediaFileMenu,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/Image.qml b/src/contents/ui/event-types/Image.qml
index e770662..e08cdbc 100644
--- a/src/contents/ui/event-types/Image.qml
+++ b/src/contents/ui/event-types/Image.qml
@@ -1,116 +1,116 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
import '..' as Kazv
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var curEvent: event
property var thumbnailFile: imageInfo.thumbnail_file || {}
property var thumbnailInfo: imageInfo.thumbnail_info || {}
property var thumbnailWidth: thumbnailInfo.w
property var thumbnailHeight: thumbnailInfo.h
property var imageInfo: event.content.info || {}
property var imageFile: event.content.file || {}
property var imageWidth: imageInfo.w
property var imageHeight: imageInfo.h
property var imageMaxHeight: compactMode ? Kirigami.Units.gridUnit * 5 : Infinity
property var imageMxcUri: fileHandler.getFile().mxcUri
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
property var jobManager: kazvIOManager
- property var mtxSdk: matrixSdk
+ property var session: matrixSession
property var isSticker: event.type === 'm.sticker'
summaryItem: Label {
objectName: 'summaryLabel'
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
elide: Text.ElideRight
text: l10n.get(isSticker ? 'event-summary-sticker-sent' : 'event-summary-image-sent', { gender, body})
}
Kazv.FileHandler {
id: fileHandler
eventContent: event.content
autoCache: true
kazvIOManager: upper.jobManager
- matrixSdk: upper.mtxSdk
+ matrixSession: upper.session
}
Types.MediaBubble {
id: bubble
eventContent: event.content
eventId: event.eventId
property var label: Kazv.SelectableText {
objectName: 'bodyLabel'
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get(isSticker ? 'event-message-sticker-sent' : 'event-message-image-sent', { gender, body})
visible: !compactMode
}
property var image: Image {
id: image
objectName: 'mainImage'
Layout.fillWidth: true
property double imageWidth: upper.thumbnailWidth || upper.imageWidth || image.implicitWidth || 1
property double imageHeight: upper.thumbnailHeight || upper.imageHeight || image.implicitHeight || 1
property double privHeight: Math.min(imageMaxHeight, imageHeight)
property double privWidth: privHeight / imageHeight * imageWidth
Layout.preferredWidth: privWidth
Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
horizontalAlignment: Image.AlignLeft
fillMode: Image.PreserveAspectFit
}
property var canAddSticker: !!event.content.url
property var addStickerPopupComp: Component {
Kazv.AddStickerPopup {
parent: applicationWindow().overlay
shouldSelfDestroy: true
- stickerPackList: matrixSdk.stickerPackList()
+ stickerPackList: matrixSession.stickerPackList()
event: upper.curEvent
}
}
property var addStickerAction: Kirigami.Action {
text: l10n.get('media-file-menu-add-sticker-action')
onTriggered: bubble.addStickerPopupComp.createObject(applicationWindow().overlay).open()
}
additionalMenuContent: canAddSticker ? [addStickerAction] : []
ColumnLayout {
id: layout
Layout.fillWidth: true
data: [
bubble.mediaFileMenu,
bubble.label,
bubble.image,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/Simple.qml b/src/contents/ui/event-types/Simple.qml
index a23f96e..668f4f5 100644
--- a/src/contents/ui/event-types/Simple.qml
+++ b/src/contents/ui/event-types/Simple.qml
@@ -1,127 +1,127 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '..' as Kazv
ColumnLayout {
objectName: 'eventViewMainItem'
property var event
property var sender
default property alias children: container.data
property var isSelected
/// The item shown after the username, in compact mode
property var summaryItem: Item {}
property var nameProvider: Kazv.UserNameProvider {
user: sender
}
property var senderNickname: nameProvider.name
property var contentMaxWidth: {
(parent.width
// avatar size and margins
- iconSize
- 2 * Kirigami.Units.largeSpacing
- 2 * Kirigami.Units.largeSpacing
)
}
property var shouldCollapseSender: (
!compactMode
&& prevEventType
&& prevEventType !== 'ignore'
&& (
event.sender === prevEvent.sender
// local echo does not have a sender, will use the current user
|| (!!event.isLocalEcho && !!prevEvent.isLocalEcho)
- || (!!event.isLocalEcho && prevEvent.sender === matrixSdk.userId)
+ || (!!event.isLocalEcho && prevEvent.sender === matrixSession.userId)
)
)
id: layout
RowLayout {
// width: parent.width
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: shouldCollapseSender ? 0 : Kirigami.Units.largeSpacing
Layout.bottomMargin: 0
Kazv.AvatarAdapter {
id: avatar
objectName: 'senderAvatar'
Layout.alignment: Qt.AlignTop
mxcUri: sender.avatarMxcUri
name: senderNickname
visible: !compactMode && !shouldCollapseSender
TapHandler {
onTapped: {
activateUserPage(sender, room, event.sender);
}
}
}
Item {
objectName: 'senderCollapsedPlaceholder'
visible: shouldCollapseSender
Layout.preferredWidth: iconSize
Layout.preferredHeight: 1
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
RowLayout {
visible: !shouldCollapseSender
Kazv.AvatarAdapter {
id: avatarCompact
Layout.preferredWidth: Kirigami.Units.iconSizes.sizeForLabels
Layout.preferredHeight: Kirigami.Units.iconSizes.sizeForLabels
mxcUri: sender.avatarMxcUri
name: senderNickname
visible: compactMode
}
Label {
id: userNicknameText
objectName: 'userNicknameText'
text: senderNickname
Layout.fillWidth: !compactMode
property var reasonableWidth: Math.max(contentWidth, Kirigami.Units.gridUnit)
TapHandler {
enabled: !compactMode
onTapped: {
if (point.position.x <= userNicknameText.reasonableWidth) {
mentionUserRequested(sender.userId);
}
}
}
}
RowLayout {
objectName: 'compactModeSummary'
visible: compactMode
data: [summaryItem]
}
}
RowLayout {
id: container
Layout.fillWidth: true
}
}
}
}
diff --git a/src/contents/ui/event-types/TextTemplate.qml b/src/contents/ui/event-types/TextTemplate.qml
index ef8e9ab..87852c1 100644
--- a/src/contents/ui/event-types/TextTemplate.qml
+++ b/src/contents/ui/event-types/TextTemplate.qml
@@ -1,203 +1,203 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Types
import '..' as Kazv
import '../matrix-helpers.js' as Helpers
Types.Simple {
id: upper
property var text
default property var children
property alias textFormat: label.textFormat
property var linkToActivate
property var selectedUserId
property var ensureMemberEvent: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.member', upper.selectedUserId)
onResolved: (success, data) => {
if (success) {
activateUserPage(room.member(upper.selectedUserId), room);
} else {
// TODO: This opens the matrix.to url directly.
// In a future version this should take you to a window that
// gives you the option to create a DM with that user.
openLink(upper.linkToActivate);
}
}
}
Kazv.Bubble {
id: bubble
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
property var label: Kazv.SelectableText {
objectName: 'textEventContent'
id: label
Layout.fillWidth: true
wrapMode: Text.Wrap
text: upper.text
onLinkActivated: (link) => {
const userId = MK.KazvUtil.matrixLinkUserId(link);
if (userId) {
upper.selectedUserId = userId;
upper.linkToActivate = link;
ensureMemberEvent.call();
} else {
openLink(link);
}
}
onHoveredLinkChanged: (link) => {
if (link) {
// first give it text, then make it visible
label.ToolTip.text = link;
label.ToolTip.visible = true;
} else {
// first make it invisible, then remove the text
label.ToolTip.visible = false;
label.ToolTip.text = '';
}
}
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
}
data: [
...(Array.isArray(upper.children) ? upper.children :
upper.children ? [upper.children] : []),
label
]
}
}
property var isHtmlFormatted: event.content.format === 'org.matrix.custom.html' && event.content.formatted_body
property var openRoomAliasLink: Kazv.AsyncHandler {
property var link
property var joinRoom
- trigger: () => matrixSdk.getRoomIdByAlias(link.identifiers[0])
+ trigger: () => matrixSession.getRoomIdByAlias(link.identifiers[0])
onResolved: {
if (success) {
if (sdkVars.roomList.contains(data.roomId)) {
switchToRoomRequested(data.roomId);
} else {
joinRoom(link.identifiers[0], link.routingServers);
}
} else {
showPassiveNotification(l10n.get('get-room-id-by-alias-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function getMaybeFormattedText() {
if (isHtmlFormatted) {
const stylesheet = `
<style>
del {
text-decoration: line-through;
}
a[href^="https://matrix.to/#/@"] {
background-color: ${Kirigami.Theme.positiveBackgroundColor};
}
</style>
`;
const formattedBody = event.content.formatted_body;
if (event.replyingToEventId && formattedBody.startsWith('<mx-reply>')) {
const index = formattedBody.indexOf('</mx-reply>');
return stylesheet + formattedBody.slice(index + '</mx-reply>'.length);
} else {
return stylesheet + formattedBody;
}
} else {
return event.content.body;
}
}
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md#operations-on-matrix-uris
function openLink(link) {
const matrixLink = MK.KazvUtil.matrixLink(link);
// Not a valid matrix uri
if (!matrixLink.isValid) {
Qt.openUrlExternally(link);
return;
}
const action = matrixLink.action;
const servers = matrixLink.routingServers;
const joinRoom = (roomId, servers) => {
pushJoinRoomPage();
pageStack.currentItem.presetPage(roomId, servers);
};
// Handle link to a user
if (matrixLink.isUser) {
const userId = matrixLink.identifiers[0];
if (action !== 'chat') {
mentionUserRequested(userId);
return;
}
// If the room with the user already exists, switch to the room.
// Otherwise try to create the room.
- for (const roomId of matrixSdk.directRoomIds(userId)) {
+ for (const roomId of matrixSession.directRoomIds(userId)) {
const invitedOrJoined = (roomId) => {
const membership = sdkVars.roomList.room(roomId).membership;
return (membership == MK.MatrixRoom.Invite) || (membership == MK.MatrixRoom.Join);
};
if (sdkVars.roomList.contains(roomId) && invitedOrJoined(roomId)) {
switchToRoomRequested(roomId);
return;
}
}
pushCreateRoomPage();
pageStack.currentItem.presetPage(userId);
return;
}
// Handle link to a event
if (matrixLink.isEvent) {
const roomId = matrixLink.identifiers[0];
const eventId = matrixLink.identifiers[1];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
pageStack.currentItem.delayGoToEvent(eventId, 500);
} else {
joinRoom(roomId, servers);
}
return;
}
// Handle link to a room
if (matrixLink.isRoom) {
// If user has joined the room, open it.
// Otherwise try to join it.
if (matrixLink.isRoomAlias) {
openRoomAliasLink.link = matrixLink;
openRoomAliasLink.joinRoom = joinRoom;
openRoomAliasLink.call();
return;
}
const roomId = matrixLink.identifiers[0];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
} else {
joinRoom(roomId, servers);
}
return;
}
}
}
diff --git a/src/contents/ui/event-types/Video.qml b/src/contents/ui/event-types/Video.qml
index adef4d3..c4031c7 100644
--- a/src/contents/ui/event-types/Video.qml
+++ b/src/contents/ui/event-types/Video.qml
@@ -1,144 +1,144 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtMultimedia
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
import '..' as Kazv
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var videoInfo: event.content.info || {}
property var videoResolution: video.metaData.value(MediaMetaData.Resolution)
property var videoWidth: videoResolution && videoResolution.width || (videoInfo.w || 1)
property var videoHeight: videoResolution && videoResolution.height || (videoInfo.h || 1)
property var videoMaxHeight: compactMode ? Kirigami.Units.gridUnit * 5 : Infinity
property var thumbnailInfo: videoInfo.thumbnail_info || {}
property var thumbnailMxcUri: videoInfo.thumbnail_url
- property var thumbnailUri: matrixSdk.mxcUriToHttp(thumbnailMxcUri)
+ property var thumbnailUri: matrixSession.mxcUriToHttp(thumbnailMxcUri)
property var hasThumbnail: !! thumbnailMxcUri
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
property var jobManager: kazvIOManager
- property var mtxSdk: matrixSdk
+ property var session: matrixSession
summaryItem: Label {
objectName: 'summaryLabel'
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
elide: Text.ElideRight
text: l10n.get('event-summary-video-sent', { gender, body })
}
Kazv.FileHandler {
id: fileHandler
eventContent: event.content
autoCache: true
kazvIOManager: upper.jobManager
- matrixSdk: upper.mtxSdk
+ matrixSession: upper.session
}
Types.MediaBubble {
id: bubble
Layout.fillWidth: true
eventContent: event.content
eventId: event.eventId
ColumnLayout {
id: layout
property var label: Kazv.SelectableText {
objectName: 'bodyLabel'
Layout.fillWidth: true
wrapMode: Text.Wrap
text: l10n.get('event-message-video-sent', { gender, body })
visible: !compactMode
}
property var video: Video {
id: video
objectName: 'video'
source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
autoPlay: !hasAudio
loops: MediaPlayer.Infinite
Layout.fillWidth: true
property double videoWidth: upper.thumbnailWidth || upper.videoWidth || video.implicitWidth || 1
property double videoHeight: upper.thumbnailHeight || upper.videoHeight || video.implicitHeight || 1
property double privHeight: Math.min(videoMaxHeight, videoHeight)
property double privWidth: privHeight / videoHeight * videoWidth
Layout.preferredWidth: privWidth
Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
fillMode: VideoOutput.PreserveAspectFit
TapHandler {
onSingleTapped: {
video.playOrPause();
}
}
Image {
anchors.fill: parent
source: thumbnailUri
visible: video.playbackState == MediaPlayer.StoppedState && hasThumbnail
fillMode: Image.PreserveAspectFit
data: [bubble.mediaFileMenu]
}
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
visible: {
video.playbackState == MediaPlayer.StoppedState &&
! hasThumbnail
}
}
Control {
anchors.centerIn: parent
height: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
width: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
visible: video.playbackState !== MediaPlayer.PlayingState
background: Rectangle {
radius: width / 2
color: Qt.alpha(Kirigami.Theme.backgroundColor, 0.4)
}
contentItem: Kirigami.Icon {
source: 'media-playback-start'
}
}
function playOrPause() {
console.log('playback state ==', video.playbackState);
if (video.playbackState == MediaPlayer.PlayingState) {
video.pause();
} else {
if (video.playbackState == MediaPlayer.StoppedState) {
video.seek(0);
}
video.play();
}
}
}
data: [
label,
video,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml b/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
index 6f0595c..6974671 100644
--- a/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
+++ b/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
@@ -1,80 +1,80 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates 1.0 as KAD
import moe.kazv.mxc.kazv as MK
import '..' as Kazv
import '.' as RoomSettings
import '../matrix-helpers.js' as Helpers
Kirigami.SwipeListItem {
id: upper
property var stickerPack
property var stickerRoomsEvent
text: stickerPack.packName || stickerPack.stateKey
contentItem: KAD.SubtitleContentItem {
itemDelegate: upper
subtitle: stickerPack.packName ? stickerPack.stateKey : ''
}
actions: [
Kirigami.Action {
objectName: 'usePackAction'
text: l10n.get('room-sticker-packs-use-action')
checkable: true
checked: upper.getIsUsingPack()
icon.name: checked ? 'starred-symbolic' : 'non-starred-symbolic'
onTriggered: checked ? upper.usePack() : upper.disusePack()
}
]
function getIsUsingPack() {
return Object.entries(stickerRoomsEvent.content.rooms || {})
.some(([roomId, packs]) =>
roomId === stickerPack.roomId &&
Object.entries(packs).some(([stateKey, _]) =>
stateKey === stickerPack.stateKey
)
);
}
function usePack() {
const content = stickerRoomsEvent.content;
content.rooms = content.rooms || {};
content.rooms[stickerPack.roomId] = content.rooms[stickerPack.roomId] || {};
content.rooms[stickerPack.roomId][stickerPack.stateKey] = {};
send(content);
}
function disusePack() {
const content = stickerRoomsEvent.content;
if (!stickerPack.stateKey in content.rooms?.[stickerPack.roomId]) {
return;
}
delete content.rooms[stickerPack.roomId][stickerPack.stateKey];
send(content);
}
property var sendAccountData: Kazv.AsyncHandler {
- trigger: () => matrixSdk.sendAccountData(Helpers.imagePackRoomsEventType, content);
+ trigger: () => matrixSession.sendAccountData(Helpers.imagePackRoomsEventType, content);
property var content
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('room-sticker-packs-use-failed', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function send(content) {
sendAccountData.content = content;
sendAccountData.call();
}
}
diff --git a/src/contents/ui/room-settings/RoomStickerPacksPage.qml b/src/contents/ui/room-settings/RoomStickerPacksPage.qml
index 847707f..41a5463 100644
--- a/src/contents/ui/room-settings/RoomStickerPacksPage.qml
+++ b/src/contents/ui/room-settings/RoomStickerPacksPage.qml
@@ -1,90 +1,90 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import moe.kazv.mxc.kazv as MK
import '..' as Kazv
import '.' as RoomSettings
import '../matrix-helpers.js' as Helpers
Kazv.ClosableScrollableOverlay {
id: roomStickerPacksPage
property var room
property var stickerPackList: room.stickerPackList()
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomStickerPacksPage.room
}
title: l10n.get('room-sticker-packs-page-title', {
room: roomNameProvider.name,
})
ListView {
model: roomStickerPacksPage.stickerPackList
delegate: RoomSettings.RoomStickerPackItemDelegate {
required property int index
- stickerRoomsEvent: matrixSdk.stickerRoomsEvent()
+ stickerRoomsEvent: matrixSession.stickerRoomsEvent()
stickerPack: stickerPackList.at(index)
}
}
actions: [
Kirigami.Action {
objectName: 'addStickerPackAction'
text: l10n.get('room-sticker-packs-page-add-action')
icon.name: 'list-add'
onTriggered: addStickerPackPopup.open()
}
]
property var addStickerPack: Kazv.AsyncHandler {
id: addStickerPack
trigger: () => {
const type = Helpers.roomImagePackEventType;
const stateKey = stickerPackStateKey.text;
const state = room.state(type, stateKey);
const content = state.content;
content.pack = content.pack || {};
content.pack.display_name = stickerPackName.text;
return roomStickerPacksPage.room.sendStateEvent({
type,
state_key: stateKey,
content,
});
}
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('room-sticker-packs-page-add-success'));
} else {
showPassiveNotification(l10n.get('room-sticker-packs-page-add-failed', { errorMsg: data.error, errorCode: data.errorCode }));
}
}
}
property var addStickerPackPopup: Kirigami.PromptDialog {
id: addStickerPackPopup
title: l10n.get('room-sticker-packs-page-add-popup-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: addStickerPack.call()
Kirigami.FormLayout {
TextField {
id: stickerPackName
objectName: 'stickerPackName'
Kirigami.FormData.label: l10n.get('room-sticker-packs-page-add-popup-name-prompt')
}
TextField {
id: stickerPackStateKey
objectName: 'stickerPackStateKey'
Kirigami.FormData.label: l10n.get('room-sticker-packs-page-add-popup-state-key-prompt')
}
}
}
}
diff --git a/src/contents/ui/settings/ProfileSettings.qml b/src/contents/ui/settings/ProfileSettings.qml
index faa8c15..dddfda2 100644
--- a/src/contents/ui/settings/ProfileSettings.qml
+++ b/src/contents/ui/settings/ProfileSettings.qml
@@ -1,110 +1,110 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '..' as Kazv
ColumnLayout {
id: profileSettings
property var avatarUrl: ''
property var displayName: ''
property var getSelfProfilePromise: undefined
property var loaded: false
Kazv.AvatarAdapter {
id: avatar
objectName: 'avatar'
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: Kirigami.Units.iconSizes.enormous
Layout.preferredWidth: Kirigami.Units.iconSizes.enormous
mxcUri: profileSettings.avatarUrl
}
property var saveAvatarUrl: Kazv.AsyncHandler {
trigger: () => {
profileSettings.loaded = false;
- return matrixSdk.setAvatarUrl(profileSettings.avatarUrl);
+ return matrixSession.setAvatarUrl(profileSettings.avatarUrl);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('settings-profile-save-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
profileSettings.loaded = true;
}
}
property var uploadFileHelper: Kazv.UploadFileHelper {
onUploaded: {
profileSettings.avatarUrl = mxcUri;
saveAvatarUrl.call();
}
onFailed: showPassiveNotification(l10n.get('kazv-io-upload-failure-prompt'))
}
Button {
Layout.fillWidth: true
text: l10n.get('settings-profile-change-avatar')
enabled: profileSettings.loaded
onClicked: profileSettings.uploadFileHelper.chooseFileAndUpload()
}
RowLayout {
Layout.fillWidth: true
Label {
text: l10n.get('settings-profile-display-name')
}
TextField {
Layout.fillWidth: true
id: displayNameEntry
objectName: 'displayNameEntry'
text: profileSettings.displayName
enabled: profileSettings.loaded
}
}
property var getSelfPromise: Kazv.AsyncHandler {
- trigger: () => matrixSdk.getSelfProfile()
+ trigger: () => matrixSession.getSelfProfile()
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('settings-profile-load-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
profileSettings.displayName = data.displayName;
profileSettings.avatarUrl = data.avatarUrl;
profileSettings.loaded = true;
}
}
}
Component.onCompleted: {
getSelfPromise.call();
}
property var saveDisplayName: Kazv.AsyncHandler {
trigger: () => {
profileSettings.loaded = false;
- return matrixSdk.setDisplayName(displayNameEntry.text);
+ return matrixSession.setDisplayName(displayNameEntry.text);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('settings-profile-save-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
profileSettings.loaded = true;
}
}
function save() {
if (displayNameEntry.text !== profileSettings.displayName) {
saveDisplayName.call();
}
}
}
diff --git a/src/contents/ui/settings/SecuritySettings.qml b/src/contents/ui/settings/SecuritySettings.qml
index f4d1e4e..1824858 100644
--- a/src/contents/ui/settings/SecuritySettings.qml
+++ b/src/contents/ui/settings/SecuritySettings.qml
@@ -1,68 +1,68 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Dialogs
import org.kde.kirigami as Kirigami
import '..' as Kazv
ColumnLayout {
id: securitySettings
property string fileToImport: ''
Button {
objectName: 'importKeysButton'
Layout.fillWidth: true
text: l10n.get('settings-security-import-keys-action')
onClicked: fileDialog.open()
}
FileDialog {
id: fileDialog
objectName: 'fileDialog'
onAccepted: {
securitySettings.fileToImport = selectedFile;
// reset state of passwordInput before showing
passwordInput.text = '';
passwordInput.showPassword = false;
passwordInputPopup.open();
}
}
property var passwordInputOverlay: Kazv.ConfirmationOverlay {
id: passwordInputPopup
parent: Overlay.overlay
title: l10n.get('settings-security-import-keys-password-input-title')
message: l10n.get('settings-security-import-keys-password-input-label')
confirmActionText: l10n.get('settings-security-import-keys-confirm-action')
cancelActionText: l10n.get('settings-security-import-keys-cancel-action')
Kirigami.PasswordField {
id: passwordInput
objectName: 'passwordInput'
Layout.fillWidth: true
}
onAccepted: {
securitySettings.importKeys.call();
}
}
property var importKeys: Kazv.AsyncHandler {
trigger: () => {
const password = passwordInput.text;
passwordInput.text = '';
- return matrixSdk.importFromKeyBackupFile(securitySettings.fileToImport, password);
+ return matrixSession.importFromKeyBackupFile(securitySettings.fileToImport, password);
}
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('settings-security-import-keys-success-notification', { imported: data.imported }));
} else {
showPassiveNotification(l10n.get('settings-security-import-keys-failure-notification', { errorMsg: data.error, errorCode: data.errorCode }));
}
}
}
}
diff --git a/src/db-store.cpp b/src/db-store.cpp
index f69ed7c..1924904 100644
--- a/src/db-store.cpp
+++ b/src/db-store.cpp
@@ -1,368 +1,368 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include "db-store.hpp"
-#include "matrix-sdk.hpp"
+#include "matrix-session-controller.hpp"
#include "event.hpp"
#include "kazv-log.hpp"
#include <QPromise>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QCoroFuture>
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
QString DbStore::getHandleFor(std::string userId, std::string deviceId)
{
return QString::fromStdString(userId + "/" + deviceId);
}
std::optional<Kazv::Event> DbStore::resultToEvent(bool encrypted, bool decrypted, QString originalJson, QString decryptedJson)
{
Event e;
try {
auto oj = json::parse(std::move(originalJson).toStdString());
e = Event(JsonWrap(oj));
} catch (const json::parse_error &) {
return std::nullopt;
}
try {
if (encrypted) {
auto dj = json::parse(std::move(decryptedJson).toStdString());
e = std::move(e).setDecryptedJson(
JsonWrap(dj),
decrypted ? Event::Decrypted : Event::NotDecrypted
);
}
return e;
} catch (const json::parse_error &) {
return e;
}
}
DbStore::DbStore()
: m_thread(new QThread)
, m_obj(new QObject)
, m_d(std::nullopt)
, m_handle()
{
m_obj->moveToThread(m_thread);
}
DbStore::~DbStore()
{
cleanup();
}
QCoro::Task<std::pair<bool, QString>> DbStore::setup(std::string userDataDir, std::string userId, std::string deviceId)
{
m_handle = getHandleFor(userId, deviceId);
m_d = QSqlDatabase::addDatabase(u"QSQLITE"_s, m_handle);
auto dbFullDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId) / dbDirName;
std::error_code err;
if ((! std::filesystem::create_directories(dbFullDir, err))
&& err) {
qCWarning(kazvLog) << "DbStore: setup: cannot create db dir:" << dbFullDir.string() << ", error:" << err.message();
co_return {false, u"Cannot create db dir"_s};
}
auto dbName = QString::fromStdString((dbFullDir / dbBaseName).string());
qCDebug(kazvLog) << "DbStore: setup: data file name" << dbName;
m_d->setDatabaseName(dbName);
bool status = m_d->open();
if (!status) {
qCWarning(kazvLog) << "DbStore: Cannot open database";
std::pair<bool, QString> res = {status, m_d->lastError().text()};
m_d.reset();
QSqlDatabase::removeDatabase(m_handle);
co_return res;
}
qCDebug(kazvLog) << "DbStore: Opened database";
m_d->moveToThread(m_thread);
m_thread->start();
qCDebug(kazvLog) << "DbStore: Running database migrations";
auto res = co_await migrate();
co_return res;
}
QCoro::Task<std::pair<bool, QString>> DbStore::importAllFrom(const Kazv::SdkModel &model)
{
std::size_t count = 0;
for (const auto &[roomId, room] : model.c().roomList.rooms) {
auto roomIdQs = QString::fromStdString(roomId);
for (const auto &[eventId, event] : room.messages) {
auto res = co_await importOne(roomIdQs, event, /* isInTimeline = */ true);
if (!res.first) {
qCWarning(kazvLog) << "Error inserting value:" << res.second;
co_return res;
}
++count;
if (count % 100 == 0) {
qCInfo(kazvLog) << "Imported" << count << "events";
}
}
}
co_return {true, QString()};
}
QCoro::Task<std::pair<bool, QString>> DbStore::saveEvents(Kazv::SaveEventsRequested s)
{
for (const auto &[roomId, el] : s.timelineEvents) {
auto roomIdQs = QString::fromStdString(roomId);
for (const auto &event : el) {
auto res = co_await importOne(roomIdQs, event, /* isInTimeline = */ true);
if (!res.first) {
qCWarning(kazvLog) << "Error inserting value:" << res.second;
co_return res;
}
}
}
for (const auto &[roomId, el] : s.nonTimelineEvents) {
auto roomIdQs = QString::fromStdString(roomId);
for (const auto &event : el) {
auto res = co_await importOne(roomIdQs, event, /* isInTimeline = */ false);
if (!res.first) {
qCWarning(kazvLog) << "Error inserting value:" << res.second;
co_return res;
}
}
}
co_return {true, QString()};
}
inline const auto getEventByIdQuery = uR"xxx(SELECT
encrypted, decrypted,
original_json, decrypted_json, is_in_timeline
FROM events WHERE
room_id = :room_id AND event_id = :event_id
;)xxx"_s;
QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> DbStore::getEventById(QString roomId, QString eventId)
{
QPromise<std::optional<std::pair<Kazv::Event, bool>>> p;
auto fut = p.future();
QMetaObject::invokeMethod(m_obj, [this, p=std::move(p), roomId, eventId]() mutable {
QSqlQuery q(m_d.value());
auto res = q.prepare(getEventByIdQuery);
if (!res) {
p.addResult(std::nullopt);
p.finish();
return;
}
q.bindValue(u":room_id"_s, roomId);
q.bindValue(u":event_id"_s, eventId);
res = q.exec();
if (!res || !q.next()) {
p.addResult(std::nullopt);
p.finish();
return;
}
auto encrypted = q.value(0).toBool();
auto decrypted = q.value(1).toBool();
auto originalJson = q.value(2).toString();
auto decryptedJson = q.value(3).toString();
auto isInTimeline = q.value(4).toBool();
auto eventOpt = resultToEvent(encrypted, decrypted, std::move(originalJson), std::move(decryptedJson));
auto ret = eventOpt
? std::optional<std::pair<Event, bool>>(
{eventOpt.value(), isInTimeline}
) : std::nullopt;
p.addResult(ret);
p.finish();
});
co_return co_await fut;
}
inline const auto getEventsBeforeQuery = uR"xxx(SELECT
encrypted, decrypted,
original_json, decrypted_json, is_in_timeline
FROM events WHERE
room_id = :room_id AND (
origin_server_ts < :ref_ts OR (
origin_server_ts = :ref_ts AND event_id < :ref_event_id))
ORDER BY origin_server_ts DESC, event_id DESC
LIMIT :limit
;)xxx"_s;
auto DbStore::getEventsBefore(QString roomId, QString eventId, std::size_t limit) -> QCoro::Task<GetEventsBeforeResult>
{
auto getByIdRes = co_await getEventById(roomId, eventId);
if (!getByIdRes.has_value()) {
// reference event not in database
co_return {};
}
auto refEventTs = std::move(getByIdRes).value().first.originServerTs();
QPromise<GetEventsBeforeResult> p;
auto fut = p.future();
QMetaObject::invokeMethod(m_obj, [this, p=std::move(p), roomId, refEventTs, eventId, limit]() mutable {
QSqlQuery q(m_d.value());
auto res = q.prepare(getEventsBeforeQuery);
if (!res) {
p.addResult(GetEventsBeforeResult{});
p.finish();
return;
}
q.bindValue(u":room_id"_s, roomId);
q.bindValue(u":ref_ts"_s, static_cast<qint64>(refEventTs));
q.bindValue(u":ref_event_id"_s, eventId);
q.bindValue(u":limit"_s, static_cast<qint64>(limit));
res = q.exec();
if (!res) {
p.addResult(GetEventsBeforeResult{});
p.finish();
return;
}
EventList::transient_type tlEvents;
EventList::transient_type nonTlEvents;
while (q.next()) {
auto encrypted = q.value(0).toBool();
auto decrypted = q.value(1).toBool();
auto originalJson = q.value(2).toString();
auto decryptedJson = q.value(3).toString();
auto isInTimeline = q.value(4).toBool();
auto eventOpt = resultToEvent(encrypted, decrypted, originalJson, std::move(decryptedJson));
if (!eventOpt) {
qCWarning(kazvLog) << "event in database is malformed:" << originalJson;
continue;
}
if (isInTimeline) {
tlEvents.push_back(eventOpt.value());
} else {
nonTlEvents.push_back(eventOpt.value());
}
}
auto roomIdStd = roomId.toStdString();
// sqlite returns in DESC order, but we want ASC in timeline order
auto ret = GetEventsBeforeResult{
{{roomIdStd, intoImmer(EventList{}, zug::reversed, std::move(tlEvents))}},
{{roomIdStd, intoImmer(EventList{}, zug::reversed, std::move(nonTlEvents))}},
};
p.addResult(ret);
p.finish();
});
co_return co_await fut;
}
bool DbStore::valid() const
{
return m_d.has_value();
}
void DbStore::cleanup()
{
if (m_d.has_value()) {
m_d->close();
m_d.reset();
QSqlDatabase::removeDatabase(m_handle);
m_handle = QString();
}
QMetaObject::invokeMethod(m_obj, [thread=m_thread]() {
thread->quit();
});
m_thread->wait();
m_thread->deleteLater();
m_obj->deleteLater();
}
QCoro::Task<std::pair<bool, QString>> DbStore::migrate()
{
auto res = co_await asyncQuery(uR"xxx(CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY NOT NULL,
room_id TEXT NOT NULL,
event_id TEXT NOT NULL,
encrypted INTEGER NOT NULL,
decrypted INTEGER NOT NULL,
original_json TEXT NOT NULL,
decrypted_json TEXT,
is_in_timeline INTEGER NOT NULL,
origin_server_ts INTEGER NOT NULL GENERATED ALWAYS AS (coalesce(CAST(original_json->>'origin_server_ts' AS INTEGER), 0)) STORED
);)xxx"_s);
if (!res.first) { co_return res; }
res = co_await asyncQuery(uR"xxx(CREATE UNIQUE INDEX IF NOT EXISTS events_room_id_event_id_index
ON events (
room_id,
event_id
);)xxx"_s);
if (!res.first) { co_return res; }
res = co_await asyncQuery(uR"xxx(CREATE INDEX IF NOT EXISTS events_room_id_index
ON events (
room_id
);)xxx"_s);
if (!res.first) { co_return res; }
res = co_await asyncQuery(uR"xxx(CREATE INDEX IF NOT EXISTS events_order_index
ON events (origin_server_ts, event_id)
)xxx"_s);
co_return res;
}
static const auto importEventQuery = uR"xxx(INSERT INTO events (
room_id, event_id, encrypted, decrypted,
original_json, decrypted_json, is_in_timeline
) VALUES (
:room_id, :event_id, :encrypted, :decrypted,
:original_json, :decrypted_json, :is_in_timeline
) ON CONFLICT (room_id, event_id) DO UPDATE SET
encrypted = :encrypted, decrypted = :decrypted,
original_json = :original_json, decrypted_json = :decrypted_json,
is_in_timeline = is_in_timeline OR :is_in_timeline
;)xxx"_s;
QCoro::Task<std::pair<bool, QString>> DbStore::importOne(const QString &roomId, const Kazv::Event &event, bool isInTimeline)
{
auto eventIdQs = QString::fromStdString(event.id());
auto encrypted = event.encrypted();
auto decrypted = event.decrypted();
auto originalJson = QString::fromStdString(event.originalJson().get().dump());
QVariant decryptedJson = event.encrypted() ? QString::fromStdString(event.decryptedJson().get().dump()) : QVariant(QMetaType::fromType<QString>());
auto tf = [=](QSqlQuery &q) {
q.bindValue(u":room_id"_s, roomId);
q.bindValue(u":event_id"_s, eventIdQs);
q.bindValue(u":encrypted"_s, encrypted);
q.bindValue(u":decrypted"_s, decrypted);
q.bindValue(u":original_json"_s, originalJson);
q.bindValue(u":decrypted_json"_s, decryptedJson);
q.bindValue(u":is_in_timeline"_s, isInTimeline);
};
co_return co_await asyncQuery(importEventQuery, tf);
}
QCoro::Task<std::pair<bool, QString>> DbStore::asyncQuery(const QString &qs, std::function<void(QSqlQuery &)> tf)
{
// This function exists because QSqlQuery must be used in the thread where
// the QSqlDatabase is in.
// We invoke the query inside the thread of the database, then use the
// promise-future protocol to pass the result back as a coroutine.
QPromise<std::pair<bool, QString>> p;
auto fut = p.future();
QMetaObject::invokeMethod(m_obj, [this, p=std::move(p), s=std::move(qs), tf]() mutable {
p.start();
QSqlQuery q(m_d.value());
auto res = q.prepare(std::move(s));
if (!res) {
qCWarning(kazvLog) << "DbStore: Async Query Prepare Failed: Driver:" << q.lastError().driverText();
qCWarning(kazvLog) << "DbStore: Async Query Prepare Failed: Db:" << q.lastError().databaseText();
p.addResult({false, q.lastError().text()});
p.finish();
return;
}
tf(q);
res = q.exec();
if (!res) {
qCWarning(kazvLog) << "DbStore: Async Query Failed: Driver:" << q.lastError().driverText();
qCWarning(kazvLog) << "DbStore: Async Query Failed: Db:" << q.lastError().databaseText();
p.addResult({false, q.lastError().text()});
} else {
p.addResult({true, QString()});
}
p.finish();
});
co_return co_await fut;
}
diff --git a/src/db-store.hpp b/src/db-store.hpp
index fcf98a1..fdd32f6 100644
--- a/src/db-store.hpp
+++ b/src/db-store.hpp
@@ -1,100 +1,101 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+#pragma once
#include <kazv-defs.hpp>
#include <tuple>
#include <functional>
#include <QString>
#include <QThread>
#include <QFuture>
#include <QSqlDatabase>
#include <QCoroTask>
#include <sdk-model.hpp>
#include <kazvevents.hpp>
/**
* This implements the event storage system using a relational database.
*
* The functions in this class are implemented as async because we do not
* want to block the calling thread. Just like in MatrixSdk, we create a
* dedicated thread for the database, and perform all work there.
* Results are sent back using coroutines as they are easier to use than QFuture.
*/
class DbStore
{
public:
static inline constexpr std::string dbDirName{"database"};
static inline constexpr std::string dbBaseName{"db.sqlite3"};
static QString getHandleFor(std::string userId, std::string deviceId);
static std::optional<Kazv::Event> resultToEvent(bool encrypted, bool decrypted, QString originalJson, QString decryptedJson);
DbStore();
~DbStore();
/**
* Open the database for a specific session.
* @param userDataDir The user data dir from MatrixSdk.
* @param userId The user id.
* @param deviceId The device id.
* @return A pair of (status, error text). status is true iff the setup is successful. error text contains the error text reported by the database.
*/
QCoro::Task<std::pair<bool, QString>> setup(std::string userDataDir, std::string userId, std::string deviceId);
/// @return Whether the DbStore is valid
bool valid() const;
public Q_SLOTS:
/**
* Import all events from a model.
* @param model The model.
*/
QCoro::Task<std::pair<bool, QString>> importAllFrom(const Kazv::SdkModel &model);
/**
* Save events from a SaveEventsRequested trigger.
*
* @param s The SaveEventsRequested trigger.
*/
QCoro::Task<std::pair<bool, QString>> saveEvents(Kazv::SaveEventsRequested s);
/**
* Get one event from the database.
*
* @param roomId The room id of the event.
* @param eventId The id of the event.
* @return A pair of (Event, isInTimeline) associated with the ids or std::nullopt if it is not found.
*/
QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> getEventById(QString roomId, QString eventId);
struct GetEventsBeforeResult
{
immer::map<std::string, Kazv::EventList> timelineEvents;
immer::map<std::string, Kazv::EventList> relatedEvents;
};
/**
* Get events before a certain event id.
*
* Up to `limit` events sorted before the reference event will be returned.
* @param roomId The room id for which to get events.
* @param eventId The event id of the reference event.
* @param limit The maximum number of timeline events to return.
* @return A tuple-ish, the content of which is to be passed to
* Kazv::Client::loadEventsFromStorage() .
*/
QCoro::Task<GetEventsBeforeResult> getEventsBefore(QString roomId, QString eventId, std::size_t limit = 10);
private:
void cleanup();
QCoro::Task<std::pair<bool, QString>> migrate();
QCoro::Task<std::pair<bool, QString>> importOne(const QString &roomId, const Kazv::Event &event, bool isInTimeline);
QCoro::Task<std::pair<bool, QString>> asyncQuery(const QString &qs, std::function<void(QSqlQuery &)> tf = [](auto &) {});
QThread *m_thread;
QObject *m_obj;
std::optional<QSqlDatabase> m_d;
QString m_handle;
};
diff --git a/src/helper.hpp b/src/helper.hpp
index a60fc97..ce346fe 100644
--- a/src/helper.hpp
+++ b/src/helper.hpp
@@ -1,74 +1,85 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <immer/array.hpp>
#include <QString>
#include <QStringList>
-
+#include <QObject>
#include <zug/transducer/map.hpp>
#include <cursorutil.hpp>
#include <device-list-tracker.hpp>
namespace Kazv
{
namespace detail
{
struct StrToQtT
{
template<class T>
auto operator()(T &&x) {
return QString::fromStdString(std::forward<T>(x));
}
};
struct QStringToStdT
{
template<class T>
auto operator()(T &&x) {
return std::forward<T>(x).toStdString();
}
};
struct ContainerSizeT
{
template<class T>
auto operator()(T &&x) -> int {
return std::forward<T>(x).size();
}
};
}
}
constexpr auto strToQt = zug::map(::Kazv::detail::StrToQtT{});
constexpr auto strListToQtF = [](auto container) {
return zug::into(QStringList{}, strToQt, std::move(container));
};
constexpr auto strListToQt = zug::map(strListToQtF);
constexpr auto qStringToStd = zug::map(::Kazv::detail::QStringToStdT{});
constexpr auto qStringListToStdF = [](auto container) {
return Kazv::intoImmer(immer::array<std::string>{}, qStringToStd, std::move(container));
};
constexpr auto containerSize = zug::map(::Kazv::detail::ContainerSizeT{});
QString trustLevelToQStringFunc(Kazv::DeviceTrustLevel trustLevel);
Kazv::DeviceTrustLevel qStringToTrustLevelFunc(QString trustLevel);
constexpr auto trustLevelToQString = zug::map(trustLevelToQStringFunc);
constexpr auto qStringToTrustLevel = zug::map(qStringToTrustLevelFunc);
inline auto roomMemberToAvatar = Kazv::eventContent | Kazv::jsonAtOr(std::string("avatar_url"), std::string(""));
+
+struct QObjectDeleterDeleteLater
+{
+ void operator()(QObject *obj) const
+ {
+ obj->deleteLater();
+ }
+};
+
+template<class T>
+using UniquePtrDL = std::unique_ptr<T, QObjectDeleterDeleteLater>;
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index f8205ee..f1350c6 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,1234 +1,210 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
-#include <boost/archive/text_oarchive.hpp>
-#include <boost/archive/text_iarchive.hpp>
-#include <fstream>
-#include <filesystem>
-#include <chrono>
-
-#include <QMutex>
-#include <QMutexLocker>
#include <QtConcurrent>
-#include <QThreadPool>
-#include <KConfig>
-#include <KConfigGroup>
-#include <QCoroTask>
-#include <eventemitter/lagerstoreeventemitter.hpp>
-#include <client/sdk.hpp>
#include <client/notification-handler.hpp>
#include <crypto/base64.hpp>
-#include <csapi/directory.hpp>
-#include <client/alias.hpp>
-#include <csapi/login.hpp>
#include <zug/util.hpp>
#include <lager/event_loop/qt.hpp>
#include "matrix-sdk.hpp"
-#include "matrix-room-list.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
+#include "matrix-session-types.hpp"
#include "helper.hpp"
#include "kazv-path-config.hpp"
#include "kazv-version.hpp"
#include "qt-json.hpp"
#include "qt-rand-adapter.hpp"
-#include "qt-promise-handler.hpp"
#include "qt-job-handler.hpp"
-#include "device-mgmt/matrix-device-list.hpp"
-#include "matrix-sticker-pack-list.hpp"
-#include "matrix-sticker-pack-list-p.hpp"
-#include "matrix-user-given-attrs-map.hpp"
#include "kazv-log.hpp"
#include "matrix-utils.hpp"
+#include "matrix-session-controller.hpp"
#include "kazv-session-lock-guard.hpp"
#include "db-store.hpp"
-#include "sso-login-process.hpp"
-#include "matrix-verification-list.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
-static const std::string clientName = "kazv";
-
-// Sdk with qt event loop, identity transform and no enhancers
-using SdkT =
- decltype(makeSdk(
- SdkModel{},
- detail::declref<JobInterface>(),
- detail::declref<EventInterface>(),
- QtPromiseHandler(detail::declref<QObject>()),
- zug::identity,
- withRandomGenerator(detail::declref<RandomInterface>())));
-
-struct QtEventLoop
-{
- QObject *m_obj;
-
- template<class Fn>
- void async(Fn &&) { throw std::runtime_error{"not implemented!"}; }
-
- template<class Fn>
- void post(Fn &&fn)
- {
- QMetaObject::invokeMethod(
- m_obj, std::forward<Fn>(fn), Qt::QueuedConnection
- );
- }
-
- void finish() {}
- void pause() { throw std::runtime_error{"not implemented!"}; }
- void resume() { throw std::runtime_error{"not implemented!"}; }
-};
-
-QCoro::Task<std::pair<bool, QString>> setupAndImport(SdkModel model, DbStore *dbStore, std::string userDataDir)
-{
- auto res = co_await dbStore->setup(
- userDataDir,
- model.c().userId,
- model.c().deviceId
- );
- if (!res.first) {
- co_return res;
- }
- co_return co_await dbStore->importAllFrom(model);
-}
-
-std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId)
-{
- auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
- auto sessionDir = userDataDir / "sessions"
- / encodedUserId / deviceId;
- return sessionDir;
-}
-
-struct PendingSaveEvents : public SaveEventsRequested
+struct MatrixSdkPrivate
{
- using DataT = immer::map<std::string, EventList>;
- void add(SaveEventsRequested s)
+ MatrixSdkPrivate(MatrixSdk *q)
+ : q(q)
+ , userDataDir(kazvUserDataDir().toStdString())
+ , thread(new QThread())
+ , controller(new MatrixSessionController(thread, userDataDir))
{
- timelineEvents = addOneType(std::move(timelineEvents), s.timelineEvents);
- nonTimelineEvents = addOneType(std::move(nonTimelineEvents), s.nonTimelineEvents);
}
- static DataT addOneType(DataT orig, DataT addon)
+ ~MatrixSdkPrivate()
{
- for (const auto &[roomId, el]: addon) {
- orig = std::move(orig).update(roomId, [&el](EventList origEl) {
- // This works without explicit dedupe because we do not do parallel
- // inserts and the insert order is the same as list order.
- return origEl + el;
- });
+ if (controller) {
+ controller->prepareDestroy();
+ controller->save();
}
- return orig;
+ thread->quit();
+ thread->wait();
+ thread->deleteLater();
}
-};
-enum DbStatus
-{
- Pending,
- Ready,
- Failed,
-};
-
-struct MatrixSdkPrivate
-{
- MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore);
- MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore);
- bool testing;
+ MatrixSdk *q;
std::string userDataDir;
- std::unique_ptr<KazvSessionLockGuard> lockGuard;
- RandomInterface randomGenerator;
- std::unique_ptr<DbStore> dbStore;
QThread *thread;
- QObject *obj;
- QtJobHandler *jobHandler;
- LagerStoreEventEmitter ee;
- LagerStoreEventEmitter::Watchable watchable;
- SdkT sdk;
+ UniquePtrDL<MatrixSessionController> controller;
QTimer saveTimer;
- QPointer<SsoLoginProcess> ssoLoginProcess{};
- using SecondaryRootT = decltype(sdk.createSecondaryRoot(std::declval<lager::with_qt_event_loop>()));
- SecondaryRootT secondaryRoot;
- Client clientOnSecondaryRoot;
- NotificationHandler notificationHandler;
- DbStatus dbStatus;
- PendingSaveEvents pendingSaveEvents{};
- lager::state<VerificationTrackerModel> verificationTrackerState;
+ void emplaceController()
+ {
+ if (controller) {
+ controller->prepareDestroy();
+ controller->save();
+ }
+ controller.reset(new MatrixSessionController(thread, userDataDir));
+ Q_EMIT q->sessionChanged();
+ QObject::connect(controller.get(), &MatrixSessionController::loadSessionFinished, q, &MatrixSdk::loadSessionFinished);
+ QObject::connect(controller.get(), &MatrixSessionController::sessionReady, q, &MatrixSdk::sessionChanged);
+ }
void runIoContext() {
thread->start();
}
void stopIoContext() {
thread->quit();
}
void maybeSerialize()
{
- if (!testing) {
- serializeClientToFile(clientOnSecondaryRoot);
- }
- }
-
- void serializeClientToFile(Client c);
-
- void saveOrQueueEvents(SaveEventsRequested s)
- {
- if (dbStatus == Ready) {
- saveEvents(std::move(s));
- } else if (dbStatus == Pending) {
- pendingSaveEvents.add(std::move(s));
- }
- // Database open failed, do not do anything
- }
-
- void saveEvents(SaveEventsRequested s)
- {
- dbStore->saveEvents(std::move(s))
- .then([](const auto &stat) {
- if (!stat.first) {
- qCWarning(kazvLog) << "Cannot save events:" << stat.second;
- }
- });
- }
-};
-
-// Cleaning up notes:
-// 0. Callback functions may store the context for an indefinite time
-// 1. The QThread event loop can be stopped
-// 2. QtJobHandler::submit() should only happen in the primary event loop thread
-// 3. QtJobHandler lives in the primary event loop thread
-// 4. QtJobs live in the primary event loop thread
-// 5. Job callbacks are called in the primary event loop thread
-// 6. QtPromise::then() callbacks are called in the primary event loop thread
-// 7. When the QThread event loop stops, no more callbacks will be executed (there is nothing to post to)
-// 8. The QThread should stop before obj is deleted
-class CleanupHelper : public QObject
-{
- Q_OBJECT
-public:
- explicit CleanupHelper(std::unique_ptr<MatrixSdkPrivate> d)
- : oldD(std::move(d))
- {
- }
-
- std::unique_ptr<MatrixSdkPrivate> oldD;
-
- void cleanup()
- {
- qCInfo(kazvLog) << "start to clean up everything";
- if (oldD->ssoLoginProcess) {
- oldD->ssoLoginProcess->deleteLater();
- }
-
- oldD->clientOnSecondaryRoot.stopSyncing()
- .then([obj=oldD->obj, thread=oldD->thread](auto &&) {
- qCDebug(kazvLog) << "stopped syncing";
- QMetaObject::invokeMethod(obj, [thread]() {
- thread->quit();
- });
- });
- oldD->thread->wait();
- oldD->thread->deleteLater();
- // After the thread's event loop is finished, we can delete the root object
- oldD->obj->deleteLater();
- this->deleteLater();
- qCInfo(kazvLog) << "thread is done";
+ controller->save();
}
};
-void MatrixSdkPrivate::serializeClientToFile(Client c)
-{
- using namespace Kazv::CursorOp;
- auto userId = +c.userId();
- auto deviceId = +c.deviceId();
- if (userId.empty() || deviceId.empty()) {
- qDebug() << "Not logged in, nothing to serialize";
- return;
- }
-
- using StdPath = std::filesystem::path;
- auto userDataDir = StdPath(this->userDataDir);
- auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
-
- auto storeFile = sessionDir / "store";
- auto storeFileNew = sessionDir / "store.new";
-
- auto metadataFile = sessionDir / "metadata";
-
- qDebug() << "storeFile=" << QString::fromStdString(storeFile.string());
-
- std::error_code err;
- if ((! std::filesystem::create_directories(sessionDir, err))
- && err) {
- qDebug() << "Unable to create sessionDir";
- return;
- }
-
- if (!lockGuard) {
- try {
- lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
- } catch (const std::runtime_error &e) {
- qCWarning(kazvLog) << "Error locking session: " << e.what();
- return;
- }
- }
-
- try {
- auto storeStream = std::ofstream(storeFileNew);
- if (! storeStream) {
- qCWarning(kazvLog) << "Unable to open storeFile";
- return;
- }
- using OAr = boost::archive::text_oarchive;
- auto archive = OAr{storeStream};
- c.serializeTo(archive);
- } catch (const std::exception &e) {
- qCWarning(kazvLog) << "Cannot write to store file: " << e.what();
- return;
- }
-
- err.clear();
- std::filesystem::rename(storeFileNew, storeFile, err);
- if (err) {
- qCWarning(kazvLog) << "Cannot move storeFile into place: " << QString::fromStdString(err.message());
- return;
- }
- qDebug() << "Serialization done";
-
-
- // store metadata
- {
- KConfig metadata(QString::fromStdString(metadataFile.string()));
- KConfigGroup mdGroup(&metadata, u"Metadata"_s);
- mdGroup.writeEntry("kazvVersion", QString::fromStdString(kazvVersionString()));
- mdGroup.writeEntry("archiveFormat", "text-with-sqlite");
- }
-}
-
-MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore)
- : testing(testing)
- , userDataDir{kazvUserDataDir().toStdString()}
- , lockGuard(std::move(lockGuard))
- , randomGenerator(QtRandAdapter{})
- , dbStore(std::move(dbStore))
- , thread(new QThread())
- , obj(new QObject())
- , jobHandler(new QtJobHandler(obj))
- , ee{QtEventLoop{obj}}
- , watchable(ee.watchable())
- , sdk(makeDefaultSdkWithCryptoRandom(
- randomGenerator.generateRange<std::string>(makeDefaultSdkWithCryptoRandomSize()),
- static_cast<JobInterface &>(*jobHandler),
- static_cast<EventInterface &>(ee),
- QtPromiseHandler(*obj),
- zug::identity,
- withRandomGenerator(randomGenerator)))
- , secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{q}))
- , clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
- , notificationHandler(clientOnSecondaryRoot.notificationHandler())
- , dbStatus(this->dbStore ? Ready : Pending)
-{
- obj->moveToThread(thread);
-}
-
-MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore)
- : testing(testing)
- , userDataDir{kazvUserDataDir().toStdString()}
- , lockGuard(std::move(lockGuard))
- , randomGenerator(QtRandAdapter{})
- , dbStore(std::move(dbStore))
- , thread(new QThread())
- , obj(new QObject())
- , jobHandler(new QtJobHandler(obj))
- , ee{QtEventLoop{obj}}
- , watchable(ee.watchable())
- , sdk(makeSdk(
- model,
- static_cast<JobInterface &>(*jobHandler),
- static_cast<EventInterface &>(ee),
- QtPromiseHandler(*obj),
- zug::identity,
- withRandomGenerator(randomGenerator)))
- , secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{q}, std::move(model)))
- , clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
- , notificationHandler(clientOnSecondaryRoot.notificationHandler())
- , dbStatus(this->dbStore ? Ready : Pending)
-{
- obj->moveToThread(thread);
-}
-
MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent)
: QObject(parent)
, m_d(std::move(d))
{
init();
- connect(this, &MatrixSdk::trigger,
- this, [this](KazvEvent e) {
- if (std::holds_alternative<SaveEventsRequested>(e)) {
- auto s = std::get<SaveEventsRequested>(e);
- m_d->saveOrQueueEvents(s);
- }
- });
-
- // loginSuccessful will be emitted only for new sessions (not for loaded
- // sessions),
- connect(this, &MatrixSdk::loginSuccessful, this, [this]() {
- if (m_d->dbStore) {
- qCWarning(kazvLog) << "Trying to replace current db store, ignoring";
- return;
- }
- m_d->dbStore = std::make_unique<DbStore>();
- // This must be called from the GUI thread
- auto model = m_d->secondaryRoot.get();
- setupAndImport(std::move(model), m_d->dbStore.get(), m_d->userDataDir)
- .then([this](const auto &stat) {
- if (!stat.first) {
- qCWarning(kazvLog) << "Cannot set up database:" << stat.second;
- }
- Q_EMIT dbResolved(stat.first);
- });
- });
-
- connect(this, &MatrixSdk::dbResolved, this, [this](bool success) {
- if (!success) {
- m_d->dbStatus = Failed;
- } else {
- qCDebug(kazvLog) << "db loading succeeded";
- auto pendingSaveEvents = std::move(m_d->pendingSaveEvents);
- m_d->pendingSaveEvents = {};
- m_d->saveEvents(std::move(pendingSaveEvents));
- m_d->dbStatus = Ready;
- }
- });
}
void MatrixSdk::init()
{
- LAGER_QT(serverUrl) = m_d->clientOnSecondaryRoot.serverUrl().xform(strToQt); Q_EMIT serverUrlChanged(serverUrl());
- LAGER_QT(userId) = m_d->clientOnSecondaryRoot.userId().xform(strToQt); Q_EMIT userIdChanged(userId());
- LAGER_QT(token) = m_d->clientOnSecondaryRoot.token().xform(strToQt); Q_EMIT tokenChanged(token());
- LAGER_QT(deviceId) = m_d->clientOnSecondaryRoot.deviceId().xform(strToQt); Q_EMIT deviceIdChanged(deviceId());
- LAGER_QT(specVersions) = m_d->clientOnSecondaryRoot.supportVersions(); Q_EMIT specVersionsChanged(specVersions());
-
- m_d->watchable.afterAll(
- [this](KazvEvent e) {
- Q_EMIT this->trigger(e);
- });
-
- m_d->watchable.after<LoginSuccessful>(
- [this](LoginSuccessful e) {
- Q_EMIT this->loginSuccessful(e);
- });
-
- m_d->watchable.after<LoginFailed>(
- [this](LoginFailed e) {
- Q_EMIT this->loginFailed(
- QString::fromStdString(e.errorCode),
- QString::fromStdString(e.error)
- );
- });
-
- m_d->watchable.after<ReceivingRoomTimelineEvent>(
- [this](ReceivingRoomTimelineEvent e) {
- Q_EMIT this->receivedMessage(
- QString::fromStdString(e.roomId),
- QString::fromStdString(e.event.id())
- );
- });
-
- m_d->watchable.after<VerificationTrackerModelChanged>(
- [this](VerificationTrackerModelChanged) {
- auto model = lager::get<VerificationTracker>(m_d->sdk.context()).model;
- QMetaObject::invokeMethod(
- this, [this, model=std::move(model)]() {
- qCDebug(kazvLog) << "Verification tracker model changed" << model.processes.size();
- m_d->verificationTrackerState.set(model);
- lager::commit(m_d->verificationTrackerState);
- });
- }
- );
-
connect(&m_d->saveTimer, &QTimer::timeout, &m_d->saveTimer, [m_d=m_d.get()]() {
m_d->maybeSerialize();
});
const int saveIntervalMs = 1000 * 60 * 5;
m_d->saveTimer.start(std::chrono::milliseconds{saveIntervalMs});
-}
-MatrixSdk::MatrixSdk(QObject *parent)
- : MatrixSdk(std::make_unique<MatrixSdkPrivate>(
- this,
- /* testing = */ false,
- std::unique_ptr<KazvSessionLockGuard>(),
- std::unique_ptr<DbStore>()
- ), parent)
-{
+ connect(this, &MatrixSdk::loadSessionFinished, this, &MatrixSdk::handleLoadSessionResult);
}
-MatrixSdk::MatrixSdk(SdkModel model, bool testing, QObject *parent)
+MatrixSdk::MatrixSdk(QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(
- this,
- testing,
- std::move(model),
- std::unique_ptr<KazvSessionLockGuard>(),
- std::unique_ptr<DbStore>()
+ this
), parent)
{
}
-static void cleanupDPointer(std::unique_ptr<MatrixSdkPrivate> oldD)
-{
- oldD->saveTimer.disconnect();
- oldD->saveTimer.stop();
-
- auto helper = new CleanupHelper(std::move(oldD));
- helper->cleanup();
-}
-
MatrixSdk::~MatrixSdk()
{
- if (m_d) {
- serializeToFile();
- cleanupDPointer(std::move(m_d));
- }
-}
-
-QString MatrixSdk::mxcUriToHttp(QString mxcUri) const
-{
- return QString::fromStdString(m_d->clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
-}
-
-QString MatrixSdk::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
-{
- return QString::fromStdString(m_d->clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
-}
-
-MatrixDeviceList *MatrixSdk::devicesOfUser(QString userId) const
-{
- return new MatrixDeviceList(m_d->clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
-}
-
-bool isIllFormatSpecVersion(const QString &version)
-{
- const bool isLegacy = version.startsWith(u"r"_s);
- if (isLegacy && version.split(u'.').size() == 3) {
- return false;
- }
- if (!isLegacy && version.split(u'.').size() == 2) {
- return false;
- }
- return true;
-}
-
-// Return true if v1 is at least as new as v2, false if v1 is older than v2
-// Return false if v1 or v2 is ill-format
-bool compareSpecVersion(QString v1, QString v2)
-{
- // Check parameters format
- if (isIllFormatSpecVersion(v1) || isIllFormatSpecVersion(v2)) {
- return false;
- }
-
- const bool v1IsLegacy = v1.startsWith(u"r"_s);
- const bool v2IsLegacy = v2.startsWith(u"r"_s);
- if (v1IsLegacy != v2IsLegacy) {
- return v2IsLegacy;
- }
-
- v1.remove(0, 1);
- v2.remove(0, 1);
- auto v1VersionNumbers = v1.split(u'.');
- auto v2VersionNumbers = v2.split(u'.');
-
- for (int i = 0; i < v1VersionNumbers.size(); i++) {
- auto v1VerNum = v1VersionNumbers[i].toInt();
- auto v2VerNum = v2VersionNumbers[i].toInt();
- if (v1VerNum != v2VerNum) {
- return v1VerNum > v2VerNum;
- }
- }
- // v1 is equal to v2
- return true;
-}
-
-// Return true if version in the range [minVer, maxVer]
-// Return false if any parameter is ill-format
-bool compareSpecVersionRange(const QString &version,
- const QString &minVer, const QString &maxVer)
-{
- if (isIllFormatSpecVersion(version)
- || isIllFormatSpecVersion(minVer)
- || isIllFormatSpecVersion(maxVer)) {
- return false;
- }
-
- if (compareSpecVersion(version, minVer) && compareSpecVersion(maxVer, version)) {
- return true;
- }
-
- return false;
-}
-
-bool MatrixSdk::checkSpecVersion(QString version) const
-{
- return std::find_if(specVersions().begin(), specVersions().end(),
- [&version](auto v) {
- return compareSpecVersion(QString::fromStdString(v), version);
- }) != specVersions().end();
-}
-
-bool MatrixSdk::checkSpecVersionRange(QString minVer, QString maxVer) const
-{
- return std::find_if(specVersions().begin(), specVersions().end(),
- [&minVer, maxVer](auto v) {
- return compareSpecVersionRange(QString::fromStdString(v), minVer, maxVer);
- }) != specVersions().end();
-}
-
-QStringList MatrixSdk::directRoomIds(QString userId) const
-{
- auto content = m_d->clientOnSecondaryRoot.accountData().get()["m.direct"].content().get();
- auto roomIds = QStringList{};
- for (auto i : content[userId.toStdString()]) {
- roomIds.push_back(QString::fromStdString(i.get<std::string>()));
- }
- return roomIds;
-}
-
-MatrixVerificationList *MatrixSdk::verificationList() const
-{
- return new MatrixVerificationList(m_d->clientOnSecondaryRoot, m_d->verificationTrackerState);
-}
-
-std::string MatrixSdk::validateHomeserverUrl(const QString &url)
-{
- if (url.isEmpty()) {
- return std::string();
- }
-
- auto u = QUrl::fromUserInput(url);
- if (!u.isValid()) {
- return std::string();
- }
-
- if (u.scheme() == u"http"_s) {
- qCInfo(kazvLog) << "url" << u << "is http. Force switching to https.";
- u.setScheme(u"https"_s);
- } else if (u.scheme() != u"https"_s) {
- qCWarning(kazvLog) << "url" << u << "is not http/https.";
- return std::string();
- }
-
- return u.toString().toStdString();
-}
-
-void MatrixSdk::login(const QString &userId, const QString &password, const QString &homeserverUrl)
-{
- auto loginFunc = [userId, password](const Client &client, const std::string &serverUrl) {
- client.passwordLogin(
- serverUrl,
- userId.toStdString(),
- password.toStdString(),
- clientName
- );
- };
-
- auto validated = validateHomeserverUrl(homeserverUrl);
- if (!validated.empty()) {
- loginFunc(m_d->clientOnSecondaryRoot, validated);
- } else {
- m_d->clientOnSecondaryRoot
- .autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
- .then([
- this,
- client=m_d->clientOnSecondaryRoot.toEventLoop(),
- userId,
- password,
- loginFunc
- ](auto res) {
- if (!res.success()) {
- // FIXME use real error codes and msgs when available in libkazv
- Q_EMIT this->discoverFailed(u""_s, u""_s);
- return res;
- }
-
- auto serverUrl = res.dataStr("homeserverUrl");
-
- loginFunc(client, serverUrl);
-
- return res;
- });
- }
-
- m_d->runIoContext();
-}
-
-void MatrixSdk::discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl)
-{
- auto validated = validateHomeserverUrl(homeserverUrl);
- if (!validated.empty()) {
- getLoginFlows(homeserverUrl);
- } else {
- m_d->clientOnSecondaryRoot
- .autoDiscover(userId.toStdString())
- .then([
- this
- ](const EffectStatus &res) {
- if (!res.success()) {
- Q_EMIT discoverFailed(
- QString::fromStdString(res.dataStr("errorCode")),
- QString::fromStdString(res.dataStr("error"))
- );
- return;
- }
- auto serverUrl = QString::fromStdString(res.dataStr("homeserverUrl"));
- Q_EMIT discoverSuccessful(serverUrl);
- getLoginFlows(serverUrl);
- });
- }
-
- m_d->runIoContext();
-}
-
-void MatrixSdk::getLoginFlows(const QString &serverUrl)
-{
- m_d->jobHandler->submit(
- Api::GetLoginFlowsJob(serverUrl.toStdString()),
- [this](Api::GetLoginFlowsResponse r) {
- if (!r.success()) {
- Q_EMIT getLoginFlowsFailed(
- QString::fromStdString(r.errorCode()),
- QString::fromStdString(r.errorMessage())
- );
- return;
- }
- auto v = std::move(r).jsonBody().get().at("flows").template get<QJsonValue>();
- Q_EMIT getLoginFlowsSuccessful(v);
- }
- );
-}
-
-QUrl MatrixSdk::ssoLoginStart(const QString &homeserverUrl)
-{
- if (m_d->ssoLoginProcess) {
- m_d->ssoLoginProcess->deleteLater();
- }
- m_d->ssoLoginProcess = new SsoLoginProcess();
- if (!m_d->ssoLoginProcess->startServer()) {
- return QUrl();
- }
- auto link = m_d->ssoLoginProcess->getSsoLink(homeserverUrl);
- connect(m_d->ssoLoginProcess.get(), &SsoLoginProcess::loginTokenAvailable,
- this, [this, homeserverUrl](const QString &loginToken) {
- Q_EMIT ssoLoginTokenAvailable();
- qCInfo(kazvLog) << "Got SSO login token";
- m_d->ssoLoginProcess->deleteLater();
- m_d->clientOnSecondaryRoot.mLoginTokenLogin(
- homeserverUrl.toStdString(),
- loginToken.toStdString(),
- clientName
- );
- });
- return link;
-}
-
-void MatrixSdk::logout()
-{
- m_d->clientOnSecondaryRoot.logout()
- .then([&] (EffectStatus stat) {
- if (stat.success()) {
- m_d->stopIoContext();
- Q_EMIT this->logoutSuccessful();
- } else {
- Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
- }
- });
-}
-
-MatrixRoomList *MatrixSdk::roomList() const
-{
- return new MatrixRoomList(m_d->clientOnSecondaryRoot);
-}
-
-void MatrixSdk::emplace(std::optional<SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore)
-{
- auto testing = m_d->testing;
- auto userDataDir = m_d->userDataDir;
-
- if (m_d) {
- cleanupDPointer(std::move(m_d));
- }
-
- m_d = model.has_value()
- ? std::make_unique<MatrixSdkPrivate>(
- this, testing, std::move(model.value()),
- std::move(lockGuard), std::move(dbStore))
- : std::make_unique<MatrixSdkPrivate>(
- this, testing, std::move(lockGuard), std::move(dbStore)
- );
- m_d->userDataDir = userDataDir;
-
- // Re-initialize lager-qt cursors and watchable connections
- init();
-
- m_d->runIoContext();
-
- m_d->clientOnSecondaryRoot.startSyncing();
-
- Q_EMIT sessionChanged();
}
QStringList MatrixSdk::allSessions() const
{
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(m_d->userDataDir);
auto allSessionsDir = userDataDir / "sessions";
QStringList sessionNames;
try {
for (const auto &p : std::filesystem::directory_iterator(allSessionsDir)) {
if (p.is_directory()) {
auto maybeEncodedUserId = p.path().filename().string();
auto userId = decodeBase64(maybeEncodedUserId, Base64Opts::urlSafe);
if (userId.empty() || userId[0] != '@') {
continue;
}
for (const auto &q : std::filesystem::directory_iterator(p.path())) {
auto path = q.path();
auto deviceId = path.filename().string();
std::error_code err;
if (std::filesystem::exists(path / "store", err)) {
sessionNames.append(QString::fromStdString(userId + "/" + deviceId));
}
}
}
}
} catch (const std::filesystem::filesystem_error &) {
qDebug() << "sessionDir not available, ignoring";
}
return sessionNames;
}
void MatrixSdk::serializeToFile() const
{
m_d->maybeSerialize();
}
void MatrixSdk::loadSession(QString sessionName)
{
- using StdPath = std::filesystem::path;
- auto loadFromSession = [this, sessionName](StdPath sessionDir, std::unique_ptr<KazvSessionLockGuard> lockGuard) {
- auto storeFile = sessionDir / "store";
-
- auto metadataFile = sessionDir / "metadata";
-
- if (! std::filesystem::exists(storeFile)) {
- qDebug() << "storeFile does not exist, skip loading session " << sessionName;
- Q_EMIT loadSessionFinished(sessionName, SessionNotFound);
- return;
- }
-
- if (std::filesystem::exists(metadataFile)) {
- KConfig metadata(QString::fromStdString(metadataFile.string()));
- KConfigGroup mdGroup(&metadata, u"Metadata"_s);
- auto format = mdGroup.readEntry(u"archiveFormat"_s);
- if (format != u"text"_s && format != u"text-with-sqlite"_s) {
- qDebug() << "Unknown archive format:" << format;
- Q_EMIT loadSessionFinished(sessionName, SessionFormatUnknown);
- return;
- }
- auto version = mdGroup.readEntry(u"kazvVersion"_s);
- auto curVersion = kazvVersionString();
- if (version != QString::fromStdString(curVersion)) {
- qDebug() << "A different version from the current one, making a backup";
- std::error_code err;
- auto now = std::chrono::system_clock::now();
- auto backupName =
- std::to_string(std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count());
- auto backupDir = sessionDir / "backup" / backupName;
- if (! std::filesystem::create_directories(backupDir, err)
- && err) {
- qDebug() << "Cannot create backup directory";
- Q_EMIT loadSessionFinished(sessionName, SessionCannotBackup);
- return;
- }
- std::filesystem::copy_file(storeFile, backupDir / "store");
- std::filesystem::copy_file(metadataFile, backupDir / "metadata");
- }
- }
-
- SdkModel model;
- try {
- auto storeStream = std::ifstream(storeFile);
- if (! storeStream) {
- qDebug() << "Unable to open storeFile";
- Q_EMIT loadSessionFinished(sessionName, SessionCannotOpenFile);
- return;
- }
- using IAr = boost::archive::text_iarchive;
- auto archive = IAr{storeStream};
- archive >> model;
- qDebug() << "Finished loading session store file";
- } catch (const std::exception &e) {
- qDebug() << "Error when loading session:" << QString::fromStdString(e.what());
- Q_EMIT loadSessionFinished(sessionName, SessionDeserializeFailed);
- return;
- }
-
- auto dbStore = std::make_unique<DbStore>();
- auto stat = QCoro::waitFor(setupAndImport(model, dbStore.get(), m_d->userDataDir));
- if (!stat.first) {
- qCWarning(kazvLog) << "Unable to open db store:" << stat.second;
- Q_EMIT loadSessionFinished(sessionName, SessionDeserializeFailed);
- return;
- }
- QMetaObject::invokeMethod(this, [this, model=std::move(model), lockGuard=std::move(lockGuard), dbStore=std::move(dbStore)]() mutable {
- emplace(std::move(model), std::move(lockGuard), std::move(dbStore));
- });
- Q_EMIT loadSessionFinished(sessionName, SessionLoadSuccess);
- };
-
- qDebug() << "in loadSession(), sessionName=" << sessionName;
- auto userDataDir = StdPath(m_d->userDataDir);
- auto parts = sessionName.split(u'/');
- if (parts.size() == 2) {
- auto userId = parts[0].toStdString();
- auto deviceId = parts[1].toStdString();
- auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
- std::unique_ptr<KazvSessionLockGuard> lockGuard;
- try {
- lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
- } catch (const std::runtime_error &e) {
- qCWarning(kazvLog) << "Error locking session: " << e.what();
- Q_EMIT loadSessionFinished(sessionName, SessionLockFailed);
- return;
- }
- QThreadPool::globalInstance()->start([loadFromSession, sessionDir, lockGuard=std::move(lockGuard)]() mutable {
- loadFromSession(sessionDir, std::move(lockGuard));
- });
- return;
- }
- qDebug(kazvLog) << "no session found for" << sessionName;
- Q_EMIT loadSessionFinished(sessionName, SessionNotFound);
+ m_d->emplaceController();
+ m_d->controller->load(sessionName);
}
bool MatrixSdk::deleteSession(QString sessionName) {
using StdPath = std::filesystem::path;
qDebug() << "in deleteSession(), sessionName=" << sessionName;
auto userDataDir = StdPath(m_d->userDataDir);
auto parts = sessionName.split(u'/');
if (parts.size() == 2) {
auto userId = parts[0].toStdString();
auto deviceId = parts[1].toStdString();
auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
if (std::filesystem::exists(sessionDir)) {
qCDebug(kazvLog) << "new path works";
return std::filesystem::remove_all(sessionDir);
}
qCDebug(kazvLog) << "trying legacy path";
auto legacySessionDir = userDataDir / "sessions" / userId / deviceId;
if (std::filesystem::exists(legacySessionDir)) {
qCDebug(kazvLog) << "legacy path works";
return std::filesystem::remove_all(legacySessionDir);
}
}
qDebug(kazvLog) << "no session found for" << sessionName;
return false;
}
bool MatrixSdk::startNewSession()
{
- emplace(std::nullopt, std::unique_ptr<KazvSessionLockGuard>(), std::unique_ptr<DbStore>());
+ m_d->emplaceController();
+ m_d->controller->create();
return true;
}
-static std::optional<std::string> optMaybe(QString s)
-{
- if (s.isEmpty()) {
- return std::nullopt;
- } else {
- return s.toStdString();
- }
-}
-
-MatrixPromise *MatrixSdk::createRoom(
- bool isPrivate,
- const QString &name,
- const QString &alias,
- const QStringList &invite,
- bool isDirect,
- bool allowFederate,
- const QString &topic,
- const QJsonValue &powerLevelContentOverride,
- CreateRoomPreset preset,
- bool encrypted
-)
-{
- immer::array<Event> initialState;
-
- if (encrypted) {
- initialState = {Event{json{
- {"type", "m.room.encryption"},
- {"state_key", ""},
- {"content", {
- {"algorithm", "m.megolm.v1.aes-sha2"},
- }},
- }}};
- }
-
- return new MatrixPromise(m_d->clientOnSecondaryRoot.createRoom(
- isPrivate ? Kazv::RoomVisibility::Private : Kazv::RoomVisibility::Public,
- optMaybe(name),
- optMaybe(alias),
- qStringListToStdF(invite),
- isDirect,
- allowFederate,
- optMaybe(topic),
- nlohmann::json(powerLevelContentOverride),
- static_cast<Kazv::CreateRoomPreset>(preset),
- initialState
- ));
-}
-
-MatrixPromise *MatrixSdk::joinRoom(const QString &idOrAlias, const QStringList &servers)
-{
- return new MatrixPromise(m_d->clientOnSecondaryRoot.joinRoom(
- idOrAlias.toStdString(),
- qStringListToStdF(servers)
- ));
-}
-
-MatrixPromise *MatrixSdk::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
-{
- return new MatrixPromise(
- m_d->clientOnSecondaryRoot.setDeviceTrustLevel(
- userId.toStdString(),
- deviceId.toStdString(),
- qStringToTrustLevelFunc(trustLevel)
- )
- );
-}
-
-MatrixPromise *MatrixSdk::getSelfProfile()
-{
- return new MatrixPromise(
- m_d->clientOnSecondaryRoot.getProfile(userId().toStdString())
- );
-}
-
-MatrixPromise *MatrixSdk::setDisplayName(QString displayName)
-{
- return new MatrixPromise(
- m_d->clientOnSecondaryRoot.setDisplayName(
- displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
- )
- );
-}
-
-MatrixPromise *MatrixSdk::setAvatarUrl(QString avatarUrl)
-{
- return new MatrixPromise(
- m_d->clientOnSecondaryRoot.setAvatarUrl(
- avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
- )
- );
-}
-
void MatrixSdk::startThread()
{
m_d->runIoContext();
}
-RandomInterface &MatrixSdk::randomGenerator() const
+void MatrixSdk::handleLoadSessionResult(QString sessionName, Constants::LoadSessionResult result)
{
- return m_d->randomGenerator;
-}
-
-bool MatrixSdk::shouldNotify(MatrixEvent *event) const
-{
- // Do not notify own event
- if (event->sender() == userId()) {
- return false;
+ if (result == Constants::SessionLoadSuccess) {
+ m_d->controller->startSyncing();
}
- return m_d->notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
-}
-
-bool MatrixSdk::shouldPlaySound(MatrixEvent *event) const
-{
- return m_d->notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
-}
-
-MatrixStickerPackList *MatrixSdk::stickerPackList() const
-{
- return new MatrixStickerPackList(m_d->clientOnSecondaryRoot);
-}
-
-MatrixEvent *MatrixSdk::stickerRoomsEvent() const
-{
- return new MatrixEvent(m_d->clientOnSecondaryRoot.accountData()[imagePackRoomsEventType][lager::lenses::or_default]);
-}
-
-MatrixPromise *MatrixSdk::updateStickerPack(MatrixStickerPackSource source)
-{
- if (source.source == MatrixStickerPackSource::AccountData) {
- auto eventJson = std::move(source.event).raw().get();
- eventJson["type"] = source.eventType;
- return sendAccountDataImpl(Event(std::move(eventJson)));
- } else if (source.source == MatrixStickerPackSource::RoomState) {
- auto eventJson = std::move(source.event).raw().get();
- eventJson["type"] = source.eventType;
- eventJson["state_key"] = source.stateKey;
- return new MatrixPromise(
- m_d->clientOnSecondaryRoot
- .room(source.roomId)
- .sendStateEvent(Event(std::move(eventJson))));
- } else {
- return 0;
- }
-}
-
-MatrixUserGivenAttrsMap *MatrixSdk::userGivenNicknameMap() const
-{
- return new MatrixUserGivenAttrsMap(
- userGivenNicknameMapFor(m_d->clientOnSecondaryRoot),
- [client=m_d->clientOnSecondaryRoot](json content) {
- return client.setAccountData(json{
- {"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
- {"content", std::move(content)},
- });
- }
- );
-}
-
-MatrixPromise *MatrixSdk::sendAccountData(const QString &type, const QJsonObject &content)
-{
- Event e = json{
- {"type", type.toStdString()},
- {"content", content},
- };
- return sendAccountDataImpl(std::move(e));
-}
-
-MatrixPromise *MatrixSdk::sendAccountDataImpl(Event event)
-{
- return new MatrixPromise(m_d->clientOnSecondaryRoot.setAccountData(event));
-}
-
-MatrixPromise *MatrixSdk::getSpecVersions()
-{
- return new MatrixPromise(m_d->clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
-}
-
-MatrixPromise *MatrixSdk::addDirectRoom(const QString &userId, const QString &roomId)
-{
- return new MatrixPromise(m_d->clientOnSecondaryRoot.addDirectRoom(userId.toStdString(), roomId.toStdString()));
-}
-
-MatrixPromise *MatrixSdk::getRoomIdByAlias(const QString &roomAlias)
-{
- auto job = m_d->clientOnSecondaryRoot
- .getRoomIdByAliasJob(roomAlias.toStdString());
- auto jh = m_d->jobHandler;
- return new MatrixPromise(
- m_d->sdk.context().createPromise([jh, job](auto resolve) {
- jh->submit(job, [resolve](GetRoomIdByAliasResponse r) {
- resolve(parseGetRoomIdByAliasResponse(r));
- });
- }));
-}
-
-inline constexpr std::size_t purgeEventsKeepNumber = 5;
-MatrixPromise *MatrixSdk::purgeEventsExceptRooms(const QStringList &roomIds)
-{
- return new MatrixPromise(
- m_d->sdk.context().createResolvedPromise({})
- .then([client=m_d->sdk.client(), roomIds=qStringListToStdF(roomIds)
- ]([[maybe_unused]] auto &&stat) {
- auto map = intoImmer(immer::map<std::string, std::size_t>{},
- zug::filter([&roomIds](const std::string &roomId) {
- return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
- })
- | zug::map([](const std::string &roomId) {
- return std::make_pair(roomId, purgeEventsKeepNumber);
- }),
- client.roomIds().make().get()
- );
- return client.purgeRoomEvents(map);
- }).then([](const auto &stat) {
- qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
- return stat;
- })
- );
-}
-
-MatrixPromise *MatrixSdk::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
-{
- return new MatrixPromise(
- m_d->sdk.context().createPromise([
- dbStore=m_d->dbStore.get(),
- client=m_d->sdk.client(),
- roomId,
- eventId
- ](auto resolve) {
- dbStore->getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
- auto [timelineEvents, relatedEvents] = std::move(res);
- auto loadedCount = timelineEvents[roomId.toStdString()].size();
- client.loadEventsFromStorage(timelineEvents, relatedEvents)
- .then([resolve, loadedCount](auto &&) {
- resolve(EffectStatus(
- !!loadedCount,
- json{{"loadedCount", loadedCount}}
- ));
- });
- });
- })
- );
-}
-
-MatrixPromise *MatrixSdk::importFromKeyBackupFile(QUrl fileUrl, QString password)
-{
- if (!fileUrl.isLocalFile()) {
- return new MatrixPromise(m_d->sdk.context().createResolvedPromise({
- /* succ = */ false,
- json{{"error", "Not a local file"}, {"errorCode", "NOT_LOCAL_FILE"}},
- }));
- }
- auto filename = fileUrl.toLocalFile();
- auto f = QFile(filename);
- if (!f.open(QFile::ReadOnly)) {
- return new MatrixPromise(m_d->sdk.context().createResolvedPromise({
- /* succ = */ false,
- json{{"error", "File open failed"}, {"errorCode", "FILE_OPEN_FAILED"}},
- }));
- }
- auto ba = f.readAll();
- auto content = std::string(ba.begin(), ba.end());
- auto passwordStd = std::move(password).toStdString();
- return new MatrixPromise(m_d->clientOnSecondaryRoot.importFromKeyBackupFile(
- std::move(content), std::move(passwordStd)
- ));
-}
-
-MatrixPromise *MatrixSdk::requestVerifyDevice(QString userId, QString deviceId)
-{
- return new MatrixPromise(m_d->clientOnSecondaryRoot.requestOutgoingToDeviceVerification(
- std::move(userId).toStdString(),
- std::move(deviceId).toStdString()
- ));
}
void MatrixSdk::setUserDataDir(const std::string &userDataDir)
{
m_d->userDataDir = userDataDir;
}
-#include "matrix-sdk.moc"
+MatrixSession *MatrixSdk::session() const
+{
+ return m_d->controller->session();
+}
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
index 0d93cbf..2d1513f 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -1,397 +1,116 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
-
+#include "constants.hpp"
+#include "meta-types.hpp"
+Q_MOC_INCLUDE("matrix-promise.hpp")
+Q_MOC_INCLUDE("matrix-event.hpp")
+Q_MOC_INCLUDE("matrix-session.hpp")
#include <QObject>
#include <QQmlEngine>
#include <QString>
#include <string>
#include <memory>
#include <filesystem>
#include <lager/extra/qt.hpp>
#include <immer/map.hpp>
#include <sdk-model.hpp>
#include <random-generator.hpp>
-#include "meta-types.hpp"
-Q_MOC_INCLUDE("matrix-room-list.hpp")
-Q_MOC_INCLUDE("matrix-device-list.hpp")
-Q_MOC_INCLUDE("matrix-promise.hpp")
-Q_MOC_INCLUDE("matrix-event.hpp")
-Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
-Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
-Q_MOC_INCLUDE("matrix-verification-list.hpp")
-
-class MatrixRoomList;
-class MatrixDeviceList;
class MatrixPromise;
-class MatrixSdkTest;
class MatrixSdkSessionsTest;
-class MatrixEvent;
-class MatrixStickerPackList;
-class MatrixUserGivenAttrsMap;
class KazvSessionLockGuard;
class DbStore;
-class MatrixVerificationList;
+class MatrixSession;
struct MatrixSdkPrivate;
-std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId);
-
class MatrixSdk : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<MatrixSdkPrivate> m_d;
/// @param d A dynamically allocated d-pointer, whose ownership
/// will be transferred to this MatrixSdk.
explicit MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent);
void init();
public:
- enum CreateRoomPreset {
- PrivateChat = Kazv::CreateRoomPreset::PrivateChat,
- PublicChat = Kazv::CreateRoomPreset::PublicChat,
- TrustedPrivateChat = Kazv::CreateRoomPreset::TrustedPrivateChat,
- };
-
- Q_ENUM(CreateRoomPreset);
-
- enum LoadSessionResult {
- /// Successfully loaded the session
- SessionLoadSuccess,
- /// There is no store file
- SessionNotFound,
- /// The format of the store file is not supported
- SessionFormatUnknown,
- /// The store file cannot be backed up
- SessionCannotBackup,
- /// Cannot grab the lock on the session file
- SessionLockFailed,
- /// Cannot open store file
- SessionCannotOpenFile,
- /// Cannot deserialize the store file
- SessionDeserializeFailed,
- };
-
- Q_ENUM(LoadSessionResult);
-
- enum MediaDownloadEndpointVersion {
- UnauthenticatedMediaV3, // Classical endpoint series
- AuthenticatedMediaV1, // Endpoints that needs authorization
- };
-
- Q_ENUM(MediaDownloadEndpointVersion);
-
explicit MatrixSdk(QObject *parent = 0);
~MatrixSdk() override;
- LAGER_QT_READER(QString, serverUrl);
- LAGER_QT_READER(QString, userId);
- LAGER_QT_READER(QString, token);
- LAGER_QT_READER(QString, deviceId);
- LAGER_QT_READER(immer::array<std::string>, specVersions); // The versions of the Matrix Spec supported by the server.
-
- Q_INVOKABLE MatrixRoomList *roomList() const;
-
- Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
-
- Q_INVOKABLE QString mxcUriToHttpAuthenticatedV1(QString mxcUri) const;
-
- Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
-
- // Return true if version is at least as new as the spec version of server
- Q_INVOKABLE bool checkSpecVersion(QString version) const;
-
- // Return true if the spec version of server in the range [minVer, maxVer]
- Q_INVOKABLE bool checkSpecVersionRange(QString minVer, QString maxVer) const;
-
- Q_INVOKABLE QStringList directRoomIds(QString userId) const;
-
- Q_INVOKABLE MatrixVerificationList *verificationList() const;
-
- Kazv::RandomInterface &randomGenerator() const;
-
-private:
- // Replaces the store with another one
- void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore);
-
- static std::string validateHomeserverUrl(const QString &url);
+ /**
+ * Get the current session.
+ */
+ Q_INVOKABLE MatrixSession *session() const;
Q_SIGNALS:
- void trigger(Kazv::KazvEvent e);
-
- void loginSuccessful(Kazv::KazvEvent e);
- void loginFailed(QString errorCode, QString errorMsg);
- void discoverFailed(QString errorCode, QString errorMsg);
- void discoverSuccessful(QString serverUrl);
- void getLoginFlowsFailed(QString errorCode, QString errorMsg);
- void getLoginFlowsSuccessful(QJsonValue flows);
- void ssoLoginTokenAvailable();
- void logoutSuccessful();
- void logoutFailed(QString errorCode, QString errorMsg);
-
- void receivedMessage(QString roomId, QString eventId);
-
void sessionChanged();
- void loadSessionFinished(QString sessionName, MatrixSdk::LoadSessionResult result);
+ void loadSessionFinished(QString sessionName, Constants::LoadSessionResult result);
void dbResolved(bool success);
public Q_SLOTS:
- void login(const QString &userId, const QString &password, const QString &homeserverUrl);
-
- /**
- * Auto-discover the server url and then get the login flows from the server.
- *
- * If homeserverUrl is not provided, try to get it from auto-discovery.
- * If discovery is successful, or it is already provided, get the login flows
- * by calling getLoginFlows.
- */
- void discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl);
- void getLoginFlows(const QString &serverUrl);
-
- /**
- * Start SSO login flow.
- *
- * It will start an http server on a local port and pass it as
- * the redirect url to the SSO login link.
- *
- * When the user completes the SSO login flow, we get the login token
- * for us to login via the token flow.
- *
- * @return The SSO login link for the user to open.
- */
- QUrl ssoLoginStart(const QString &homeserverUrl);
- void logout();
/**
* Serialize data to <AppDataDir>/sessions/<userid>/<deviceid>/
*
* If not logged in, do nothing.
*/
void serializeToFile() const;
/**
* Load session at <AppDataDir>/sessions/<sessionName> asynchronously.
*
* When the session is loaded or when there is an error, loadSessionFinished
* will be called with the result.
*
* @param sessionName A string in the form of <userid>/<deviceid> .
*/
void loadSession(QString sessionName);
/**
* Delete session at <AppDataDir>/sessions/<sessionName> .
*
* @param sessionName A string in the form of <userid>/<deviceid> .
*
* @return true if successful, false otherwise.
*/
bool deleteSession(QString sessionName);
/**
* Start an empty session.
*
* The new session is not logged in, and need to call login().
*
* @return true if successful, false otherwise.
*/
bool startNewSession();
/**
* Get all saved sessions.
*
* @return A list of session names in the form of <userid>/<deviceid> .
*/
QStringList allSessions() const;
- /**
- * Create a new room.
- *
- * @param isPrivate Whether the room is private.
- * @param name The room's name.
- * @param alias The alias of the room.
- * @param invite List of matrix ids of users to invite.
- * @param isDirect Whether it is a direct message room.
- * @param allowFederate Whether to allow users on other servers to join.
- * @param topic The topic of the room.
- * @param powerLevelContentOverride The content to override m.room.power_levels event.
- * @param preset The preset to create the room with.
- * @param encrypted Whether to enable encryption for this room.
- */
- MatrixPromise *createRoom(
- bool isPrivate,
- const QString &name,
- const QString &alias,
- const QStringList &invite,
- bool isDirect,
- bool allowFederate,
- const QString &topic,
- const QJsonValue &powerLevelContentOverride,
- CreateRoomPreset preset,
- bool encrypted
- );
-
- /**
- * Join a room.
- * @param idOrAlias The id or alias of the room to join.
- * @param servers The servers to use when joining the room.
- */
- MatrixPromise *joinRoom(
- const QString &idOrAlias,
- const QStringList &servers
- );
-
- /**
- * Change the trust level of a device.
- *
- * @param userId The user id that owns the device.
- * @param deviceId The device id to set the trust level.
- * @param trustLevel The trust level.
- *
- * @return A MatrixPromise representing the progress.
- */
- MatrixPromise *setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel);
-
- /**
- * Get the profile of the current user.
- *
- * @return A MatrixPromise representing the progress.
- */
- MatrixPromise *getSelfProfile();
-
- /**
- * Set the display name of the current user.
- *
- * @return A MatrixPromise representing the progress.
- */
- MatrixPromise *setDisplayName(QString displayName);
-
- /**
- * Set the avatar url of the current user.
- *
- * @return A MatrixPromise representing the progress.
- */
- MatrixPromise *setAvatarUrl(QString avatarUrl);
-
- /**
- * Check if an event should be notified.
- *
- * @param event The event to check.
- * @return Whether `event` should be notified.
- */
- bool shouldNotify(MatrixEvent *event) const;
-
- /**
- * Check if an event should be notified with sound.
- *
- * You should only call this method when `shouldNotify(event)`
- * returns true.
- *
- * @param event The event to check.
- * @return Whether `event` should be notified with sound.
- */
- bool shouldPlaySound(MatrixEvent *event) const;
-
- /**
- * Get the sticker pack list for the current account.
- *
- * @return A list of sticker packs associated with the current account.
- */
- MatrixStickerPackList *stickerPackList() const;
-
- /**
- * Get the sticker rooms account data event for the current account.
- *
- * @return A MatrixEvent representing the sticker rooms account data event.
- */
- MatrixEvent *stickerRoomsEvent() const;
-
- /**
- * Update the sticker pack from source.
- *
- * @param source The source of the sticker pack to update.
- * @return A promise that resolves when the sticker pack is updated,
- * or when there is an error.
- */
- MatrixPromise *updateStickerPack(MatrixStickerPackSource source);
-
- MatrixUserGivenAttrsMap *userGivenNicknameMap() const;
-
- MatrixPromise *sendAccountData(const QString &type, const QJsonObject &content);
-
- /**
- * Get all Matrix Spec versions supported by the server.
- * Use MatrixSdk::supportSpecVersion() to check if a version is supported.
- */
- MatrixPromise *getSpecVersions();
-
- MatrixPromise *addDirectRoom(const QString &userId, const QString &roomId);
-
- MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
-
- /**
- * Purge events in all rooms except those specified in roomIds
- *
- * @param roomIds The ids for rooms to NOT be purged. This allows
- * keeping the rooms that are already open intact.
- * @return A MatrixPromise that resolves when the events are purged.
- */
- MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
-
- /**
- * Backfill from the event storage from an existing event in a room.
- *
- * @param roomId The id of the room to operate on.
- * @param eventId The id of the event to backfill from.
- * @return A MatrixPromise that resolves when the events are backfilled.
- * The Promise is considered successful if and only if at least one event
- * is loaded.
- */
- MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
-
- /**
- * Import from key backup file.
- *
- * @param fileUrl The url of the key backup file. Will be passed to
- * QUrl::toLocalFile(), and must be a local file.
- * @param password The password to decrypt the key backup file.
- * @return A MatrixPromise that resolves when the keys are imported.
- * If successful, `data["imported"]` contains the number of imported keys.
- * If unsuccessful, `data` contains a standard error structure.
- */
- MatrixPromise *importFromKeyBackupFile(QUrl fileUrl, QString password);
-
- /**
- * Send an outbound device verification request.
- *
- * @param userId The user id of the device to verify.
- * @param deviceId The device id of the device to verify.
- */
- MatrixPromise *requestVerifyDevice(QString userId, QString deviceId);
+ void startThread();
-private:
- MatrixPromise *sendAccountDataImpl(Kazv::Event event);
+private Q_SLOTS:
+ void handleLoadSessionResult(QString sessionName, Constants::LoadSessionResult result);
private: // Testing
- friend MatrixSdkTest;
friend MatrixSdkSessionsTest;
- friend MatrixSdk *makeTestSdk(Kazv::SdkModel model);
void setUserDataDir(const std::string &userDataDir);
-
- explicit MatrixSdk(Kazv::SdkModel model, bool testing = false, QObject *parent = 0);
- void startThread();
};
diff --git a/src/matrix-session-controller-p.hpp b/src/matrix-session-controller-p.hpp
new file mode 100644
index 0000000..61bdaed
--- /dev/null
+++ b/src/matrix-session-controller-p.hpp
@@ -0,0 +1,113 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include "matrix-session-controller.hpp"
+#include "matrix-session-types.hpp"
+#include "kazv-session-lock-guard.hpp"
+#include "helper.hpp"
+#include "qt-rand-adapter.hpp"
+#include "qt-job-handler.hpp"
+#include "db-store.hpp"
+#include <lager/event_loop/qt.hpp>
+#include <QPointer>
+
+using namespace Kazv;
+
+struct QtEventLoop
+{
+ QObject *m_obj;
+
+ template<class Fn>
+ void async(Fn &&) { throw std::runtime_error{"not implemented!"}; }
+
+ template<class Fn>
+ void post(Fn &&fn)
+ {
+ QMetaObject::invokeMethod(
+ m_obj, std::forward<Fn>(fn), Qt::QueuedConnection
+ );
+ }
+
+ inline void finish() {}
+ inline void pause() { throw std::runtime_error{"not implemented!"}; }
+ inline void resume() { throw std::runtime_error{"not implemented!"}; }
+};
+
+struct PendingSaveEvents : public SaveEventsRequested
+{
+ using DataT = immer::map<std::string, EventList>;
+ void add(SaveEventsRequested s)
+ {
+ timelineEvents = addOneType(std::move(timelineEvents), s.timelineEvents);
+ nonTimelineEvents = addOneType(std::move(nonTimelineEvents), s.nonTimelineEvents);
+ }
+
+ static DataT addOneType(DataT orig, DataT addon)
+ {
+ for (const auto &[roomId, el]: addon) {
+ orig = std::move(orig).update(roomId, [&el](EventList origEl) {
+ // This works without explicit dedupe because we do not do parallel
+ // inserts and the insert order is the same as list order.
+ return origEl + el;
+ });
+ }
+ return orig;
+ }
+};
+
+enum DbStatus
+{
+ Pending,
+ Ready,
+ Failed,
+};
+
+class MatrixSessionControllerPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ MatrixSessionControllerPrivate(
+ QThread *thread,
+ std::string userDataDir,
+ SdkModel model,
+ std::unique_ptr<KazvSessionLockGuard> lockGuard,
+ std::unique_ptr<DbStore> dbStore,
+ QObject *parent = nullptr
+ );
+
+ ~MatrixSessionControllerPrivate();
+
+ void saveSession();
+
+ void saveOrQueueEvents(SaveEventsRequested s);
+
+ void saveEvents(SaveEventsRequested s);
+
+Q_SIGNALS:
+ void dbResolved(bool success);
+
+private:
+ friend class MatrixSessionController;
+
+ QPointer<QThread> thread;
+ std::string userDataDir;
+ UniquePtrDL<QObject> obj;
+ std::unique_ptr<KazvSessionLockGuard> lockGuard;
+ RandomInterface randomGenerator;
+ DbStatus dbStatus;
+ std::unique_ptr<DbStore> dbStore;
+ QPointer<QtJobHandler> jobHandler;
+ LagerStoreEventEmitter ee;
+ LagerStoreEventEmitter::Watchable watchable;
+ MatrixSessionSdkT sdk;
+ SecondaryRootT secondaryRoot;
+ Client clientOnSecondaryRoot;
+ PendingSaveEvents pendingSaveEvents{};
+ lager::state<VerificationTrackerModel> verificationTrackerState;
+};
diff --git a/src/matrix-session-controller.cpp b/src/matrix-session-controller.cpp
new file mode 100644
index 0000000..d2838ac
--- /dev/null
+++ b/src/matrix-session-controller.cpp
@@ -0,0 +1,454 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "matrix-session-controller-p.hpp"
+#include "matrix-session.hpp"
+#include "kazv-log.hpp"
+#include "kazv-version.hpp"
+#include <crypto/base64.hpp>
+#include <KConfig>
+#include <KConfigGroup>
+#include <QCoroTask>
+#include <boost/archive/text_oarchive.hpp>
+#include <boost/archive/text_iarchive.hpp>
+#include <fstream>
+#include <filesystem>
+#include <chrono>
+
+using namespace Qt::Literals::StringLiterals;
+using namespace Kazv;
+
+std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId)
+{
+ auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
+ auto sessionDir = userDataDir / "sessions"
+ / encodedUserId / deviceId;
+ return sessionDir;
+}
+
+QCoro::Task<std::pair<bool, QString>> setupAndImport(SdkModel model, DbStore *dbStore, std::string userDataDir)
+{
+ auto res = co_await dbStore->setup(
+ userDataDir,
+ model.c().userId,
+ model.c().deviceId
+ );
+ if (!res.first) {
+ co_return res;
+ }
+ co_return co_await dbStore->importAllFrom(model);
+}
+
+MatrixSessionControllerPrivate::MatrixSessionControllerPrivate(
+ QThread *threadArg,
+ std::string userDataDirArg,
+ SdkModel model,
+ std::unique_ptr<KazvSessionLockGuard> lockGuardArg,
+ std::unique_ptr<DbStore> dbStoreArg,
+ QObject *parent
+)
+ : QObject(parent)
+ , thread(threadArg)
+ , userDataDir(userDataDirArg)
+ , obj(new QObject())
+ , lockGuard(std::move(lockGuardArg))
+ , randomGenerator(QtRandAdapter{})
+ , dbStatus(dbStoreArg ? Ready : Pending)
+ , dbStore(dbStoreArg ? std::move(dbStoreArg) : std::make_unique<DbStore>())
+ , jobHandler(new QtJobHandler(obj.get()))
+ , ee{QtEventLoop{obj.get()}}
+ , watchable(ee.watchable())
+ , sdk(makeSdk(
+ model,
+ static_cast<JobInterface &>(*jobHandler),
+ static_cast<EventInterface &>(ee),
+ QtPromiseHandler(*obj),
+ zug::identity,
+ withRandomGenerator(randomGenerator)))
+ , secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{this}, std::move(model)))
+ , clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
+{
+ obj->moveToThread(thread);
+
+ watchable.after<SaveEventsRequested>([this](SaveEventsRequested e) {
+ QMetaObject::invokeMethod(this, [this, e=std::move(e)]() mutable {
+ saveOrQueueEvents(std::move(e));
+ });
+ });
+
+ watchable.after<LoginSuccessful>([this](LoginSuccessful) {
+ QMetaObject::invokeMethod(this, [this]() {
+ if (dbStatus == Ready) {
+ qCWarning(kazvLog) << "Trying to replace current db store, ignoring";
+ return;
+ }
+ // This must be called from the GUI thread
+ auto model = secondaryRoot.get();
+ setupAndImport(std::move(model), dbStore.get(), userDataDir)
+ .then([this](const auto &stat) {
+ if (!stat.first) {
+ qCWarning(kazvLog) << "Cannot set up database:" << stat.second;
+ }
+ Q_EMIT dbResolved(stat.first);
+ });
+ });
+ });
+
+ watchable.after<VerificationTrackerModelChanged>([this](VerificationTrackerModelChanged) {
+ auto model = lager::get<VerificationTracker>(sdk.context()).model;
+ QMetaObject::invokeMethod(
+ this, [this, model=std::move(model)]() {
+ qCDebug(kazvLog) << "Verification tracker model changed" << model.processes.size();
+ verificationTrackerState.set(model);
+ lager::commit(verificationTrackerState);
+ });
+ });
+
+
+ connect(this, &MatrixSessionControllerPrivate::dbResolved, this, [this](bool success) {
+ if (!success) {
+ dbStatus = Failed;
+ } else {
+ qCDebug(kazvLog) << "db loading succeeded";
+ auto origPendingSaveEvents = std::move(pendingSaveEvents);
+ pendingSaveEvents = {};
+ saveEvents(std::move(pendingSaveEvents));
+ dbStatus = Ready;
+ }
+ });
+}
+
+void MatrixSessionControllerPrivate::saveSession()
+{
+ using namespace Kazv::CursorOp;
+ auto &c = clientOnSecondaryRoot;
+ auto userId = +c.userId();
+ auto deviceId = +c.deviceId();
+ if (userId.empty() || deviceId.empty()) {
+ qDebug() << "Not logged in, nothing to serialize";
+ return;
+ }
+
+ using StdPath = std::filesystem::path;
+ auto userDataDir = StdPath(this->userDataDir);
+ auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
+
+ auto storeFile = sessionDir / "store";
+ auto storeFileNew = sessionDir / "store.new";
+
+ auto metadataFile = sessionDir / "metadata";
+
+ qDebug() << "storeFile=" << QString::fromStdString(storeFile.string());
+
+ std::error_code err;
+ if ((! std::filesystem::create_directories(sessionDir, err))
+ && err) {
+ qDebug() << "Unable to create sessionDir";
+ return;
+ }
+
+ if (!lockGuard) {
+ try {
+ lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
+ } catch (const std::runtime_error &e) {
+ qCWarning(kazvLog) << "Error locking session: " << e.what();
+ return;
+ }
+ }
+
+ try {
+ auto storeStream = std::ofstream(storeFileNew);
+ if (! storeStream) {
+ qCWarning(kazvLog) << "Unable to open storeFile";
+ return;
+ }
+ using OAr = boost::archive::text_oarchive;
+ auto archive = OAr{storeStream};
+ c.serializeTo(archive);
+ } catch (const std::exception &e) {
+ qCWarning(kazvLog) << "Cannot write to store file: " << e.what();
+ return;
+ }
+
+ err.clear();
+ std::filesystem::rename(storeFileNew, storeFile, err);
+ if (err) {
+ qCWarning(kazvLog) << "Cannot move storeFile into place: " << QString::fromStdString(err.message());
+ return;
+ }
+ qDebug() << "Serialization done";
+
+
+ // store metadata
+ {
+ KConfig metadata(QString::fromStdString(metadataFile.string()));
+ KConfigGroup mdGroup(&metadata, u"Metadata"_s);
+ mdGroup.writeEntry("kazvVersion", QString::fromStdString(kazvVersionString()));
+ mdGroup.writeEntry("archiveFormat", "text-with-sqlite");
+ }
+}
+
+void MatrixSessionControllerPrivate::saveOrQueueEvents(SaveEventsRequested s)
+{
+ if (dbStatus == Ready) {
+ saveEvents(std::move(s));
+ } else if (dbStatus == Pending) {
+ pendingSaveEvents.add(std::move(s));
+ }
+ // Database open failed, do not do anything
+}
+
+void MatrixSessionControllerPrivate::saveEvents(SaveEventsRequested s)
+{
+ dbStore->saveEvents(std::move(s))
+ .then([](const auto &stat) {
+ if (!stat.first) {
+ qCWarning(kazvLog) << "Cannot save events:" << stat.second;
+ }
+ });
+}
+
+MatrixSessionControllerPrivate::~MatrixSessionControllerPrivate()
+{
+}
+
+MatrixSessionController::MatrixSessionController(
+ QThread *workerThread,
+ std::string userDataDir,
+ QObject *parent
+)
+ : QObject(parent)
+ , m_thread(workerThread)
+ , m_userDataDir(userDataDir)
+ , m_d(nullptr)
+{
+}
+
+MatrixSessionController::~MatrixSessionController() = default;
+
+// Cleaning up notes:
+// 0. Callback functions may store the context for an indefinite time
+// 1. The QThread event loop can be stopped
+// 2. QtJobHandler::submit() should only happen in the primary event loop thread
+// 3. QtJobHandler lives in the primary event loop thread
+// 4. QtJobs live in the primary event loop thread
+// 5. Job callbacks are called in the primary event loop thread
+// 6. QtPromise::then() callbacks are called in the primary event loop thread
+// 7. When the QThread event loop stops, no more callbacks will be executed (there is nothing to post to)
+// 8. The QThread should stop before obj is deleted
+// class CleanupHelper : public QObject
+// {
+// Q_OBJECT
+// public:
+// explicit CleanupHelper(std::unique_ptr<MatrixSdkPrivate> d)
+// : oldD(std::move(d))
+// {
+// }
+
+// std::unique_ptr<MatrixSdkPrivate> oldD;
+
+// void cleanup()
+// {
+// qCInfo(kazvLog) << "start to clean up everything";
+
+// oldD->clientOnSecondaryRoot.stopSyncing()
+// .then([obj=oldD->obj, thread=oldD->thread](auto &&) {
+// qCDebug(kazvLog) << "stopped syncing";
+// QMetaObject::invokeMethod(obj, [thread]() {
+// thread->quit();
+// });
+// });
+// oldD->thread->wait();
+// oldD->thread->deleteLater();
+// // After the thread's event loop is finished, we can delete the root object
+// oldD->obj->deleteLater();
+// this->deleteLater();
+// qCInfo(kazvLog) << "thread is done";
+// }
+// };
+
+// static void cleanupDPointer(std::unique_ptr<MatrixSdkPrivate> oldD)
+// {
+// oldD->saveTimer.disconnect();
+// oldD->saveTimer.stop();
+
+// auto helper = new CleanupHelper(std::move(oldD));
+// helper->cleanup();
+// }
+
+void MatrixSessionController::load(QString sessionName)
+{
+ if (m_d) {
+ qCWarning(kazvLog) << "MatrixSessionController: attempting to load when there is already a session";
+ return;
+ }
+ using StdPath = std::filesystem::path;
+ auto loadFromSession = [this, sessionName](StdPath sessionDir, std::unique_ptr<KazvSessionLockGuard> lockGuard) {
+ auto storeFile = sessionDir / "store";
+
+ auto metadataFile = sessionDir / "metadata";
+
+ if (! std::filesystem::exists(storeFile)) {
+ qDebug() << "storeFile does not exist, skip loading session " << sessionName;
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionNotFound);
+ return;
+ }
+
+ if (std::filesystem::exists(metadataFile)) {
+ KConfig metadata(QString::fromStdString(metadataFile.string()));
+ KConfigGroup mdGroup(&metadata, u"Metadata"_s);
+ auto format = mdGroup.readEntry(u"archiveFormat"_s);
+ if (format != u"text"_s && format != u"text-with-sqlite"_s) {
+ qDebug() << "Unknown archive format:" << format;
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionFormatUnknown);
+ return;
+ }
+ auto version = mdGroup.readEntry(u"kazvVersion"_s);
+ auto curVersion = kazvVersionString();
+ if (version != QString::fromStdString(curVersion)) {
+ qDebug() << "A different version from the current one, making a backup";
+ std::error_code err;
+ auto now = std::chrono::system_clock::now();
+ auto backupName =
+ std::to_string(std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count());
+ auto backupDir = sessionDir / "backup" / backupName;
+ if (! std::filesystem::create_directories(backupDir, err)
+ && err) {
+ qDebug() << "Cannot create backup directory";
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionCannotBackup);
+ return;
+ }
+ std::filesystem::copy_file(storeFile, backupDir / "store");
+ std::filesystem::copy_file(metadataFile, backupDir / "metadata");
+ }
+ }
+
+ SdkModel model;
+ try {
+ auto storeStream = std::ifstream(storeFile);
+ if (! storeStream) {
+ qDebug() << "Unable to open storeFile";
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionCannotOpenFile);
+ return;
+ }
+ using IAr = boost::archive::text_iarchive;
+ auto archive = IAr{storeStream};
+ archive >> model;
+ qDebug() << "Finished loading session store file";
+ } catch (const std::exception &e) {
+ qDebug() << "Error when loading session:" << QString::fromStdString(e.what());
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionDeserializeFailed);
+ return;
+ }
+
+ auto dbStore = std::make_unique<DbStore>();
+ auto stat = QCoro::waitFor(setupAndImport(model, dbStore.get(), m_userDataDir));
+ if (!stat.first) {
+ qCWarning(kazvLog) << "Unable to open db store:" << stat.second;
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionDeserializeFailed);
+ return;
+ }
+ QMetaObject::invokeMethod(this, [this, model=std::move(model), lockGuard=std::move(lockGuard), dbStore=std::move(dbStore)]() mutable {
+ m_d.reset(new MatrixSessionControllerPrivate(
+ m_thread, m_userDataDir,
+ std::move(model), std::move(lockGuard), std::move(dbStore)
+ ));
+ Q_EMIT sessionReady();
+ });
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionLoadSuccess);
+ };
+
+ qDebug() << "in loadSession(), sessionName=" << sessionName;
+ auto userDataDir = StdPath(m_userDataDir);
+ auto parts = sessionName.split(u'/');
+ if (parts.size() == 2) {
+ auto userId = parts[0].toStdString();
+ auto deviceId = parts[1].toStdString();
+ auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
+ std::unique_ptr<KazvSessionLockGuard> lockGuard;
+ try {
+ lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
+ } catch (const std::runtime_error &e) {
+ qCWarning(kazvLog) << "Error locking session: " << e.what();
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionLockFailed);
+ return;
+ }
+ QThreadPool::globalInstance()->start([loadFromSession, sessionDir, lockGuard=std::move(lockGuard)]() mutable {
+ loadFromSession(sessionDir, std::move(lockGuard));
+ });
+ return;
+ }
+ qDebug(kazvLog) << "no session found for" << sessionName;
+ Q_EMIT loadSessionFinished(sessionName, Constants::SessionNotFound);
+}
+
+void MatrixSessionController::create()
+{
+ auto model = SdkModel{};
+ RandomData random(Crypto::constructRandomSize(), '\0');
+ QRandomGenerator::securelySeeded().generate(random.begin(), random.end());
+ model.client.crypto = Crypto(
+ RandomTag{},
+ std::move(random)
+ );
+
+ createWithModel(model);
+}
+
+void MatrixSessionController::createWithModel(const Kazv::SdkModel &model)
+{
+ if (m_d) {
+ qCWarning(kazvLog) << "MatrixSessionController: attempting to create when there is already a session";
+ return;
+ }
+ m_d.reset(new MatrixSessionControllerPrivate(
+ m_thread,
+ m_userDataDir,
+ model,
+ /* lockGuard = */ nullptr,
+ /* dbStore = */ nullptr
+ ));
+ Q_EMIT sessionReady();
+}
+
+void MatrixSessionController::save() const
+{
+ if (m_d) {
+ m_d->saveSession();
+ }
+}
+
+void MatrixSessionController::prepareDestroy()
+{
+ if (m_d) {
+ qCDebug(kazvLog) << "MatrixSessionController::prepareDestroy";
+ m_d->clientOnSecondaryRoot.stopSyncing();
+ m_d->obj = nullptr;
+ }
+}
+
+void MatrixSessionController::startSyncing()
+{
+ m_d->clientOnSecondaryRoot.startSyncing();
+}
+
+MatrixSession *MatrixSessionController::session() const
+{
+ if (m_d) {
+ return new MatrixSession(
+ m_d->sdk.context(),
+ m_d->clientOnSecondaryRoot,
+ m_d->ee.watchable(),
+ m_d->verificationTrackerState,
+ [d=m_d.get()]() -> DbStore & { return *(d->dbStore); }
+ );
+ } else {
+ return nullptr;
+ }
+}
+
+#include "moc_matrix-session-controller-p.cpp"
diff --git a/src/matrix-session-controller.hpp b/src/matrix-session-controller.hpp
new file mode 100644
index 0000000..23c6c74
--- /dev/null
+++ b/src/matrix-session-controller.hpp
@@ -0,0 +1,104 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include "constants.hpp"
+#include "helper.hpp"
+#include <QObject>
+#include <QThread>
+#include <QPointer>
+#include <memory>
+
+namespace Kazv
+{
+ struct SdkModel;
+}
+
+class MatrixSession;
+class MatrixSessionControllerPrivate;
+
+std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId);
+
+/**
+ * Control the creation/loading/saving of a session.
+ *
+ * The controller object should be in the GUI thread.
+ * However, it owns objects that are in worker thread
+ * passed in as the argument of the constructor.
+ */
+class MatrixSessionController : public QObject
+{
+ Q_OBJECT
+
+public:
+ MatrixSessionController(
+ QThread *workerThread,
+ std::string userDataDir,
+ QObject *parent = nullptr
+ );
+
+ ~MatrixSessionController();
+
+ /**
+ * Get the MatrixSession representing the current session.
+ *
+ * If there is no current session, this returns nullptr.
+ */
+ Q_INVOKABLE MatrixSession *session() const;
+
+Q_SIGNALS:
+ void loadSessionFinished(
+ QString sessionName,
+ Constants::LoadSessionResult result
+ );
+
+ /**
+ * Signals that the session is ready.
+ *
+ * This is emitted after successfully loading a session or after
+ * creating a new session.
+ */
+ void sessionReady();
+
+public Q_SLOTS:
+ /**
+ * Load session at <userDataDir>/sessions/<sessionName> asynchronously.
+ *
+ * When the session is loaded or when there is an error, loadSessionFinished
+ * will be called with the result.
+ *
+ * @param sessionName A string in the form of <userid>/<deviceid> .
+ */
+ void load(QString sessionName);
+
+ /**
+ * Create a new empty session.
+ */
+ void create();
+
+ /**
+ * Create a new session with model m.
+ */
+ void createWithModel(const Kazv::SdkModel &m);
+
+ void save() const;
+
+ /**
+ * Prepare the MatrixSessionController for destruction.
+ *
+ * After calling this function, you can call deleteLater().
+ */
+ void prepareDestroy();
+
+ void startSyncing();
+
+private:
+ QPointer<QThread> m_thread;
+ std::string m_userDataDir;
+
+ UniquePtrDL<MatrixSessionControllerPrivate> m_d;
+};
diff --git a/src/matrix-session-types.hpp b/src/matrix-session-types.hpp
new file mode 100644
index 0000000..98c16ba
--- /dev/null
+++ b/src/matrix-session-types.hpp
@@ -0,0 +1,27 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include "qt-promise-handler.hpp"
+#include <lagerstoreeventemitter.hpp>
+#include <sdk.hpp>
+#include <lager/event_loop/qt.hpp>
+#include <QObject>
+
+// Sdk with qt event loop, identity transform and no enhancers
+using MatrixSessionSdkT =
+ decltype(Kazv::makeSdk(
+ Kazv::SdkModel{},
+ Kazv::detail::declref<Kazv::JobInterface>(),
+ Kazv::detail::declref<Kazv::EventInterface>(),
+ QtPromiseHandler(Kazv::detail::declref<QObject>()),
+ zug::identity,
+ withRandomGenerator(Kazv::detail::declref<Kazv::RandomInterface>())));
+
+using MatrixSessionContextT = decltype(std::declval<MatrixSessionSdkT>().context());
+
+using SecondaryRootT = decltype(std::declval<MatrixSessionSdkT>().createSecondaryRoot(std::declval<lager::with_qt_event_loop>()));
diff --git a/src/matrix-session.cpp b/src/matrix-session.cpp
new file mode 100644
index 0000000..526a465
--- /dev/null
+++ b/src/matrix-session.cpp
@@ -0,0 +1,597 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "matrix-session.hpp"
+#include "matrix-utils.hpp"
+#include "matrix-room-list.hpp"
+#include "matrix-promise.hpp"
+#include "matrix-event.hpp"
+#include "device-mgmt/matrix-device-list.hpp"
+#include "matrix-sticker-pack-list.hpp"
+#include "matrix-sticker-pack-list-p.hpp"
+#include "matrix-user-given-attrs-map.hpp"
+#include "matrix-verification-list.hpp"
+#include "sso-login-process.hpp"
+#include "db-store.hpp"
+#include "helper.hpp"
+#include "kazv-log.hpp"
+#include <csapi/login.hpp>
+#include <csapi/directory.hpp>
+#include <client/alias.hpp>
+#include <QFile>
+
+using namespace Qt::Literals::StringLiterals;
+using namespace Kazv;
+
+static const std::string clientName = "kazv";
+
+MatrixSession::MatrixSession(
+ MatrixSessionContextT context,
+ Client client,
+ LagerStoreEventEmitter::Watchable watchable,
+ lager::reader<VerificationTrackerModel> verificationTrackerState,
+ std::function<DbStore &()> dbStoreGetter,
+ QObject *parent
+)
+ : QObject(parent)
+ , m_context(std::move(context))
+ , m_clientOnSecondaryRoot(std::move(client))
+ , m_watchable(std::move(watchable))
+ , m_notificationHandler(m_clientOnSecondaryRoot.notificationHandler())
+ , m_verificationTrackerState(std::move(verificationTrackerState))
+ , m_ssoLoginProcess()
+ , m_dbStoreGetter(std::move(dbStoreGetter))
+ , LAGER_QT(serverUrl)(m_clientOnSecondaryRoot.serverUrl().xform(strToQt))
+ , LAGER_QT(userId)(m_clientOnSecondaryRoot.userId().xform(strToQt))
+ , LAGER_QT(token)(m_clientOnSecondaryRoot.token().xform(strToQt))
+ , LAGER_QT(deviceId)(m_clientOnSecondaryRoot.deviceId().xform(strToQt))
+ , LAGER_QT(specVersions)(m_clientOnSecondaryRoot.supportVersions())
+{
+ m_watchable.afterAll(
+ [this](KazvEvent e) {
+ Q_EMIT this->trigger(e);
+ });
+
+ m_watchable.after<LoginSuccessful>(
+ [this](LoginSuccessful e) {
+ Q_EMIT this->loginSuccessful(e);
+ });
+
+ m_watchable.after<LoginFailed>(
+ [this](LoginFailed e) {
+ Q_EMIT this->loginFailed(
+ QString::fromStdString(e.errorCode),
+ QString::fromStdString(e.error)
+ );
+ });
+
+ m_watchable.after<ReceivingRoomTimelineEvent>(
+ [this](ReceivingRoomTimelineEvent e) {
+ Q_EMIT this->receivedMessage(
+ QString::fromStdString(e.roomId),
+ QString::fromStdString(e.event.id())
+ );
+ });
+}
+
+MatrixSession::~MatrixSession() = default;
+
+QString MatrixSession::mxcUriToHttp(QString mxcUri) const
+{
+ return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
+}
+
+QString MatrixSession::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
+{
+ return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
+}
+
+MatrixDeviceList *MatrixSession::devicesOfUser(QString userId) const
+{
+ return new MatrixDeviceList(m_clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
+}
+
+bool isIllFormatSpecVersion(const QString &version)
+{
+ const bool isLegacy = version.startsWith(u"r"_s);
+ if (isLegacy && version.split(u'.').size() == 3) {
+ return false;
+ }
+ if (!isLegacy && version.split(u'.').size() == 2) {
+ return false;
+ }
+ return true;
+}
+
+// Return true if v1 is at least as new as v2, false if v1 is older than v2
+// Return false if v1 or v2 is ill-format
+bool compareSpecVersion(QString v1, QString v2)
+{
+ // Check parameters format
+ if (isIllFormatSpecVersion(v1) || isIllFormatSpecVersion(v2)) {
+ return false;
+ }
+
+ const bool v1IsLegacy = v1.startsWith(u"r"_s);
+ const bool v2IsLegacy = v2.startsWith(u"r"_s);
+ if (v1IsLegacy != v2IsLegacy) {
+ return v2IsLegacy;
+ }
+
+ v1.remove(0, 1);
+ v2.remove(0, 1);
+ auto v1VersionNumbers = v1.split(u'.');
+ auto v2VersionNumbers = v2.split(u'.');
+
+ for (int i = 0; i < v1VersionNumbers.size(); i++) {
+ auto v1VerNum = v1VersionNumbers[i].toInt();
+ auto v2VerNum = v2VersionNumbers[i].toInt();
+ if (v1VerNum != v2VerNum) {
+ return v1VerNum > v2VerNum;
+ }
+ }
+ // v1 is equal to v2
+ return true;
+}
+
+// Return true if version in the range [minVer, maxVer]
+// Return false if any parameter is ill-format
+bool compareSpecVersionRange(const QString &version,
+ const QString &minVer, const QString &maxVer)
+{
+ if (isIllFormatSpecVersion(version)
+ || isIllFormatSpecVersion(minVer)
+ || isIllFormatSpecVersion(maxVer)) {
+ return false;
+ }
+
+ if (compareSpecVersion(version, minVer) && compareSpecVersion(maxVer, version)) {
+ return true;
+ }
+
+ return false;
+}
+
+bool MatrixSession::checkSpecVersion(QString version) const
+{
+ return std::find_if(specVersions().begin(), specVersions().end(),
+ [&version](auto v) {
+ return compareSpecVersion(QString::fromStdString(v), version);
+ }) != specVersions().end();
+}
+
+bool MatrixSession::checkSpecVersionRange(QString minVer, QString maxVer) const
+{
+ return std::find_if(specVersions().begin(), specVersions().end(),
+ [&minVer, maxVer](auto v) {
+ return compareSpecVersionRange(QString::fromStdString(v), minVer, maxVer);
+ }) != specVersions().end();
+}
+
+QStringList MatrixSession::directRoomIds(QString userId) const
+{
+ auto content = m_clientOnSecondaryRoot.accountData().get()["m.direct"].content().get();
+ auto roomIds = QStringList{};
+ for (auto i : content[userId.toStdString()]) {
+ roomIds.push_back(QString::fromStdString(i.get<std::string>()));
+ }
+ return roomIds;
+}
+
+MatrixVerificationList *MatrixSession::verificationList() const
+{
+ return new MatrixVerificationList(m_clientOnSecondaryRoot, m_verificationTrackerState);
+}
+
+std::string MatrixSession::validateHomeserverUrl(const QString &url)
+{
+ if (url.isEmpty()) {
+ return std::string();
+ }
+
+ auto u = QUrl::fromUserInput(url);
+ if (!u.isValid()) {
+ return std::string();
+ }
+
+ if (u.scheme() == u"http"_s) {
+ qCInfo(kazvLog) << "url" << u << "is http. Force switching to https.";
+ u.setScheme(u"https"_s);
+ } else if (u.scheme() != u"https"_s) {
+ qCWarning(kazvLog) << "url" << u << "is not http/https.";
+ return std::string();
+ }
+
+ return u.toString().toStdString();
+}
+
+void MatrixSession::login(const QString &userId, const QString &password, const QString &homeserverUrl)
+{
+ auto loginFunc = [userId, password](const Client &client, const std::string &serverUrl) {
+ client.passwordLogin(
+ serverUrl,
+ userId.toStdString(),
+ password.toStdString(),
+ clientName
+ );
+ };
+
+ auto validated = validateHomeserverUrl(homeserverUrl);
+ if (!validated.empty()) {
+ loginFunc(m_clientOnSecondaryRoot, validated);
+ } else {
+ m_clientOnSecondaryRoot
+ .autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
+ .then([
+ this,
+ client=m_clientOnSecondaryRoot.toEventLoop(),
+ userId,
+ password,
+ loginFunc
+ ](auto res) {
+ if (!res.success()) {
+ // FIXME use real error codes and msgs when available in libkazv
+ Q_EMIT this->discoverFailed(u""_s, u""_s);
+ return res;
+ }
+
+ auto serverUrl = res.dataStr("homeserverUrl");
+
+ loginFunc(client, serverUrl);
+
+ return res;
+ });
+ }
+}
+
+void MatrixSession::discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl)
+{
+ auto validated = validateHomeserverUrl(homeserverUrl);
+ if (!validated.empty()) {
+ getLoginFlows(homeserverUrl);
+ } else {
+ m_clientOnSecondaryRoot
+ .autoDiscover(userId.toStdString())
+ .then([
+ this
+ ](const EffectStatus &res) {
+ if (!res.success()) {
+ Q_EMIT discoverFailed(
+ QString::fromStdString(res.dataStr("errorCode")),
+ QString::fromStdString(res.dataStr("error"))
+ );
+ return;
+ }
+ auto serverUrl = QString::fromStdString(res.dataStr("homeserverUrl"));
+ Q_EMIT discoverSuccessful(serverUrl);
+ getLoginFlows(serverUrl);
+ });
+ }
+}
+
+void MatrixSession::getLoginFlows(const QString &serverUrl)
+{
+ lager::get<JobInterface &>(m_context).submit(
+ Api::GetLoginFlowsJob(serverUrl.toStdString()),
+ [this](Api::GetLoginFlowsResponse r) {
+ if (!r.success()) {
+ Q_EMIT getLoginFlowsFailed(
+ QString::fromStdString(r.errorCode()),
+ QString::fromStdString(r.errorMessage())
+ );
+ return;
+ }
+ auto v = std::move(r).jsonBody().get().at("flows").template get<QJsonValue>();
+ Q_EMIT getLoginFlowsSuccessful(v);
+ }
+ );
+}
+
+QUrl MatrixSession::ssoLoginStart(const QString &homeserverUrl)
+{
+ if (m_ssoLoginProcess) {
+ m_ssoLoginProcess.reset();
+ }
+ m_ssoLoginProcess.reset(new SsoLoginProcess());
+ if (!m_ssoLoginProcess->startServer()) {
+ return QUrl();
+ }
+ auto link = m_ssoLoginProcess->getSsoLink(homeserverUrl);
+ connect(m_ssoLoginProcess.get(), &SsoLoginProcess::loginTokenAvailable,
+ this, [this, homeserverUrl](const QString &loginToken) {
+ Q_EMIT ssoLoginTokenAvailable();
+ qCInfo(kazvLog) << "Got SSO login token";
+ m_ssoLoginProcess.reset();
+ m_clientOnSecondaryRoot.mLoginTokenLogin(
+ homeserverUrl.toStdString(),
+ loginToken.toStdString(),
+ clientName
+ );
+ });
+ return link;
+}
+
+void MatrixSession::logout()
+{
+ m_clientOnSecondaryRoot.logout()
+ .then([&] (EffectStatus stat) {
+ if (stat.success()) {
+ Q_EMIT this->logoutSuccessful();
+ } else {
+ Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
+ }
+ });
+}
+
+MatrixRoomList *MatrixSession::roomList() const
+{
+ return new MatrixRoomList(m_clientOnSecondaryRoot);
+}
+
+static std::optional<std::string> optMaybe(QString s)
+{
+ if (s.isEmpty()) {
+ return std::nullopt;
+ } else {
+ return s.toStdString();
+ }
+}
+
+MatrixPromise *MatrixSession::createRoom(
+ bool isPrivate,
+ const QString &name,
+ const QString &alias,
+ const QStringList &invite,
+ bool isDirect,
+ bool allowFederate,
+ const QString &topic,
+ const QJsonValue &powerLevelContentOverride,
+ Constants::CreateRoomPreset preset,
+ bool encrypted
+)
+{
+ immer::array<Event> initialState;
+
+ if (encrypted) {
+ initialState = {Event{json{
+ {"type", "m.room.encryption"},
+ {"state_key", ""},
+ {"content", {
+ {"algorithm", "m.megolm.v1.aes-sha2"},
+ }},
+ }}};
+ }
+
+ return new MatrixPromise(m_clientOnSecondaryRoot.createRoom(
+ isPrivate ? Kazv::RoomVisibility::Private : Kazv::RoomVisibility::Public,
+ optMaybe(name),
+ optMaybe(alias),
+ qStringListToStdF(invite),
+ isDirect,
+ allowFederate,
+ optMaybe(topic),
+ nlohmann::json(powerLevelContentOverride),
+ static_cast<Kazv::CreateRoomPreset>(preset),
+ initialState
+ ));
+}
+
+MatrixPromise *MatrixSession::joinRoom(const QString &idOrAlias, const QStringList &servers)
+{
+ return new MatrixPromise(m_clientOnSecondaryRoot.joinRoom(
+ idOrAlias.toStdString(),
+ qStringListToStdF(servers)
+ ));
+}
+
+MatrixPromise *MatrixSession::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
+{
+ return new MatrixPromise(
+ m_clientOnSecondaryRoot.setDeviceTrustLevel(
+ userId.toStdString(),
+ deviceId.toStdString(),
+ qStringToTrustLevelFunc(trustLevel)
+ )
+ );
+}
+
+MatrixPromise *MatrixSession::getSelfProfile()
+{
+ return new MatrixPromise(
+ m_clientOnSecondaryRoot.getProfile(userId().toStdString())
+ );
+}
+
+MatrixPromise *MatrixSession::setDisplayName(QString displayName)
+{
+ return new MatrixPromise(
+ m_clientOnSecondaryRoot.setDisplayName(
+ displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
+ )
+ );
+}
+
+MatrixPromise *MatrixSession::setAvatarUrl(QString avatarUrl)
+{
+ return new MatrixPromise(
+ m_clientOnSecondaryRoot.setAvatarUrl(
+ avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
+ )
+ );
+}
+
+bool MatrixSession::shouldNotify(MatrixEvent *event) const
+{
+ // Do not notify own event
+ if (event->sender() == userId()) {
+ return false;
+ }
+ return m_notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
+}
+
+bool MatrixSession::shouldPlaySound(MatrixEvent *event) const
+{
+ return m_notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
+}
+
+MatrixStickerPackList *MatrixSession::stickerPackList() const
+{
+ return new MatrixStickerPackList(m_clientOnSecondaryRoot);
+}
+
+MatrixEvent *MatrixSession::stickerRoomsEvent() const
+{
+ return new MatrixEvent(m_clientOnSecondaryRoot.accountData()[imagePackRoomsEventType][lager::lenses::or_default]);
+}
+
+MatrixPromise *MatrixSession::updateStickerPack(MatrixStickerPackSource source)
+{
+ if (source.source == MatrixStickerPackSource::AccountData) {
+ auto eventJson = std::move(source.event).raw().get();
+ eventJson["type"] = source.eventType;
+ return sendAccountDataImpl(Event(std::move(eventJson)));
+ } else if (source.source == MatrixStickerPackSource::RoomState) {
+ auto eventJson = std::move(source.event).raw().get();
+ eventJson["type"] = source.eventType;
+ eventJson["state_key"] = source.stateKey;
+ return new MatrixPromise(
+ m_clientOnSecondaryRoot
+ .room(source.roomId)
+ .sendStateEvent(Event(std::move(eventJson))));
+ } else {
+ return 0;
+ }
+}
+
+MatrixUserGivenAttrsMap *MatrixSession::userGivenNicknameMap() const
+{
+ return new MatrixUserGivenAttrsMap(
+ userGivenNicknameMapFor(m_clientOnSecondaryRoot),
+ [client=m_clientOnSecondaryRoot](json content) {
+ return client.setAccountData(json{
+ {"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
+ {"content", std::move(content)},
+ });
+ }
+ );
+}
+
+MatrixPromise *MatrixSession::sendAccountData(const QString &type, const QJsonObject &content)
+{
+ Event e = json{
+ {"type", type.toStdString()},
+ {"content", content},
+ };
+ return sendAccountDataImpl(std::move(e));
+}
+
+MatrixPromise *MatrixSession::sendAccountDataImpl(Event event)
+{
+ return new MatrixPromise(m_clientOnSecondaryRoot.setAccountData(event));
+}
+
+MatrixPromise *MatrixSession::getSpecVersions()
+{
+ return new MatrixPromise(m_clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
+}
+
+MatrixPromise *MatrixSession::addDirectRoom(const QString &userId, const QString &roomId)
+{
+ return new MatrixPromise(m_clientOnSecondaryRoot.addDirectRoom(userId.toStdString(), roomId.toStdString()));
+}
+
+MatrixPromise *MatrixSession::getRoomIdByAlias(const QString &roomAlias)
+{
+ auto job = m_clientOnSecondaryRoot
+ .getRoomIdByAliasJob(roomAlias.toStdString());
+ return new MatrixPromise(
+ m_context.createPromise([ctx=m_context, job](auto resolve) {
+ lager::get<JobInterface &>(ctx).submit(job, [resolve](GetRoomIdByAliasResponse r) {
+ resolve(parseGetRoomIdByAliasResponse(r));
+ });
+ }));
+}
+
+inline constexpr std::size_t purgeEventsKeepNumber = 5;
+MatrixPromise *MatrixSession::purgeEventsExceptRooms(const QStringList &roomIds)
+{
+ return new MatrixPromise(
+ m_context.createResolvedPromise({})
+ .then([client=m_clientOnSecondaryRoot.toEventLoop(), roomIds=qStringListToStdF(roomIds)
+ ]([[maybe_unused]] auto &&stat) {
+ auto map = intoImmer(immer::map<std::string, std::size_t>{},
+ zug::filter([&roomIds](const std::string &roomId) {
+ return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
+ })
+ | zug::map([](const std::string &roomId) {
+ return std::make_pair(roomId, purgeEventsKeepNumber);
+ }),
+ client.roomIds().make().get()
+ );
+ return client.purgeRoomEvents(map);
+ }).then([](const auto &stat) {
+ qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
+ return stat;
+ })
+ );
+}
+
+MatrixPromise *MatrixSession::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
+{
+ return new MatrixPromise(
+ m_context.createPromise([
+ dbStoreGetter=m_dbStoreGetter,
+ client=m_clientOnSecondaryRoot.toEventLoop(),
+ roomId,
+ eventId
+ ](auto resolve) {
+ dbStoreGetter().getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
+ auto [timelineEvents, relatedEvents] = std::move(res);
+ auto loadedCount = timelineEvents[roomId.toStdString()].size();
+ client.loadEventsFromStorage(timelineEvents, relatedEvents)
+ .then([resolve, loadedCount](auto &&) {
+ resolve(EffectStatus(
+ !!loadedCount,
+ json{{"loadedCount", loadedCount}}
+ ));
+ });
+ });
+ })
+ );
+}
+
+MatrixPromise *MatrixSession::importFromKeyBackupFile(QUrl fileUrl, QString password)
+{
+ if (!fileUrl.isLocalFile()) {
+ return new MatrixPromise(m_context.createResolvedPromise({
+ /* succ = */ false,
+ json{{"error", "Not a local file"}, {"errorCode", "NOT_LOCAL_FILE"}},
+ }));
+ }
+ auto filename = fileUrl.toLocalFile();
+ auto f = QFile(filename);
+ if (!f.open(QFile::ReadOnly)) {
+ return new MatrixPromise(m_context.createResolvedPromise({
+ /* succ = */ false,
+ json{{"error", "File open failed"}, {"errorCode", "FILE_OPEN_FAILED"}},
+ }));
+ }
+ auto ba = f.readAll();
+ auto content = std::string(ba.begin(), ba.end());
+ auto passwordStd = std::move(password).toStdString();
+ return new MatrixPromise(m_clientOnSecondaryRoot.importFromKeyBackupFile(
+ std::move(content), std::move(passwordStd)
+ ));
+}
+
+MatrixPromise *MatrixSession::requestVerifyDevice(QString userId, QString deviceId)
+{
+ return new MatrixPromise(m_clientOnSecondaryRoot.requestOutgoingToDeviceVerification(
+ std::move(userId).toStdString(),
+ std::move(deviceId).toStdString()
+ ));
+}
diff --git a/src/matrix-sdk.hpp b/src/matrix-session.hpp
similarity index 70%
copy from src/matrix-sdk.hpp
copy to src/matrix-session.hpp
index 0d93cbf..21c0c89 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-session.hpp
@@ -1,397 +1,301 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
-
+#include "helper.hpp"
+#include "meta-types.hpp"
+#include "matrix-session-types.hpp"
+#include "constants.hpp"
+#include <client.hpp>
+#include <notification-handler.hpp>
+#include <lagerstoreeventemitter.hpp>
+#include <lager/extra/qt.hpp>
#include <QObject>
#include <QQmlEngine>
-#include <QString>
-
-#include <string>
-#include <memory>
-#include <filesystem>
-
-#include <lager/extra/qt.hpp>
-#include <immer/map.hpp>
-
-#include <sdk-model.hpp>
-#include <random-generator.hpp>
-#include "meta-types.hpp"
Q_MOC_INCLUDE("matrix-room-list.hpp")
Q_MOC_INCLUDE("matrix-device-list.hpp")
Q_MOC_INCLUDE("matrix-promise.hpp")
Q_MOC_INCLUDE("matrix-event.hpp")
Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
Q_MOC_INCLUDE("matrix-verification-list.hpp")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
-class MatrixSdkTest;
-class MatrixSdkSessionsTest;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
-class KazvSessionLockGuard;
-class DbStore;
class MatrixVerificationList;
-struct MatrixSdkPrivate;
-
-std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId);
+class SsoLoginProcess;
+class DbStore;
-class MatrixSdk : public QObject
+/**
+ * Represent all operations that can be taken within a Matrix session.
+ */
+class MatrixSession : public QObject
{
Q_OBJECT
QML_ELEMENT
+ QML_UNCREATABLE("")
- std::unique_ptr<MatrixSdkPrivate> m_d;
-
- /// @param d A dynamically allocated d-pointer, whose ownership
- /// will be transferred to this MatrixSdk.
- explicit MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent);
-
- void init();
+ MatrixSessionContextT m_context;
+ Kazv::Client m_clientOnSecondaryRoot;
+ Kazv::LagerStoreEventEmitter::Watchable m_watchable;
+ Kazv::NotificationHandler m_notificationHandler;
+ lager::reader<Kazv::VerificationTrackerModel> m_verificationTrackerState;
+ UniquePtrDL<SsoLoginProcess> m_ssoLoginProcess;
+ std::function<DbStore &()> m_dbStoreGetter;
public:
- enum CreateRoomPreset {
- PrivateChat = Kazv::CreateRoomPreset::PrivateChat,
- PublicChat = Kazv::CreateRoomPreset::PublicChat,
- TrustedPrivateChat = Kazv::CreateRoomPreset::TrustedPrivateChat,
- };
-
- Q_ENUM(CreateRoomPreset);
-
- enum LoadSessionResult {
- /// Successfully loaded the session
- SessionLoadSuccess,
- /// There is no store file
- SessionNotFound,
- /// The format of the store file is not supported
- SessionFormatUnknown,
- /// The store file cannot be backed up
- SessionCannotBackup,
- /// Cannot grab the lock on the session file
- SessionLockFailed,
- /// Cannot open store file
- SessionCannotOpenFile,
- /// Cannot deserialize the store file
- SessionDeserializeFailed,
- };
-
- Q_ENUM(LoadSessionResult);
-
- enum MediaDownloadEndpointVersion {
- UnauthenticatedMediaV3, // Classical endpoint series
- AuthenticatedMediaV1, // Endpoints that needs authorization
- };
-
- Q_ENUM(MediaDownloadEndpointVersion);
-
- explicit MatrixSdk(QObject *parent = 0);
- ~MatrixSdk() override;
+ MatrixSession(
+ MatrixSessionContextT context,
+ Kazv::Client client,
+ Kazv::LagerStoreEventEmitter::Watchable watchable,
+ lager::reader<Kazv::VerificationTrackerModel> verificationTrackerState,
+ std::function<DbStore &()> dbStoreGetter,
+ QObject *parent = nullptr
+ );
+ ~MatrixSession() override;
+
+ static std::string validateHomeserverUrl(const QString &url);
LAGER_QT_READER(QString, serverUrl);
LAGER_QT_READER(QString, userId);
LAGER_QT_READER(QString, token);
LAGER_QT_READER(QString, deviceId);
LAGER_QT_READER(immer::array<std::string>, specVersions); // The versions of the Matrix Spec supported by the server.
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
Q_INVOKABLE QString mxcUriToHttpAuthenticatedV1(QString mxcUri) const;
Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
// Return true if version is at least as new as the spec version of server
Q_INVOKABLE bool checkSpecVersion(QString version) const;
// Return true if the spec version of server in the range [minVer, maxVer]
Q_INVOKABLE bool checkSpecVersionRange(QString minVer, QString maxVer) const;
Q_INVOKABLE QStringList directRoomIds(QString userId) const;
Q_INVOKABLE MatrixVerificationList *verificationList() const;
- Kazv::RandomInterface &randomGenerator() const;
-
-private:
- // Replaces the store with another one
- void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard, std::unique_ptr<DbStore> dbStore);
-
- static std::string validateHomeserverUrl(const QString &url);
-
Q_SIGNALS:
void trigger(Kazv::KazvEvent e);
void loginSuccessful(Kazv::KazvEvent e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void discoverSuccessful(QString serverUrl);
void getLoginFlowsFailed(QString errorCode, QString errorMsg);
void getLoginFlowsSuccessful(QJsonValue flows);
void ssoLoginTokenAvailable();
void logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
- void sessionChanged();
-
- void loadSessionFinished(QString sessionName, MatrixSdk::LoadSessionResult result);
- void dbResolved(bool success);
-
public Q_SLOTS:
void login(const QString &userId, const QString &password, const QString &homeserverUrl);
/**
* Auto-discover the server url and then get the login flows from the server.
*
* If homeserverUrl is not provided, try to get it from auto-discovery.
* If discovery is successful, or it is already provided, get the login flows
* by calling getLoginFlows.
*/
void discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl);
void getLoginFlows(const QString &serverUrl);
/**
* Start SSO login flow.
*
* It will start an http server on a local port and pass it as
* the redirect url to the SSO login link.
*
* When the user completes the SSO login flow, we get the login token
* for us to login via the token flow.
*
* @return The SSO login link for the user to open.
*/
QUrl ssoLoginStart(const QString &homeserverUrl);
void logout();
- /**
- * Serialize data to <AppDataDir>/sessions/<userid>/<deviceid>/
- *
- * If not logged in, do nothing.
- */
- void serializeToFile() const;
-
- /**
- * Load session at <AppDataDir>/sessions/<sessionName> asynchronously.
- *
- * When the session is loaded or when there is an error, loadSessionFinished
- * will be called with the result.
- *
- * @param sessionName A string in the form of <userid>/<deviceid> .
- */
- void loadSession(QString sessionName);
-
- /**
- * Delete session at <AppDataDir>/sessions/<sessionName> .
- *
- * @param sessionName A string in the form of <userid>/<deviceid> .
- *
- * @return true if successful, false otherwise.
- */
- bool deleteSession(QString sessionName);
-
- /**
- * Start an empty session.
- *
- * The new session is not logged in, and need to call login().
- *
- * @return true if successful, false otherwise.
- */
- bool startNewSession();
-
- /**
- * Get all saved sessions.
- *
- * @return A list of session names in the form of <userid>/<deviceid> .
- */
- QStringList allSessions() const;
-
/**
* Create a new room.
*
* @param isPrivate Whether the room is private.
* @param name The room's name.
* @param alias The alias of the room.
* @param invite List of matrix ids of users to invite.
* @param isDirect Whether it is a direct message room.
* @param allowFederate Whether to allow users on other servers to join.
* @param topic The topic of the room.
* @param powerLevelContentOverride The content to override m.room.power_levels event.
* @param preset The preset to create the room with.
* @param encrypted Whether to enable encryption for this room.
*/
MatrixPromise *createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
- CreateRoomPreset preset,
+ Constants::CreateRoomPreset preset,
bool encrypted
);
/**
* Join a room.
* @param idOrAlias The id or alias of the room to join.
* @param servers The servers to use when joining the room.
*/
MatrixPromise *joinRoom(
const QString &idOrAlias,
const QStringList &servers
);
/**
* Change the trust level of a device.
*
* @param userId The user id that owns the device.
* @param deviceId The device id to set the trust level.
* @param trustLevel The trust level.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel);
/**
* Get the profile of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *getSelfProfile();
/**
* Set the display name of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDisplayName(QString displayName);
/**
* Set the avatar url of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setAvatarUrl(QString avatarUrl);
/**
* Check if an event should be notified.
*
* @param event The event to check.
* @return Whether `event` should be notified.
*/
bool shouldNotify(MatrixEvent *event) const;
/**
* Check if an event should be notified with sound.
*
* You should only call this method when `shouldNotify(event)`
* returns true.
*
* @param event The event to check.
* @return Whether `event` should be notified with sound.
*/
bool shouldPlaySound(MatrixEvent *event) const;
/**
* Get the sticker pack list for the current account.
*
* @return A list of sticker packs associated with the current account.
*/
MatrixStickerPackList *stickerPackList() const;
/**
* Get the sticker rooms account data event for the current account.
*
* @return A MatrixEvent representing the sticker rooms account data event.
*/
MatrixEvent *stickerRoomsEvent() const;
/**
* Update the sticker pack from source.
*
* @param source The source of the sticker pack to update.
* @return A promise that resolves when the sticker pack is updated,
* or when there is an error.
*/
MatrixPromise *updateStickerPack(MatrixStickerPackSource source);
MatrixUserGivenAttrsMap *userGivenNicknameMap() const;
MatrixPromise *sendAccountData(const QString &type, const QJsonObject &content);
/**
* Get all Matrix Spec versions supported by the server.
* Use MatrixSdk::supportSpecVersion() to check if a version is supported.
*/
MatrixPromise *getSpecVersions();
MatrixPromise *addDirectRoom(const QString &userId, const QString &roomId);
MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
/**
* Purge events in all rooms except those specified in roomIds
*
* @param roomIds The ids for rooms to NOT be purged. This allows
* keeping the rooms that are already open intact.
* @return A MatrixPromise that resolves when the events are purged.
*/
MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
/**
* Backfill from the event storage from an existing event in a room.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to backfill from.
* @return A MatrixPromise that resolves when the events are backfilled.
* The Promise is considered successful if and only if at least one event
* is loaded.
*/
MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
/**
* Import from key backup file.
*
* @param fileUrl The url of the key backup file. Will be passed to
* QUrl::toLocalFile(), and must be a local file.
* @param password The password to decrypt the key backup file.
* @return A MatrixPromise that resolves when the keys are imported.
* If successful, `data["imported"]` contains the number of imported keys.
* If unsuccessful, `data` contains a standard error structure.
*/
MatrixPromise *importFromKeyBackupFile(QUrl fileUrl, QString password);
/**
* Send an outbound device verification request.
*
* @param userId The user id of the device to verify.
* @param deviceId The device id of the device to verify.
*/
MatrixPromise *requestVerifyDevice(QString userId, QString deviceId);
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
-
-private: // Testing
- friend MatrixSdkTest;
- friend MatrixSdkSessionsTest;
- friend MatrixSdk *makeTestSdk(Kazv::SdkModel model);
-
- void setUserDataDir(const std::string &userDataDir);
-
- explicit MatrixSdk(Kazv::SdkModel model, bool testing = false, QObject *parent = 0);
- void startThread();
};
diff --git a/src/qt-promise-handler.hpp b/src/qt-promise-handler.hpp
index 93d63c1..6206903 100644
--- a/src/qt-promise-handler.hpp
+++ b/src/qt-promise-handler.hpp
@@ -1,253 +1,253 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
-
+#include "kazv-log.hpp"
#include <future>
#include <QObject>
#include <QMetaObject>
#include <QPointer>
#include <QTimer>
#include <promise-interface.hpp>
template<class T>
class QtPromise;
namespace QtPromiseDetail
{
struct QtPromiseHelper
{
template<class T>
using PromiseType = QtPromise<T>;
};
struct IdentityFunc
{
template<class T>
constexpr T &&operator()(T &&t) const {
return std::forward<T>(t);
}
};
template<class Func>
void post(QObject *obj, Func &&func)
{
QMetaObject::invokeMethod(obj, std::forward<Func>(func), Qt::QueuedConnection);
}
}
class QtPromiseSignalTrigger : public QObject
{
Q_OBJECT
public:
QtPromiseSignalTrigger(QObject *parent = 0);
~QtPromiseSignalTrigger();
Q_SIGNALS:
void finished();
};
template<class T>
class QtPromise : public Kazv::AbstractPromise<QtPromiseDetail::QtPromiseHelper::PromiseType, T>
{
using BaseT = Kazv::AbstractPromise<QtPromiseDetail::QtPromiseHelper::PromiseType, T>;
template<class FuncT, class PromiseT, class ResolveT>
struct WaitHelper
{
void resolveOrWait()
{
auto res = func(p.get());
using ResT = decltype(res);
if constexpr (Kazv::isPromise<ResT>) {
res.then([resolve=resolve](auto val) {
resolve(std::move(val));
return std::decay_t<decltype(val)>(); // we don't care about this value
});
} else {
resolve(res);
}
}
void wait()
{
if (p.ready()) {
QtPromiseDetail::post(
executor,
[*this]() mutable { resolveOrWait(); }
);
} else {
QObject::connect(
p.m_signalTrigger, &QtPromiseSignalTrigger::finished,
executor, [*this]() mutable {
resolveOrWait();
}
);
}
}
QPointer<QObject> executor;
PromiseT p;
FuncT func;
ResolveT resolve;
};
struct ResolveHelper
{
template<class ValT>
void operator()(ValT val) const
{
using ResT = std::decay_t<decltype(val)>;
if constexpr (Kazv::isPromise<ResT>) {
QtPromiseDetail::post(
executor,
[qtPromise=std::move(val), *this]() mutable {
if (qtPromise.ready()) {
setValue(qtPromise.get());
} else {
qtPromise.then([*this](auto val) mutable {
setValue(std::move(val));
return std::decay_t<decltype(val)>(); // we don't care about this value
});
}
});
} else {
QtPromiseDetail::post(
executor,
[*this, val=std::move(val)]() mutable {
setValue(std::move(val));
});
}
}
void setValue(T resolvedValue) const
{
p->set_value(std::move(resolvedValue));
if (signalTrigger) {
Q_EMIT signalTrigger->finished();
signalTrigger->deleteLater();
}
}
QPointer<QObject> executor;
QPointer<QtPromiseSignalTrigger> signalTrigger;
std::shared_ptr<std::promise<T>> p;
};
public:
QtPromise(QObject *executor, T value)
: BaseT(this)
, m_executor(executor)
, m_signalTrigger()
{
std::promise<T> p;
m_val = p.get_future().share();
p.set_value(std::move(value));
}
template<class Func>
QtPromise(QObject *executor, Func &&callback)
: BaseT(this)
, m_executor(executor)
, m_signalTrigger(new QtPromiseSignalTrigger())
{
auto p = std::make_shared<std::promise<T>>();
m_val = p->get_future().share();
m_signalTrigger->moveToThread(m_executor->thread());
auto resolve = ResolveHelper{m_executor, m_signalTrigger, p};
QtPromiseDetail::post(
m_executor,
[=, callback=std::forward<Func>(callback),
resolve=std::move(resolve)]() {
callback(resolve);
});
}
// FuncT: (DataT) -> AnotherDataT
// where AnotherDataT = PromiseThenResult<FuncT, typename BaseT::DataT>
template<class FuncT>
auto then(FuncT &&func)
-> QtPromise<Kazv::PromiseThenResult<FuncT, typename BaseT::DataT>> {
return QtPromise<Kazv::PromiseThenResult<FuncT, typename BaseT::DataT>>(
m_executor,
[=, func=std::forward<FuncT>(func), *this](auto resolve) {
auto waitHelper = WaitHelper<std::decay_t<FuncT>,
QtPromise,
std::decay_t<decltype(resolve)>>{
m_executor,
*this,
func,
resolve
};
waitHelper.wait();
});
}
bool ready() const {
return m_val.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}
T get() const {
return m_val.get();
}
private:
QPointer<QObject> m_executor;
QPointer<QtPromiseSignalTrigger> m_signalTrigger;
std::shared_future<T> m_val;
};
class QtPromiseHandler : public Kazv::PromiseInterface<QtPromiseHandler,
QtPromiseDetail::QtPromiseHelper::PromiseType>
{
using BaseT = Kazv::PromiseInterface<QtPromiseHandler,
QtPromiseDetail::QtPromiseHelper::PromiseType>;
public:
template<class T>
using PromiseT = QtPromise<T>;
QtPromiseHandler(std::reference_wrapper<QObject> executor)
: BaseT(this)
, m_executor(&executor.get())
{}
QtPromiseHandler(const QtPromiseHandler &that)
: BaseT(this)
, m_executor(that.m_executor)
{}
QtPromiseHandler(QtPromiseHandler &&that)
: BaseT(this)
, m_executor(std::move(that.m_executor))
{}
QtPromiseHandler &operator=(const QtPromiseHandler &that) {
m_executor = that.m_executor;
return *this;
}
QtPromiseHandler &operator=(QtPromiseHandler &&that) {
m_executor = std::move(that.m_executor);
return *this;
}
template<class T, class FuncT>
PromiseT<T> create(FuncT &&func) {
return PromiseT<T>(m_executor, std::forward<FuncT>(func));
}
template<class T>
PromiseT<T> createResolved(T val) {
return PromiseT<T>(m_executor, std::move(val));
}
private:
QPointer<QObject> m_executor;
};
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 1c53144..6060f7b 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,151 +1,152 @@
set(KAZV_TEST_TEMP_DIR ${CMAKE_CURRENT_BINARY_DIR}/test-temp)
configure_file(test-temp.hpp.in test-temp.hpp)
set(kazvtestlib_SRCS
test-model.cpp
)
add_library(kazvtestlib STATIC ${kazvtestlib_SRCS})
target_include_directories(kazvtestlib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(kazvtestlib PUBLIC kazvprivlib libkazv::kazvtestfixtures)
add_executable(matrix-sdk-session-loader matrix-sdk-session-loader.cpp matrix-sdk-sessions-test.cpp)
target_compile_definitions(matrix-sdk-session-loader PRIVATE MATRIX_SDK_SESSIONS_TEST_NO_MAIN)
target_link_libraries(matrix-sdk-session-loader Qt${QT_MAJOR_VERSION}::Test kazvtestlib)
ecm_add_tests(
qt-job-handler-test.cpp
qt-promise-handler-test.cpp
qt-json-test.cpp
device-mgmt-test.cpp
- matrix-sdk-test.cpp
+ matrix-session-test.cpp
+ matrix-session-controller-test.cpp
matrix-sdk-sessions-test.cpp
matrix-promise-test.cpp
matrix-room-timeline-test.cpp
matrix-room-member-list-model-test.cpp
matrix-room-list-test.cpp
matrix-room-state-test.cpp
kazv-markdown-test.cpp
matrix-sticker-pack-test.cpp
matrix-link-test.cpp
matrix-user-given-attrs-map-test.cpp
kazv-abstract-list-model-test.cpp
matrix-event-list-test.cpp
matrix-event-test.cpp
kazv-file-test.cpp
db-store-test.cpp
sso-login-process-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
ecm_add_test(
kazv-io-job-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::HttpServer kazvtestlib
)
ecm_add_test(
kazv-io-manager-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::HttpServer kazvtestlib
)
add_executable(quicktest quick-test.cpp)
target_link_libraries(quicktest PRIVATE Qt${QT_MAJOR_VERSION}::QuickTest kazvtestlib)
if(KAZV_BUILD_BENCHMARKS)
ecm_add_test(
matrix-room-timeline-benchmark-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
endif()
set(quicktest_QMLS
tst_About.qml
tst_AddStickerPopup.qml
tst_AvatarImageAdapters.qml
tst_ConfirmUploadPopup.qml
tst_CreateRoomPage.qml
tst_DeviceList.qml
tst_DevicePopup.qml
tst_Device.qml
tst_EventHistoryView.qml
tst_EventReactions.qml
tst_EventReadIndicator.qml
tst_EventSourceView.qml
tst_EventView.qml
tst_EventViewRedacted.qml
tst_EventViewVideo.qml
tst_FileHandler.qml
tst_JoinRoomPage.qml
tst_KazvIOMenu.qml
tst_kazvshortcutsActionItem.qml
tst_LoginPage.qml
tst_MatrixHelpers.qml
tst_Notifications.qml
tst_PageManager.qml
tst_ReactToEventPopup.qml
tst_RoomInvitePage.qml
tst_RoomListViewItemDelegate.qml
tst_RoomMemberListPage.qml
tst_RoomNameProvider.qml
tst_RoomPage.qml
tst_RoomPinnedEventsPage.qml
tst_RoomSettingsPage.qml
tst_RoomStatePage.qml
tst_RoomStickerPackItemDelegate.qml
tst_RoomStickerPacksPage.qml
tst_RoomTimelineView.qml
tst_SendMessageBoxDrafts.qml
tst_SendMessageBox.qml
tst_StickerPackNameProvider.qml
tst_TypingIndicator.qml
tst_UserNameProvider.qml
tst_UserPage.qml
)
foreach(testfilename ${quicktest_QMLS})
message(STATUS "Adding quick test ${testfilename}")
string(REPLACE .qml "" testname ${testfilename})
add_test(NAME ${testname} COMMAND quicktest -input ${CMAKE_CURRENT_SOURCE_DIR}/quick-tests/${testfilename})
set_property(TEST ${testname}
PROPERTY LABELS "quicktest")
endforeach()
if(kazv_ENABLE_APPIUM_TESTS)
qt_add_qml_module(kazvappiumcomponentqmlmodule
STATIC
URI moe.kazv.mxc.kazvappiumcomponent
VERSION 0.0
)
kazv_add_qml_files(kazvappiumcomponentqmlmodule
BASE_DIRECTORY run-appium-component-qml
SOURCES
Main.qml
)
add_executable(run-appium-component run-appium-component.cpp)
target_link_libraries(run-appium-component
PRIVATE
kazvappiumcomponentqmlmodule
kazvappiumcomponentqmlmoduleplugin
kazvtestlib
Qt::Quick
)
set(appium_tests_SOURCES
TestStickerPicker.py
)
foreach(testfilename ${appium_tests_SOURCES})
message(STATUS "Adding appium test ${testfilename}")
string(REPLACE .py "" testname ${testfilename})
add_test(NAME ${testname}
COMMAND
${SELENIUM_WEBDRIVER_AT_SPI_RUN_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/appium-tests/${testfilename}
)
set_property(TEST ${testname}
PROPERTY ENVIRONMENT "KAZV_APPIUM_TEST_BIN=$<TARGET_FILE:run-appium-component>;TEST_WITH_VIDEO_RECORDER=0")
set_property(TEST ${testname}
PROPERTY LABELS "appiumtest")
endforeach()
endif()
diff --git a/src/tests/kazv-io-manager-test.cpp b/src/tests/kazv-io-manager-test.cpp
index af9087c..e89ac02 100644
--- a/src/tests/kazv-io-manager-test.cpp
+++ b/src/tests/kazv-io-manager-test.cpp
@@ -1,190 +1,190 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-manager.hpp"
#include "test-model.hpp"
#include "test-utils.hpp"
#include <QObject>
#include <QtTest>
#include <QHttpServer>
#include <QTcpServer>
#include <QThread>
#include <QTemporaryFile>
#include <QString>
#include <QHttpServerResponder>
using namespace Qt::Literals::StringLiterals;
static const QString downloadEndpoint =
u"/_matrix/client/v1/media/download/serverName/download"_s;
static const char *downloadFileContent = "download";
class KazvIOManagerTest : public QObject
{
Q_OBJECT
private:
QHttpServer httpServer;
QTcpServer tcpServer; // Required by QHttpServer
QThread serverThread;
quint16 port;
QTemporaryFile downloadFile;
QString serverUrl;
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testCache();
void testDownload();
void testUpload();
void testRoomlessUpload();
void testClearJobs();
Q_SIGNALS:
void readyResume();
void readyPause();
};
void KazvIOManagerTest::initTestCase()
{
downloadFile.open();
downloadFile.write(downloadFileContent);
downloadFile.close();
// QHttpServer::route() requires QHttpServerResponder must be passed by universal reference before Qt6.8
// https://doc.qt.io/qt-6.5/qhttpserver.html#route
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
using QHttpServerResponderRef = QHttpServerResponder &&;
#else
using QHttpServerResponderRef = QHttpServerResponder &;
#endif
httpServer.route(downloadEndpoint, [this](QHttpServerResponderRef res) {
QSignalSpy qs{this, &KazvIOManagerTest::readyResume};
Q_EMIT readyPause();
QVERIFY(qs.wait());
downloadFile.open();
res.write(downloadFile.readAll(), "application/octet-stream"_ba);
downloadFile.close();
});
QVERIFY(tcpServer.listen());
httpServer.bind(&tcpServer);
port = tcpServer.serverPort();
serverUrl = u"http://localhost:"_s + QString::number(port);
httpServer.moveToThread(&serverThread);
serverThread.start();
}
void KazvIOManagerTest::cleanupTestCase()
{
serverThread.quit();
serverThread.wait();
}
void KazvIOManagerTest::testCache()
{
KazvIOManager manager;
auto url = QUrl{serverUrl + downloadEndpoint};
auto id = u"id"_s;
QSignalSpy qsPause{this, &KazvIOManagerTest::readyPause};
manager.cacheFile(url, id);
QVERIFY(qsPause.wait());
auto job = manager.getCacheJob(id);
QVERIFY(job != nullptr);
// Multiple calls do not repeat downloads
manager.cacheFile(QUrl{u"url"_s}, id);
QCOMPARE(manager.getCacheJob(id), job);
QSignalSpy qs{job, &KazvIOBaseJob::result};
Q_EMIT readyResume();
// Cache job is automatically destroyed
QVERIFY(qs.wait());
QCOMPARE(manager.getCacheJob(id), nullptr);
}
void KazvIOManagerTest::testDownload()
{
KazvIOManager manager;
auto id = u"id"_s;
manager.startNewDownloadJob(
QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s}, id, u"hash"_s);
QVERIFY(manager.getDownloadJob(id) != nullptr);
manager.deleteDownloadJob(id);
QCOMPARE(manager.getDownloadJob(id), nullptr);
}
void KazvIOManagerTest::testUpload()
{
auto model = makeTestModel();
Kazv::RoomModel room;
room.roomId = "!test:tusooa.xyz";
model.client.roomList.rooms =
model.client.roomList.rooms.set(room.roomId, room);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
KazvIOManager manager;
auto job = manager.startNewUploadJob(QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s},
u"token"_s, QString::fromStdString(room.roomId), roomList.get(), false,
u"relType"_s, u"relatedTo"_s);
auto roomId = QString::fromStdString(room.roomId);
auto uploadModel = QPointer{manager.getUploadJobs(roomId)};
QCOMPARE(uploadModel->rowCount(), 1);
manager.deleteUploadJob(roomId, job);
QCOMPARE(uploadModel->rowCount(), 0);
manager.deleteModelIfEmpty(roomId);
QVERIFY(uploadModel.isNull());
}
void KazvIOManagerTest::testRoomlessUpload()
{
const auto ROOMLESS = u"not-a-room"_s;
KazvIOManager manager;
auto job = manager.startNewRoomlessUploadJob(QUrl{u"serverUrl"_s},
QUrl{u"localFileUrl"_s}, u"token"_s);
auto uploadModel = QPointer{manager.getUploadJobs(ROOMLESS)};
QCOMPARE(uploadModel->rowCount(), 1);
manager.deleteRoomlessUploadJob(job);
QCOMPARE(uploadModel->rowCount(), 0);
manager.deleteModelIfEmpty(ROOMLESS);
QVERIFY(uploadModel.isNull());
}
void KazvIOManagerTest::testClearJobs()
{
KazvIOManager manager;
auto id = u"id"_s;
manager.startNewDownloadJob(
QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s}, id, u"hash"_s);
QVERIFY(manager.getDownloadJob(id) != nullptr);
auto model = makeTestModel();
Kazv::RoomModel room;
room.roomId = "!test:tusooa.xyz";
model.client.roomList.rooms =
model.client.roomList.rooms.set(room.roomId, room);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
manager.startNewUploadJob(QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s},
u"token"_s, QString::fromStdString(room.roomId), roomList.get(), false,
u"relType"_s, u"relatedTo"_s);
auto roomId = QString::fromStdString(room.roomId);
QCOMPARE(manager.getUploadJobs(roomId)->rowCount(), 1);
manager.clearJobs();
QCOMPARE(manager.getDownloadJob(id), nullptr);
QCOMPARE(manager.getUploadJobs(roomId)->rowCount(), 0);
}
QTEST_MAIN(KazvIOManagerTest)
#include "kazv-io-manager-test.moc"
diff --git a/src/tests/matrix-event-test.cpp b/src/tests/matrix-event-test.cpp
index cc83932..80b9140 100644
--- a/src/tests/matrix-event-test.cpp
+++ b/src/tests/matrix-event-test.cpp
@@ -1,169 +1,169 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <QtTest>
#include <QSignalSpy>
#include <lager/state.hpp>
#include <testfixtures/factory.hpp>
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event-list.hpp>
#include <matrix-event.hpp>
#include <matrix-event-reaction-list-model.hpp>
#include <matrix-event-reaction.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixEventTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testEdits();
void testEncryptedEdits();
void testReactions();
};
void MatrixEventTest::testEdits()
{
auto r = makeRoom(withRoomTimeline({
makeEvent(withEventId("$0")),
makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0") | withEventKV("/origin_server_ts"_json_pointer, 1720751684500)),
// this one is invalid because there is no m.new_content
makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684700)),
makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684800)),
makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684900)),
}));
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
auto event = toUniquePtr(timeline->at(3));
QCOMPARE(event->eventId(), u"$1"_s);
auto history = toUniquePtr(event->history());
// three versions including the original one
QCOMPARE(history->count(), 3);
QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
auto v1 = toUniquePtr(history->at(1));
QCOMPARE(v1->content()[u"body"_s], QStringLiteral("second"));
QCOMPARE(v1->eventId(), QStringLiteral("$3"));
QCOMPARE(v1->underlyingEvent().originServerTs(), 1720751684800);
QCOMPARE(v1->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
auto v2 = toUniquePtr(history->at(2));
QCOMPARE(v2->content()[u"body"_s], QStringLiteral("third"));
QCOMPARE(v2->eventId(), QStringLiteral("$4"));
QCOMPARE(v2->underlyingEvent().originServerTs(), 1720751684900);
QCOMPARE(v2->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
}
void MatrixEventTest::testEncryptedEdits()
{
auto r = makeRoom();
auto events = EventList{
makeEvent(withEventId("$0") | withEventType("m.room.encrypted")).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.room.message"},
{"content", {{"body", "first"}}},
}, Event::Decrypted),
makeEvent(withEventId("$1") | withEventType("m.room.encrypted") | withEventRelationship("m.replace", "$0")).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.room.message"},
{"content", {{"m.new_content", {{"body", "second"}}}}},
}, Event::Decrypted),
};
withRoomTimeline(events)(r);
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
auto event = toUniquePtr(timeline->at(1));
QCOMPARE(event->eventId(), u"$0"_s);
auto history = toUniquePtr(event->history());
// three versions including the original one
QCOMPARE(history->count(), 2);
QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
auto v1 = toUniquePtr(history->at(1));
QCOMPARE(v1->sender(), QString::fromStdString(events[1].sender()));
QCOMPARE(v1->content()[u"body"_s], QStringLiteral("second"));
QCOMPARE(v1->eventId(), QStringLiteral("$1"));
QCOMPARE(v1->relationType(), QStringLiteral());
}
void MatrixEventTest::testReactions()
{
auto r = makeRoom();
auto events = EventList{
makeEvent(withEventId("$0") | withEventType("m.room.encrypted")).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.room.message"},
{"content", {{"body", "first"}}},
}, Event::Decrypted),
makeEvent(withEventId("$1")
| withEventType("m.room.encrypted")
| withEventRelationship("m.annotation", "$0")
| withEventKV("/content/m.relates_to/key"_json_pointer, "mew")
).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.reaction"},
}, Event::Decrypted),
makeEvent(withEventId("$2")
| withEventType("m.room.encrypted")
| withEventRelationship("m.annotation", "$0")
| withEventKV("/content/m.relates_to/key"_json_pointer, "mew")
).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.reaction"},
}, Event::Decrypted),
makeEvent(withEventId("$3")
| withEventType("m.room.encrypted")
| withEventRelationship("m.annotation", "$0")
| withEventKV("/content/m.relates_to/key"_json_pointer, "meww")
).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.reaction"},
}, Event::Decrypted),
};
withRoomTimeline(events)(r);
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
auto event = toUniquePtr(timeline->at(events.size() - 1));
QCOMPARE(event->eventId(), u"$0"_s);
auto reactions = toUniquePtr(event->reactions());
QCOMPARE(reactions->count(), 2);
auto r0 = toUniquePtr(reactions->at(0));
auto r1 = toUniquePtr(reactions->at(1));
QVERIFY(r0->count() == 1 || r1->count() == 1);
QVERIFY(r0->count() == 2 || r1->count() == 2);
QVERIFY(r0->key() == u"mew"_s || r1->key() == u"mew"_s);
QVERIFY(r0->key() == u"meww"_s || r1->key() == u"meww"_s);
}
QTEST_MAIN(MatrixEventTest)
#include "matrix-event-test.moc"
diff --git a/src/tests/matrix-room-list-test.cpp b/src/tests/matrix-room-list-test.cpp
index 05e77a1..b191cc0 100644
--- a/src/tests/matrix-room-list-test.cpp
+++ b/src/tests/matrix-room-list-test.cpp
@@ -1,249 +1,249 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event.hpp>
#include <matrix-utils.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
#include "factory.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixRoomListTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testRoomList();
void testSorted();
void testSortedWithTag();
void testFilter();
void testPriority();
void testExist();
};
static auto tagEvent = Event{R"({
"content": {
"tags": {"m.favourite": {}}
},
"type": "m.tag"
})"_json};
void MatrixRoomListTest::testRoomList()
{
auto model = makeTestModel();
RoomModel room;
room.roomId = "!test:tusooa.xyz";
room.accountData = room.accountData.set("m.tag", tagEvent);
model.client.roomList.rooms = model.client.roomList.rooms.set(room.roomId, room);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
QCOMPARE(roomList->count(), 2);
roomList->setTagId(u"m.favourite"_s);
QCOMPARE(roomList->count(), 1);
roomList->setTagId(u"u.xxx"_s);
QCOMPARE(roomList->count(), 0);
}
void MatrixRoomListTest::testSorted()
{
auto model = makeTestModel();
model.client = makeClient();
auto room1 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1000)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 2000))
})
| withRoomMembership(Join)
);
auto room2 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 700)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1500))
})
| withRoomMembership(Join)
);
auto room3 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1500)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 2500))
})
| withRoomMembership(Join)
);
auto room4 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 300)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 500))
})
| withRoomMembership(Invite)
);
withRoom(room1)(model.client);
withRoom(room2)(model.client);
withRoom(room3)(model.client);
withRoom(room4)(model.client);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room4.roomId));
QCOMPARE(roomList->roomIdAt(1), QString::fromStdString(room3.roomId));
QCOMPARE(roomList->roomIdAt(2), QString::fromStdString(room1.roomId));
QCOMPARE(roomList->roomIdAt(3), QString::fromStdString(room2.roomId));
}
void MatrixRoomListTest::testSortedWithTag()
{
auto model = makeTestModel();
model.client = makeClient();
auto room1 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1000)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 2000))
})
| withRoomAccountData({tagEvent})
);
auto room2 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 700)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1500))
})
| withRoomAccountData({tagEvent})
);
auto room3 = makeRoom(
withRoomTimeline({
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 1500)),
makeEvent(withEventKV("/origin_server_ts"_json_pointer, 2500))
})
| withRoomAccountData({tagEvent})
);
withRoom(room1)(model.client);
withRoom(room2)(model.client);
withRoom(room3)(model.client);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
QCOMPARE(roomList->roomIdAt(1), QString::fromStdString(room1.roomId));
QCOMPARE(roomList->roomIdAt(2), QString::fromStdString(room2.roomId));
}
void MatrixRoomListTest::testFilter()
{
auto model = makeTestModel();
model.client = makeClient();
auto room1 = makeRoom(
withRoomState({makeEvent(withStateKey("") | withEventType("m.room.name") | withEventContent(json{{"name", "some room"}}))})
);
auto room2 = makeRoom(
withRoomState({makeEvent(withStateKey("") | withEventType("m.room.name") | withEventContent(json{{"name", "some other room"}}))})
);
auto room3 = makeRoom(
withRoomId("!some:example.org")
| withRoomState({
makeMemberEvent(withStateKey("@foo:tusooa.xyz") | withEventKV(json::json_pointer("/content/displayname"), "User aaa")),
makeMemberEvent(withStateKey("@bar:tusooa.xyz") | withEventKV(json::json_pointer("/content/displayname"), "User bbb")),
})
);
room3.heroIds = immer::flex_vector<std::string>{"@foo:tusooa.xyz", "@bar:tusooa.xyz"};
withRoom(room1)(model.client);
withRoom(room2)(model.client);
withRoom(room3)(model.client);
auto nicknameEvent = makeEvent(
withEventType(USER_GIVEN_NICKNAME_EVENT_TYPES[0])
| withEventContent(json{{"@foo:tusooa.xyz", "xxxx"}})
);
withAccountData({ nicknameEvent })(model.client);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
roomList->setfilter(u"some"_s);
QCOMPARE(roomList->count(), 2);
QCOMPARE(roomList->roomIds().contains(QString::fromStdString(room2.roomId)), true);
QCOMPARE(roomList->roomIds().contains(QString::fromStdString(room1.roomId)), true);
roomList->setfilter(u"!some:example.org"_s);
QCOMPARE(roomList->count(), 1);
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
roomList->setfilter(u"foo"_s);
QCOMPARE(roomList->count(), 1);
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
roomList->setfilter(u"aaa"_s);
QCOMPARE(roomList->count(), 1);
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
// By custom nickname of user (@foo:tusooa.xyz)
roomList->setfilter(u"xxxx"_s);
QCOMPARE(roomList->count(), 1);
QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
}
void MatrixRoomListTest::testPriority()
{
auto model = makeTestModel();
RoomModel roomFavourited = makeRoom(
withRoomId("!favourite:tusooa.xyz")
| withRoomAccountData(immer::flex_vector{
makeEvent(withEventContent(tagEvent))}));
RoomModel roomInvited = makeRoom(
withRoomId("!invite:tusooa.xyz")
| withRoomMembership(RoomMembership::Invite));
model.client.roomList.rooms = model.client.roomList.rooms.set(
roomInvited.roomId, roomInvited);
model.client.roomList.rooms = model.client.roomList.rooms.set(
roomFavourited.roomId, roomFavourited);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
QCOMPARE(roomList->count(), 3);
// Priority: Invite > Favourite
QCOMPARE(roomList->roomIdAt(0), u"!invite:tusooa.xyz"_s);
QCOMPARE(roomList->roomIdAt(1), u"!favourite:tusooa.xyz"_s);
}
void MatrixRoomListTest::testExist()
{
auto model = makeTestModel();
RoomModel room;
room.roomId = "!test:tusooa.xyz";
model.client.roomList.rooms = model.client.roomList.rooms.set(room.roomId, room);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
QVERIFY(roomList->contains(u"!test:tusooa.xyz"_s));
QVERIFY(!roomList->contains(u"!notexist:tusooa.xyz"_s));
}
QTEST_MAIN(MatrixRoomListTest)
#include "matrix-room-list-test.moc"
diff --git a/src/tests/matrix-room-state-test.cpp b/src/tests/matrix-room-state-test.cpp
index f197a4f..d265b7a 100644
--- a/src/tests/matrix-room-state-test.cpp
+++ b/src/tests/matrix-room-state-test.cpp
@@ -1,57 +1,57 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <matrix-room-state.hpp>
#include <memory>
#include <QtTest>
#include <factory.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event-list.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixRoomStateTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testSimple();
};
void MatrixRoomStateTest::testSimple()
{
auto state = EventList{
makeMemberEvent(),
makeMemberEvent(),
makeEvent(withEventType("m.room.name") | withStateKey("")),
makeEvent(withEventType("moe.kazv.mxc.foo") | withStateKey("a")),
};
auto r = makeRoom(withRoomState(state));
auto c = makeClient(withRoom(r));
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(SdkModel{c})};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(SdkModel{c});
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto allState = toUniquePtr(room->allState());
QCOMPARE(allState->count(), 3);
auto members = toUniquePtr(allState->eventsForType(u"m.room.member"_s));
QCOMPARE(members->count(), 2);
auto names = toUniquePtr(allState->eventsForType(u"m.room.name"_s));
QCOMPARE(names->count(), 1);
}
QTEST_MAIN(MatrixRoomStateTest)
#include "matrix-room-state-test.moc"
diff --git a/src/tests/matrix-room-timeline-benchmark-test.cpp b/src/tests/matrix-room-timeline-benchmark-test.cpp
index d039340..d47fce0 100644
--- a/src/tests/matrix-room-timeline-benchmark-test.cpp
+++ b/src/tests/matrix-room-timeline-benchmark-test.cpp
@@ -1,101 +1,101 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <QRandomGenerator>
#include <factory.hpp>
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
Q_DECLARE_METATYPE(EventList)
static int eventListMetaType{qRegisterMetaType<EventList>("EventList")};
class MatrixRoomTimelineBenchmarkTest : public QObject
{
Q_OBJECT
public:
enum IndexType {
Random,
CloseToLatest,
Nonexistent,
};
Q_ENUM(IndexType)
private Q_SLOTS:
void testIndexOfEvent_data();
void testIndexOfEvent();
};
void MatrixRoomTimelineBenchmarkTest::testIndexOfEvent_data()
{
QTest::addColumn<EventList>("events");
QTest::addColumn<IndexType>("type");
auto events = EventList{};
for (int i = 0; i < 10000; ++i) {
events = std::move(events).push_back(makeEvent(withEventId("$" + std::to_string(i)) | withEventKV("/origin_server_ts"_json_pointer, i * 1000)));
}
QTest::addRow("random event") << events << Random;
QTest::addRow("event close to the latest") << events << CloseToLatest;
QTest::addRow("nonexistent event") << events << Nonexistent;
}
void MatrixRoomTimelineBenchmarkTest::testIndexOfEvent()
{
QFETCH(EventList, events);
QFETCH(IndexType, type);
auto r = makeRoom(withRoomTimeline(events));
r.localEchoes = {
LocalEchoDesc{"txn1", makeEvent(withEventId("")), LocalEchoDesc::Sending},
LocalEchoDesc{"txn2", makeEvent(withEventId("")), LocalEchoDesc::Sending},
};
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
if (type == Random) {
QRandomGenerator g;
const int highest = events.size();
QBENCHMARK {
timeline->indexOfEvent(QString::fromStdString(
"$" + std::to_string(g.bounded(highest))));
}
} else if (type == CloseToLatest) {
QBENCHMARK {
timeline->indexOfEvent("$9940");
}
} else if (type == Nonexistent) {
QBENCHMARK {
timeline->indexOfEvent("$xxx");
}
}
}
QTEST_MAIN(MatrixRoomTimelineBenchmarkTest)
#include "matrix-room-timeline-benchmark-test.moc"
diff --git a/src/tests/matrix-room-timeline-test.cpp b/src/tests/matrix-room-timeline-test.cpp
index 069e1cb..b60c005 100644
--- a/src/tests/matrix-room-timeline-test.cpp
+++ b/src/tests/matrix-room-timeline-test.cpp
@@ -1,162 +1,162 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <factory.hpp>
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event.hpp>
#include <matrix-event-reader-list-model.hpp>
#include <matrix-event-reader.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixRoomTimelineTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testLocalEcho();
void testReadReceipts();
void testEdits();
void testIndexOfEvent();
};
void MatrixRoomTimelineTest::testLocalEcho()
{
auto model = makeTestModel();
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(u"!foo:tusooa.xyz"_s));
auto timeline = toUniquePtr(room->timeline());
QCOMPARE(timeline->count(), 2);
auto first = toUniquePtr(timeline->at(1)); // reverse order
QCOMPARE(first->isSending(), true);
QCOMPARE(first->isLocalEcho(), true);
QCOMPARE(first->isFailed(), false);
QCOMPARE(first->txnId(), u"some-txn-id"_s);
auto second = toUniquePtr(timeline->at(0));
QCOMPARE(second->isSending(), false);
QCOMPARE(second->isLocalEcho(), true);
QCOMPARE(second->isFailed(), true);
QCOMPARE(second->txnId(), u"some-other-txn-id"_s);
}
void MatrixRoomTimelineTest::testReadReceipts()
{
auto r = makeRoom(withRoomTimeline({
makeEvent(withEventId("$1")),
makeEvent(withEventId("$2")),
}));
r.eventReadUsers = {{"$1", {"@a:example.com", "@b:example.com"}}};
r.readReceipts = {{"@a:example.com", {"$1", 1234}}, {"@b:example.com", {"$2", 5678}}};
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
{
auto event = toUniquePtr(timeline->at(1));
QCOMPARE(event->eventId(), u"$1"_s);
auto readers = toUniquePtr(event->readers());
QCOMPARE(readers->count(), 2);
auto reader1 = toUniquePtr(readers->at(0));
QCOMPARE(reader1->userId(), u"@a:example.com"_s);
QCOMPARE(reader1->timestamp(), 1234);
auto reader2 = toUniquePtr(readers->at(1));
QCOMPARE(reader2->userId(), u"@b:example.com"_s);
QCOMPARE(reader2->timestamp(), 5678);
}
{
auto event = toUniquePtr(timeline->at(0));
QCOMPARE(event->eventId(), u"$2"_s);
auto readers = toUniquePtr(event->readers());
QCOMPARE(readers->count(), 0);
}
}
void MatrixRoomTimelineTest::testEdits()
{
auto r = makeRoom(withRoomTimeline({
makeEvent(withEventId("$0")),
makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0")),
makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1")),
// valid
makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1")),
// also valid
makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1")),
// invalid, changed event type
makeEvent(withEventId("$5") | withEventContent(json{{"m.new_content", {{"body", "fourth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
// invalid, changed event sender
makeEvent(withEventId("$6") | withEventContent(json{{"m.new_content", {{"body", "fifth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withEventSenderId("@othersender:example.com")),
// invalid, has state key
makeEvent(withEventId("$7") | withEventContent(json{{"m.new_content", {{"body", "sixth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withStateKey("")),
// invalid, no m.new_content
makeEvent(withEventId("$8") | withEventContent(json{{"body", "seventh"}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
}));
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
auto event = toUniquePtr(timeline->at(7));
QCOMPARE(event->eventId(), u"$1"_s);
// the content is taken from the newest valid edit
qDebug() << event->content();
QCOMPARE(event->content(), (QJsonObject{{u"body"_s, u"third"_s}}));
// the relation is taken from the original content
QCOMPARE(event->relationType(), u"moe.kazv.mxc.some-rel"_s);
QCOMPARE(event->relatedEventId(), u"$0"_s);
QVERIFY(event->isEdited());
}
void MatrixRoomTimelineTest::testIndexOfEvent()
{
// reverse index
// 4 3 2 1 0
// event/txnId
// $1 $2 $3 txn1 txn2
auto r = makeRoom(withRoomTimeline({
makeEvent(withEventId("$1") | withEventKV("/origin_server_ts"_json_pointer, 1000)),
makeEvent(withEventId("$2") | withEventKV("/origin_server_ts"_json_pointer, 2000)),
makeEvent(withEventId("$3") | withEventKV("/origin_server_ts"_json_pointer, 3000)),
}));
r.localEchoes = {
LocalEchoDesc{"txn1", makeEvent(withEventId("")), LocalEchoDesc::Sending},
LocalEchoDesc{"txn2", makeEvent(withEventId("")), LocalEchoDesc::Sending},
};
auto model = SdkModel{makeClient(withRoom(r))};
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
QCOMPARE(timeline->indexOfEvent(u"$1"_s), 4);
QCOMPARE(timeline->indexOfEvent(u"$2"_s), 3);
QCOMPARE(timeline->indexOfEvent(u"$3"_s), 2);
QCOMPARE(timeline->indexOfEvent(u"$4"_s), -1);
}
QTEST_MAIN(MatrixRoomTimelineTest)
#include "matrix-room-timeline-test.moc"
diff --git a/src/tests/matrix-sdk-session-loader.cpp b/src/tests/matrix-sdk-session-loader.cpp
index 6e8ae95..67c98c8 100644
--- a/src/tests/matrix-sdk-session-loader.cpp
+++ b/src/tests/matrix-sdk-session-loader.cpp
@@ -1,29 +1,29 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <iostream>
#include <cstdlib>
#include <QCoreApplication>
#include <QString>
#include "matrix-sdk-sessions-test.hpp"
int main(int argc, char *argv[])
{
std::string userDataDir = argv[1];
std::string sessionName = argv[2];
auto app = QCoreApplication(argc, argv);
auto sdk = MatrixSdkSessionsTest::makeMatrixSdkImpl(userDataDir);
QObject::connect(sdk.get(), &MatrixSdk::loadSessionFinished, [](auto, auto res) {
- if (res != MatrixSdk::SessionLoadSuccess) {
+ if (res != Constants::SessionLoadSuccess) {
std::cout << "cannot load session" << std::endl;
- std::exit(res == MatrixSdk::SessionLockFailed ? 1 : 2);
+ std::exit(res == Constants::SessionLockFailed ? 1 : 2);
}
std::cout << "loaded session" << std::endl;
});
sdk->loadSession(QString::fromStdString(sessionName));
return app.exec();
}
diff --git a/src/tests/matrix-sdk-sessions-test.cpp b/src/tests/matrix-sdk-sessions-test.cpp
index 1fe11bd..56f127e 100644
--- a/src/tests/matrix-sdk-sessions-test.cpp
+++ b/src/tests/matrix-sdk-sessions-test.cpp
@@ -1,210 +1,136 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <kazv-platform.hpp>
+#include <matrix-session-controller.hpp>
#include <memory>
#include <filesystem>
#include <fstream>
#include <QObject>
#include <QtTest>
#include <QProcess>
#include <QCoreApplication>
#include <crypto/base64.hpp>
#include "test-temp.hpp"
#include "matrix-sdk-sessions-test.hpp"
#include "db-store.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
namespace Fs = std::filesystem;
using StdPath = Fs::path;
void MatrixSdkSessionsTest::initTestCase()
{
- auto dir = StdPath(kazvTestTempDir()) / "sessions-test";
+ auto dir = StdPath(m_dir.path().toStdString()) / "sessions-test";
m_userDataDir = dir.string();
}
void MatrixSdkSessionsTest::clearDir()
{
QVERIFY(!m_userDataDir.empty());
std::error_code ec;
Fs::remove_all(StdPath(m_userDataDir), ec);
QVERIFY(!ec);
+ Fs::create_directories(StdPath(m_userDataDir), ec);
+ QVERIFY(!ec);
}
void MatrixSdkSessionsTest::init() { clearDir(); }
void MatrixSdkSessionsTest::cleanup() { clearDir(); }
void MatrixSdkSessionsTest::createSession(std::string userId, std::string deviceId)
{
auto sessionsDir = StdPath(m_userDataDir) / "sessions";
+ auto thread = std::make_unique<QThread>();
SdkModel model;
model.client.userId = userId;
model.client.deviceId = deviceId;
{
- auto sdk = makeMatrixSdk(model, /* testing = */ false);
- sdk->serializeToFile();
+ auto controller = std::make_unique<MatrixSessionController>(thread.get(), m_userDataDir);
+ controller->createWithModel(model);
+ controller->save();
}
// Not ideal, but this is the only way to wait for
// MatrixSdkPravite to be destroyed.
QTest::qWait(100);
}
void MatrixSdkSessionsTest::testListSessions()
{
createSession("@mew:example.com", "device1");
createSession("@mew:example.com", "device2");
createSession("@mew2:example.com", "device3");
auto sdk = makeMatrixSdk();
QSet<QString> expected{
QStringLiteral("@mew:example.com/device1"),
QStringLiteral("@mew:example.com/device2"),
QStringLiteral("@mew2:example.com/device3"),
};
auto sessionList = sdk->allSessions();
auto sessionSet = QSet<QString>(sessionList.begin(), sessionList.end());
QCOMPARE(sessionSet, expected);
}
void MatrixSdkSessionsTest::testListLegacySessions()
{
// This test cannot be run on Windows, because the file name
// there cannot contain ':'.
#if KAZV_IS_WINDOWS
QSKIP("Skipping because there are no legacy sessions on Windows");
#endif
createSession("@mew:example.com", "device1");
auto legacySession = StdPath(m_userDataDir) / "sessions" / "@mew3:example.com" / "device4";
{
std::ofstream s{legacySession / "store"};
s << "";
}
auto sdk = makeMatrixSdk();
QStringList expected{
QStringLiteral("@mew:example.com/device1"),
};
QCOMPARE(sdk->allSessions(), expected);
}
-void MatrixSdkSessionsTest::testLoadSession()
-{
- createSession("@mew:example.com", "device4");
-
- auto sdk = makeMatrixSdk();
- auto spy = QSignalSpy(sdk.get(), &MatrixSdk::loadSessionFinished);
- sdk->loadSession(u"@mew:example.com/device4"_s);
- spy.wait();
- auto res = spy.first().at(1);
- QCOMPARE(res, MatrixSdk::SessionLoadSuccess);
- QCOMPARE(sdk->userId(), QStringLiteral("@mew:example.com"));
- QCOMPARE(sdk->deviceId(), QStringLiteral("device4"));
-}
-
void MatrixSdkSessionsTest::testSessionLock()
{
#if KAZV_IS_WINDOWS
QSKIP("Skipping because session lock is not yet supported on Windows");
#else
createSession("@mew:example.com", "device5");
auto program = QCoreApplication::applicationDirPath() + QStringLiteral("/matrix-sdk-session-loader");
QProcess proc1;
QStringList args{
QString::fromStdString(m_userDataDir),
QStringLiteral("@mew:example.com/device5"),
};
proc1.start(program, args);
proc1.waitForReadyRead();
auto line = proc1.readAllStandardOutput();
QCOMPARE(line, QByteArray("loaded session\n"));
QProcess proc2;
proc2.start(program, args);
auto res = proc2.waitForFinished();
QVERIFY(res);
QCOMPARE(proc2.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc2.exitCode(), 1);
proc1.kill();
#endif
}
-void MatrixSdkSessionsTest::testSaveSessionFailure()
-{
-#if KAZV_IS_WINDOWS
- QSKIP("Skipping because session the condition cannot be easily satisfied on Windows");
-#endif
-
- auto userId = std::string("@mew:example.com");
- auto deviceId = std::string("device1");
- createSession(userId, deviceId);
-
- auto sessionDir = sessionDirForUserAndDeviceId(Fs::path(m_userDataDir), userId, deviceId);
- // there is store, metadata and lock under the sessionDir
- auto dbFullDir = sessionDir / DbStore::dbDirName;
- auto dbName = dbFullDir / DbStore::dbBaseName;
- std::error_code err;
- Fs::create_directories(dbFullDir, err);
- QVERIFY(!err);
- {
- std::ofstream s(dbName, std::ios_base::binary);
- s.flush();
- }
-
- // make the sessionDir readonly, so that new file cannot be created
- Fs::permissions(
- sessionDir,
- Fs::perms::owner_read | Fs::perms::owner_exec,
- Fs::perm_options::replace,
- err);
- QVERIFY(!err);
- err.clear();
-
- // a model that is slightly different from the original one
- SdkModel model;
- model.client.userId = userId;
- model.client.deviceId = deviceId;
- model.client.serverUrl = "https://example.com";
-
- {
- auto sdk = makeMatrixSdk(model, /* testing = */ false);
- // this should fail
- sdk->serializeToFile();
- }
- // Not ideal, but this is the only way to wait for
- // MatrixSdkPravite to be destroyed.
- QTest::qWait(100);
-
- auto sdk = makeMatrixSdk();
- auto spy = QSignalSpy(sdk.get(), &MatrixSdk::loadSessionFinished);
- sdk->loadSession(QString::fromStdString(userId + "/" + deviceId));
- spy.wait();
- auto res = spy.first().at(1);
- QCOMPARE(res, MatrixSdk::SessionLoadSuccess);
- // verify that the original data is kept
- QCOMPARE(sdk->serverUrl(), u""_s);
-
- // before cleaning up, add write permissions back so the files can be removed
- Fs::permissions(
- sessionDir,
- Fs::perms::owner_read | Fs::perms::owner_write | Fs::perms::owner_exec,
- Fs::perm_options::replace,
- err);
- QVERIFY(!err);
- err.clear();
-}
-
#ifndef MATRIX_SDK_SESSIONS_TEST_NO_MAIN
QTEST_MAIN(MatrixSdkSessionsTest)
#endif
diff --git a/src/tests/matrix-sdk-sessions-test.hpp b/src/tests/matrix-sdk-sessions-test.hpp
index c282c6e..52fc8b6 100644
--- a/src/tests/matrix-sdk-sessions-test.hpp
+++ b/src/tests/matrix-sdk-sessions-test.hpp
@@ -1,48 +1,48 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <memory>
#include <QObject>
+#include <QTemporaryDir>
#include <matrix-sdk.hpp>
class MatrixSdkSessionsTest : public QObject
{
Q_OBJECT
private:
+ QTemporaryDir m_dir;
std::string m_userDataDir;
void clearDir();
void createSession(std::string userId, std::string deviceId);
public:
template<class ...Args>
static std::unique_ptr<MatrixSdk> makeMatrixSdkImpl(std::string userDataDir, Args &&...args)
{
std::unique_ptr<MatrixSdk> sdk(new MatrixSdk(std::forward<Args>(args)...));
sdk->setUserDataDir(userDataDir);
return sdk;
}
template<class ...Args>
std::unique_ptr<MatrixSdk> makeMatrixSdk(Args &&...args)
{
return makeMatrixSdkImpl(m_userDataDir, std::forward<Args>(args)...);
}
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testListSessions();
void testListLegacySessions();
- void testLoadSession();
void testSessionLock();
- void testSaveSessionFailure();
};
diff --git a/src/tests/matrix-session-controller-test.cpp b/src/tests/matrix-session-controller-test.cpp
new file mode 100644
index 0000000..403c1e2
--- /dev/null
+++ b/src/tests/matrix-session-controller-test.cpp
@@ -0,0 +1,158 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "test-utils.hpp"
+#include <matrix-session-controller.hpp>
+#include <matrix-session.hpp>
+#include <constants.hpp>
+#include <db-store.hpp>
+#include <QTemporaryDir>
+#include <QSignalSpy>
+#include <QtTest>
+#include <fstream>
+
+using namespace Qt::Literals::StringLiterals;
+using namespace Kazv;
+namespace Fs = std::filesystem;
+using StdPath = Fs::path;
+
+struct ControllerSetup
+{
+ QPointer<QThread> thread;
+ QTemporaryDir dir;
+ QPointer<MatrixSessionController> controller;
+
+ ControllerSetup(QObject *q)
+ : thread(new QThread(q))
+ , dir()
+ {
+ controller = new MatrixSessionController(thread, dir.path().toStdString(), q);
+ thread->start();
+ }
+
+ ~ControllerSetup()
+ {
+ controller->prepareDestroy();
+ thread->quit();
+ thread->wait();
+ controller->deleteLater();
+ thread->deleteLater();
+ }
+};
+
+class MatrixSessionControllerTest : public QObject
+{
+ Q_OBJECT
+
+ static void createSession(std::string userDataDir, std::string userId, std::string deviceId);
+
+private Q_SLOTS:
+ void testLoadSession();
+ void testSaveSessionFailure();
+};
+
+void MatrixSessionControllerTest::createSession(std::string userDataDir, std::string userId, std::string deviceId)
+{
+ SdkModel model;
+ model.client.userId = userId;
+ model.client.deviceId = deviceId;
+ QThread thread;
+ {
+ MatrixSessionController controller(&thread, userDataDir);
+ controller.createWithModel(model);
+ controller.save();
+ }
+}
+
+void MatrixSessionControllerTest::testLoadSession()
+{
+ auto s = ControllerSetup(this);
+ createSession(s.dir.path().toStdString(), "@user1:example.org", "device1");
+ s.controller->load(u"@user1:example.org/device1"_s);
+ auto spy = QSignalSpy(s.controller.get(), &MatrixSessionController::loadSessionFinished);
+ spy.wait();
+ auto res = spy.first().at(1);
+ QCOMPARE(res, Constants::SessionLoadSuccess);
+ auto session = toUniquePtr(s.controller->session());
+ QCOMPARE(session->userId(), u"@user1:example.org"_s);
+ QCOMPARE(session->deviceId(), u"device1"_s);
+}
+
+void MatrixSessionControllerTest::testSaveSessionFailure()
+{
+#if KAZV_IS_WINDOWS
+ QSKIP("Skipping because session the condition cannot be easily satisfied on Windows");
+#endif
+
+ auto s = ControllerSetup(this);
+
+ auto userId = std::string("@mew:example.com");
+ auto deviceId = std::string("device1");
+ auto userDataDir = s.dir.path().toStdString();
+ createSession(userDataDir, userId, deviceId);
+
+ auto sessionDir = sessionDirForUserAndDeviceId(Fs::path(userDataDir), userId, deviceId);
+ // there is store, metadata and lock under the sessionDir
+ auto dbFullDir = sessionDir / DbStore::dbDirName;
+ auto dbName = dbFullDir / DbStore::dbBaseName;
+ std::error_code err;
+ Fs::create_directories(dbFullDir, err);
+ QVERIFY(!err);
+ {
+ std::ofstream stream(dbName, std::ios_base::binary);
+ stream.flush();
+ }
+
+ // make the sessionDir readonly, so that new file cannot be created
+ Fs::permissions(
+ sessionDir,
+ Fs::perms::owner_read | Fs::perms::owner_exec,
+ Fs::perm_options::replace,
+ err);
+ QVERIFY(!err);
+ err.clear();
+
+ // a model that is slightly different from the original one
+ SdkModel model;
+ model.client.userId = userId;
+ model.client.deviceId = deviceId;
+ model.client.serverUrl = "https://example.com";
+
+ {
+ auto controller = MatrixSessionController(s.thread, userDataDir);
+ controller.createWithModel(model);
+ // this should fail
+ controller.save();
+ }
+ // Not ideal, but this is the only way to wait for
+ // MatrixSdkPravite to be destroyed.
+ QTest::qWait(100);
+
+ auto spy = QSignalSpy(s.controller.get(), &MatrixSessionController::loadSessionFinished);
+ s.controller->load(QString::fromStdString(userId + "/" + deviceId));
+ spy.wait();
+ auto res = spy.first().at(1);
+ QCOMPARE(res, Constants::SessionLoadSuccess);
+ // verify that the original data is kept
+ {
+ auto session = toUniquePtr(s.controller->session());
+ QCOMPARE(session->serverUrl(), u""_s);
+ }
+
+ // before cleaning up, add write permissions back so the files can be removed
+ Fs::permissions(
+ sessionDir,
+ Fs::perms::owner_read | Fs::perms::owner_write | Fs::perms::owner_exec,
+ Fs::perm_options::replace,
+ err);
+ QVERIFY(!err);
+ err.clear();
+}
+
+QTEST_MAIN(MatrixSessionControllerTest)
+
+#include "matrix-session-controller-test.moc"
diff --git a/src/tests/matrix-sdk-test.cpp b/src/tests/matrix-session-test.cpp
similarity index 53%
rename from src/tests/matrix-sdk-test.cpp
rename to src/tests/matrix-session-test.cpp
index c38221e..9e4e77b 100644
--- a/src/tests/matrix-sdk-test.cpp
+++ b/src/tests/matrix-session-test.cpp
@@ -1,134 +1,134 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
-
-#include <memory>
-
-#include <QtTest>
-#include <QSignalSpy>
-
-#include <matrix-sdk.hpp>
+#include "test-model.hpp"
+#include "test-utils.hpp"
#include <matrix-device-list.hpp>
#include <matrix-device.hpp>
#include <matrix-promise.hpp>
-
-#include "test-model.hpp"
-#include "test-utils.hpp"
+#include <lager/event_loop/qt.hpp>
+#include <QtTest>
+#include <QSignalSpy>
+#include <memory>
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
-class MatrixSdkTest : public QObject
+class MatrixSessionTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testDevicesOfUser();
void testSetDeviceTrustLevel();
void testValidateHomeserverUrl();
void testSpecVersion();
void testDirectRoomIds();
};
-void MatrixSdkTest::testDevicesOfUser()
+void MatrixSessionTest::testDevicesOfUser()
{
auto model = makeTestModel();
- std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
+ SessionSetup s(model);
- auto devices = toUniquePtr(sdk->devicesOfUser(QStringLiteral("@test1:test1.org")));
+ auto devices = toUniquePtr(s.session.devicesOfUser(QStringLiteral("@test1:test1.org")));
QVERIFY(devices->count() == 2);
auto dev = toUniquePtr(devices->at(0));
QVERIFY(dev->deviceId() == u"device1"_s);
QVERIFY(dev->trustLevel() == u"unseen"_s);
}
-void MatrixSdkTest::testSetDeviceTrustLevel()
+void MatrixSessionTest::testSetDeviceTrustLevel()
{
auto model = makeTestModel();
- std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
-
- sdk->startThread();
+ SessionSetup s(model);
auto promise = toUniquePtr(
- sdk->setDeviceTrustLevel(
+ s.session.setDeviceTrustLevel(
QStringLiteral("@test1:test1.org"),
QStringLiteral("device1"),
QStringLiteral("verified")
)
);
auto spy = QSignalSpy(promise.get(), &MatrixPromise::succeeded);
spy.wait();
- auto devices = toUniquePtr(sdk->devicesOfUser(QStringLiteral("@test1:test1.org")));
+ auto devices = toUniquePtr(s.session.devicesOfUser(QStringLiteral("@test1:test1.org")));
QVERIFY(devices->count() == 2);
auto dev = toUniquePtr(devices->at(0));
QVERIFY(dev->deviceId() == u"device1"_s);
QTRY_VERIFY(dev->trustLevel() == u"verified"_s); // wait for the change to propagate to this thread
}
-void MatrixSdkTest::testValidateHomeserverUrl()
+void MatrixSessionTest::testValidateHomeserverUrl()
{
auto v = [](const QString &u) {
- return QString::fromStdString(MatrixSdk::validateHomeserverUrl(u));
+ return QString::fromStdString(MatrixSession::validateHomeserverUrl(u));
};
QCOMPARE(v(u"example.com"_s), u"https://example.com"_s);
QCOMPARE(v(u"to"_s), u"https://to"_s);
QVERIFY(v(u""_s).isEmpty());
QVERIFY(v(u"/usr/libexec"_s).isEmpty());
QCOMPARE(v(u"ea.pl"_s), u"https://ea.pl"_s);
}
-void MatrixSdkTest::testSpecVersion()
+void MatrixSessionTest::testSpecVersion()
{
auto model = makeTestModel();
model.client.versions = immer::array{"v1.11"s, "v1.12"s};
- std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
- QVERIFY(sdk->checkSpecVersion(u"r0.5.0"_s));
- QVERIFY(sdk->checkSpecVersion(u"v1.10"_s));
- QVERIFY(sdk->checkSpecVersion(u"v1.11"_s));
- QVERIFY(!sdk->checkSpecVersion(u"v1.13"_s));
- QVERIFY(!sdk->checkSpecVersion(u"mew"_s));
- QVERIFY(sdk->checkSpecVersionRange(u"r0.5.0"_s, u"v1.12"_s));
- QVERIFY(sdk->checkSpecVersionRange(u"v1.10"_s, u"v1.12"_s));
- QVERIFY(sdk->checkSpecVersionRange(u"v1.11"_s, u"v1.13"_s));
- QVERIFY(!sdk->checkSpecVersionRange(u"r0.4.0"_s, u"v1.9"_s));
- QVERIFY(!sdk->checkSpecVersionRange(u"v1.13"_s, u"v1.14"_s));
+
+ {
+ SessionSetup s(model);
+
+ QVERIFY(s.session.checkSpecVersion(u"r0.5.0"_s));
+ QVERIFY(s.session.checkSpecVersion(u"v1.10"_s));
+ QVERIFY(s.session.checkSpecVersion(u"v1.11"_s));
+ QVERIFY(!s.session.checkSpecVersion(u"v1.13"_s));
+ QVERIFY(!s.session.checkSpecVersion(u"mew"_s));
+ QVERIFY(s.session.checkSpecVersionRange(u"r0.5.0"_s, u"v1.12"_s));
+ QVERIFY(s.session.checkSpecVersionRange(u"v1.10"_s, u"v1.12"_s));
+ QVERIFY(s.session.checkSpecVersionRange(u"v1.11"_s, u"v1.13"_s));
+ QVERIFY(!s.session.checkSpecVersionRange(u"r0.4.0"_s, u"v1.9"_s));
+ QVERIFY(!s.session.checkSpecVersionRange(u"v1.13"_s, u"v1.14"_s));
+ }
model.client.versions = immer::array{"r0.4.0"s};
- sdk.reset(new MatrixSdk(model, /* testing = */ true));
- QVERIFY(sdk->checkSpecVersion(u"r0.4.0"_s));
- QVERIFY(!sdk->checkSpecVersion(u"v1.1"_s));
- QVERIFY(sdk->checkSpecVersionRange(u"r0.3.5"_s, u"r0.4.0"_s));
- QVERIFY(!sdk->checkSpecVersionRange(u"r0.3.0"_s, u"r0.3.5"_s));
- QVERIFY(!sdk->checkSpecVersionRange(u"r0.4.1"_s, u"r0.4.5"_s));
- QVERIFY(!sdk->checkSpecVersionRange(u"v1.1"_s, u"v1.2"_s));
+ {
+ SessionSetup s(model);
+ QVERIFY(s.session.checkSpecVersion(u"r0.4.0"_s));
+ QVERIFY(!s.session.checkSpecVersion(u"v1.1"_s));
+ QVERIFY(s.session.checkSpecVersionRange(u"r0.3.5"_s, u"r0.4.0"_s));
+ QVERIFY(!s.session.checkSpecVersionRange(u"r0.3.0"_s, u"r0.3.5"_s));
+ QVERIFY(!s.session.checkSpecVersionRange(u"r0.4.1"_s, u"r0.4.5"_s));
+ QVERIFY(!s.session.checkSpecVersionRange(u"v1.1"_s, u"v1.2"_s));
+ }
}
-void MatrixSdkTest::testDirectRoomIds()
+void MatrixSessionTest::testDirectRoomIds()
{
auto model = makeTestModel();
auto j = R"({
"type": "m.direct",
"content": {
"@mew:example.org": [
"!somewhere1:example.org",
"!somewhere2:example.org"
]
}
})"_json;
model.client.accountData = model.client.accountData.set("m.direct", j);
- std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
+ SessionSetup s(model);
const auto rooms = QStringList{u"!somewhere1:example.org"_s, u"!somewhere2:example.org"_s};
- const auto directRoomIds = sdk->directRoomIds(u"@mew:example.org"_s);
+ const auto directRoomIds = s.session.directRoomIds(u"@mew:example.org"_s);
QCOMPARE(QSet(directRoomIds.begin(), directRoomIds.end()),
QSet(rooms.begin(), rooms.end()));
}
-QTEST_MAIN(MatrixSdkTest)
+QTEST_MAIN(MatrixSessionTest)
-#include "matrix-sdk-test.moc"
+#include "matrix-session-test.moc"
diff --git a/src/tests/matrix-sticker-pack-test.cpp b/src/tests/matrix-sticker-pack-test.cpp
index 71f0ebc..00e39f9 100644
--- a/src/tests/matrix-sticker-pack-test.cpp
+++ b/src/tests/matrix-sticker-pack-test.cpp
@@ -1,343 +1,343 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <QSet>
#include <QJsonObject>
#include <lager/state.hpp>
#include <base/event.hpp>
#include <testfixtures/factory.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack.hpp"
#include "matrix-sticker.hpp"
#include "matrix-event.hpp"
#include "matrix-sdk.hpp"
#include "matrix-room-list.hpp"
#include "matrix-room.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixStickerPackTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testStickerPack();
void testStickerPackList();
void testStickerPackListInRoom();
void testAddToPack();
};
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
static auto stickerPackEvent = Event{R"({
"content": {
"images": {
"myemote": {
"url": "mxc://example.org/blah"
},
"mysticker": {
"body": "my sticker",
"url": "mxc://example.org/sticker",
"usage": ["sticker"],
"info": {
"mimetype": "image/png"
}
}
},
"pack": {
"display_name": "Awesome Pack",
"usage": ["emoticon"]
}
},
"type": "im.ponies.user_emotes"
})"_json};
static const auto imagePackRoomsEvent = Event(R"({
"content": {
"rooms": {
"!someroom:example.org": {
"": {},
"de.sorunome.mx-puppet-bridge.discord": {}
},
"!someotherroom:example.org": {
"": {}
}
}
},
"type": "im.ponies.emote_rooms"
})"_json);
static Event getRoomStickersEvent(std::string stateKey, std::string name)
{
auto j = R"({
"content": {
"images": {
"myemote": {
"url": "mxc://example.org/blah"
},
"mysticker": {
"body": "my sticker",
"url": "mxc://example.org/sticker",
"usage": ["sticker"],
"info": {
"mimetype": "image/png"
}
}
},
"pack": {
"usage": ["emoticon"]
}
},
"type": "im.ponies.room_emotes"
})"_json;
j["state_key"] = stateKey;
j["content"]["pack"]["display_name"] = name;
return Event(j);
}
template<class Range>
static bool hasPack(const Range &packs, const QString &name)
{
return std::any_of(packs.begin(), packs.end(), [&name](const auto &pack) {
return pack->packName() == name;
});
}
void MatrixStickerPackTest::testStickerPack()
{
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
"",
""
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
QCOMPARE(stickerPack->rowCount(QModelIndex()), 2);
QCOMPARE(stickerPack->count(), 2);
QVERIFY(stickerPack->hasShortCode(QStringLiteral("myemote")));
QVERIFY(!stickerPack->hasShortCode(QStringLiteral("lolol")));
auto sticker0 = toUniquePtr(stickerPack->at(0));
QCOMPARE(sticker0->shortCode(), QStringLiteral("myemote"));
QCOMPARE(sticker0->body(), QStringLiteral("myemote"));
QCOMPARE(sticker0->mxcUri(), QStringLiteral("mxc://example.org/blah"));
QCOMPARE(sticker0->info(), QJsonObject());
QCOMPARE(sticker0->makeEventJson(), (QJsonObject{
{u"type"_s, u"m.sticker"_s},
{u"content"_s, QJsonObject{
{u"body"_s, u"myemote"_s},
{u"url"_s, u"mxc://example.org/blah"_s},
{u"info"_s, QJsonObject()},
}},
}));
auto sticker1 = toUniquePtr(stickerPack->at(1));
QCOMPARE(sticker1->shortCode(), QStringLiteral("mysticker"));
QCOMPARE(sticker1->body(), QStringLiteral("my sticker"));
QCOMPARE(sticker1->mxcUri(), QStringLiteral("mxc://example.org/sticker"));
QCOMPARE(sticker1->info(), (QJsonObject{{u"mimetype"_s, u"image/png"_s}}));
QCOMPARE(sticker1->makeEventJson(), (QJsonObject{
{u"type"_s, u"m.sticker"_s},
{u"content"_s, QJsonObject{
{u"body"_s, u"my sticker"_s},
{u"url"_s, u"mxc://example.org/sticker"_s},
{u"info"_s, QJsonObject{{u"mimetype"_s, u"image/png"_s}}},
}},
}));
}
void MatrixStickerPackTest::testStickerPackList()
{
auto model = makeClient(
withAccountData({stickerPackEvent, imagePackRoomsEvent})
| withRoom(makeRoom(
withRoomId("!someroom:example.org")
| withRoomState({
getRoomStickersEvent("", "Pack 1"),
getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
})
))
| withRoom(makeRoom(
withRoomId("!someotherroom:example.org")
| withRoomState({getRoomStickersEvent("", "Pack 3")})
))
);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(SdkModel{model})};
- auto stickerPackList = toUniquePtr(sdk->stickerPackList());
+ SessionSetup s(SdkModel{model});
+ auto stickerPackList = toUniquePtr(s.session.stickerPackList());
QCOMPARE(stickerPackList->rowCount(QModelIndex()), 4);
QCOMPARE(stickerPackList->count(), 4);
auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 4>{
toUniquePtr(stickerPackList->at(0)),
toUniquePtr(stickerPackList->at(1)),
toUniquePtr(stickerPackList->at(2)),
toUniquePtr(stickerPackList->at(3)),
};
QVERIFY(hasPack(packs, u"Awesome Pack"_s));
QVERIFY(hasPack(packs, u"Pack 1"_s));
QVERIFY(hasPack(packs, u"Pack 2"_s));
QVERIFY(hasPack(packs, u"Pack 3"_s));
auto expectedPacks = QSet<QJsonValue>{
QJsonObject{
{u"source"_s, MatrixStickerPackSource::AccountData},
{u"isAccountData"_s, true},
{u"isState"_s, false},
{u"eventType"_s, u"im.ponies.user_emotes"_s},
{u"roomId"_s, u""_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Awesome Pack"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 1"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u"de.sorunome.mx-puppet-bridge.discord"_s},
{u"packName"_s, u"Pack 2"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someotherroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 3"_s},
},
};
auto packsArr = stickerPackList->packs().toArray();
QCOMPARE(QSet<QJsonValue>(packsArr.begin(), packsArr.end()), expectedPacks);
auto p1 = toUniquePtr(stickerPackList->packFor(QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
}));
QCOMPARE(p1->packName(), u"Pack 1"_s);
auto p2 = toUniquePtr(stickerPackList->packFor(QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someotherroom:example.org"_s},
{u"stateKey"_s, u"somestatekey"_s},
}));
QVERIFY(p2->isAccountData());
}
void MatrixStickerPackTest::testStickerPackListInRoom()
{
auto model = makeClient(
withRoom(makeRoom(
withRoomId("!someroom:example.org")
| withRoomState({
getRoomStickersEvent("", "Pack 1"),
getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
})
))
);
- std::unique_ptr<MatrixSdk> sdk{makeTestSdk(SdkModel{model})};
- auto roomList = toUniquePtr(sdk->roomList());
+ SessionSetup s(SdkModel{model});
+ auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(u"!someroom:example.org"_s));
auto stickerPackList = toUniquePtr(room->stickerPackList());
QCOMPARE(stickerPackList->count(), 2);
auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 2>{
toUniquePtr(stickerPackList->at(0)),
toUniquePtr(stickerPackList->at(1)),
};
QVERIFY(hasPack(packs, u"Pack 1"_s));
QVERIFY(hasPack(packs, u"Pack 2"_s));
auto expectedPacks = QSet<QJsonValue>{
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 1"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u"de.sorunome.mx-puppet-bridge.discord"_s},
{u"packName"_s, u"Pack 2"_s},
},
};
auto packsArr = stickerPackList->packs().toArray();
QCOMPARE(QSet<QJsonValue>(packsArr.begin(), packsArr.end()), expectedPacks);
}
void MatrixStickerPackTest::testAddToPack()
{
using namespace nlohmann::literals;
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
"",
"",
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
auto contentJson = json{
{"url", "mxc://example.org/someotheremote"},
{"info", {{"mimetype", "image/png"}}},
{"body", "some other emote"},
};
auto eventCursor = lager::make_constant(makeEvent(
withEventContent(contentJson)
));
auto event = toUniquePtr(new MatrixEvent(eventCursor));
auto newSource = stickerPack->addSticker(u"someotheremote"_s, event.get()).template value<MatrixStickerPackSource>();
auto expected = sourceCursor.get().event.content().get();
expected["images"]["someotheremote"] = contentJson;
QVERIFY(newSource.event.content().get() == expected);
}
QTEST_MAIN(MatrixStickerPackTest)
#include "matrix-sticker-pack-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
index 4e76163..f7a5089 100644
--- a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
+++ b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
@@ -1,90 +1,22 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
QtObject {
- property var userId: ''
- property var serverUrl: 'https://example.com'
- property var token: 'token'
- property var updateStickerPack: mockHelper.promise()
- property var createRoom: mockHelper.promise([
- 'isPrivate',
- 'name',
- 'alias',
- 'invites',
- 'isDirect',
- 'allowFederate',
- 'topic',
- 'powerLevelContentOverride',
- 'preset',
- 'encrypted',
- ])
- property var joinRoom: mockHelper.promise()
- property var sendAccountData: mockHelper.promise()
- property var login: mockHelper.noop()
- property var ssoLoginStart: mockHelper.func((serverUrl) => serverUrl + '/_matrix/client/v3/login/sso/redirect?redirectUrl=http://127.0.0.1:7456/sso-redirect/random')
- property var discoverAndGetLoginFlows: mockHelper.noop()
- property var addDirectRoom: mockHelper.promise([
- 'userId',
- 'roomId'
- ])
- property var getRoomIdByAlias: mockHelper.promise([
- 'roomAlias'
- ])
- property var importFromKeyBackupFile: mockHelper.promise([
- 'fileUrl',
- 'password',
- ])
- property var requestVerifyDevice: mockHelper.promise([
- 'userId',
- 'deviceId',
- ])
-
+ id: matrixSdkMock
property var sessions: []
- property var specVersions: ["v1.11"]
-
- signal discoverFailed(string errorCode, string errorMsg)
- signal discoverSuccessful(string serverUrl)
- signal getLoginFlowsFailed(string errorCode, string errorMsg)
- signal getLoginFlowsSuccessful(var flows)
- signal loginFailed(string errorCode, string errorMsg)
-
function allSessions() {
return sessions;
}
- function mxcUriToHttp (uri) {
- console.log('mxcUriToHttp');
- return uri || '';
- }
-
- function mxcUriToHttpAuthenticatedV1 (uri) {
- console.log('mxcUriToHttpAuthenticatedV1');
- return uri || '';
- }
+ signal sessionChanged()
- function devicesOfUser (userId) {
- return [];
- }
-
- function checkSpecVersion (ver) {
- return !!specVersions.includes(ver);
- }
-
- function directRoomIds(userId) {
- const directRoomMap = {
- "@someonejoined:example.org": ["!joinedroomid:example.org"],
- "@someoneinvited:example.org": ["!invitedroomid:example.org"],
- "@someoneleft:example.org": ["!leftroomid:example.org"]
- };
- if (!directRoomMap[userId]) {
- return [];
- }
- return directRoomMap[userId];
- }
+ property var startNewSession: mockHelper.func(() => {
+ matrixSdkMock.sessionChanged();
+ })
}
diff --git a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml b/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
similarity index 93%
copy from src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
copy to src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
index 4e76163..d908ffd 100644
--- a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
+++ b/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
@@ -1,90 +1,84 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2023-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import QtQuick 2.15
+import QtQuick
QtObject {
property var userId: ''
property var serverUrl: 'https://example.com'
property var token: 'token'
property var updateStickerPack: mockHelper.promise()
property var createRoom: mockHelper.promise([
'isPrivate',
'name',
'alias',
'invites',
'isDirect',
'allowFederate',
'topic',
'powerLevelContentOverride',
'preset',
'encrypted',
])
property var joinRoom: mockHelper.promise()
property var sendAccountData: mockHelper.promise()
property var login: mockHelper.noop()
property var ssoLoginStart: mockHelper.func((serverUrl) => serverUrl + '/_matrix/client/v3/login/sso/redirect?redirectUrl=http://127.0.0.1:7456/sso-redirect/random')
property var discoverAndGetLoginFlows: mockHelper.noop()
property var addDirectRoom: mockHelper.promise([
'userId',
'roomId'
])
property var getRoomIdByAlias: mockHelper.promise([
'roomAlias'
])
property var importFromKeyBackupFile: mockHelper.promise([
'fileUrl',
'password',
])
property var requestVerifyDevice: mockHelper.promise([
'userId',
'deviceId',
])
- property var sessions: []
-
property var specVersions: ["v1.11"]
signal discoverFailed(string errorCode, string errorMsg)
signal discoverSuccessful(string serverUrl)
signal getLoginFlowsFailed(string errorCode, string errorMsg)
signal getLoginFlowsSuccessful(var flows)
signal loginFailed(string errorCode, string errorMsg)
- function allSessions() {
- return sessions;
- }
-
function mxcUriToHttp (uri) {
console.log('mxcUriToHttp');
return uri || '';
}
function mxcUriToHttpAuthenticatedV1 (uri) {
console.log('mxcUriToHttpAuthenticatedV1');
return uri || '';
}
function devicesOfUser (userId) {
return [];
}
function checkSpecVersion (ver) {
return !!specVersions.includes(ver);
}
function directRoomIds(userId) {
const directRoomMap = {
"@someonejoined:example.org": ["!joinedroomid:example.org"],
"@someoneinvited:example.org": ["!invitedroomid:example.org"],
"@someoneleft:example.org": ["!leftroomid:example.org"]
};
if (!directRoomMap[userId]) {
return [];
}
return directRoomMap[userId];
}
}
diff --git a/src/tests/quick-tests/test-helpers/TestItem.qml b/src/tests/quick-tests/test-helpers/TestItem.qml
index 562bd61..85a02b1 100644
--- a/src/tests/quick-tests/test-helpers/TestItem.qml
+++ b/src/tests/quick-tests/test-helpers/TestItem.qml
@@ -1,50 +1,55 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtTest
import '.' as QmlHelpers
import '../test-helpers.js' as JsHelpers
Item {
id: item
width: 800
height: 600
property var l10n: JsHelpers.fluentMock
property var mockHelper: QmlHelpers.MockHelper {}
property var promiseComp: Component {
QmlHelpers.MatrixPromiseMock {}
}
+ property var matrixSession: QmlHelpers.MatrixSessionMock {
+ }
property var matrixSdk: QmlHelpers.MatrixSdkMock {
+ function session() {
+ return item.matrixSession;
+ }
}
property var sdkVars: QtObject {
property var roomList: ({})
property var userGivenNicknameMap: QtObject {
property var map: ({
'@foo:tusooa.xyz': 'something',
})
property var setAndUpload: mockHelper.promise()
}
}
property var kazvIOManager: QmlHelpers.KazvIOManagerMock {}
property var showPassiveNotification: mockHelper.noop()
property var pageStack: QtObject {
property var removePage: mockHelper.noop()
}
function applicationWindow() {
return Window.window;
}
Component.onCompleted: {
const testFunctions = Object.keys(item).filter(name => name.startsWith('test_'));
if (testFunctions.length) {
console.error('There are functions starting with test_ in the root item. It is likely that they belong to the TestCase instead. The functions are:', testFunctions);
throw new Error('Unexpected test functions');
}
}
}
diff --git a/src/tests/quick-tests/tst_AddStickerPopup.qml b/src/tests/quick-tests/tst_AddStickerPopup.qml
index ac4ca56..4fceab1 100644
--- a/src/tests/quick-tests/tst_AddStickerPopup.qml
+++ b/src/tests/quick-tests/tst_AddStickerPopup.qml
@@ -1,138 +1,138 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import org.kde.kirigami 2.20 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers.js' as Helpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
Kazv.AddStickerPopup {
id: addStickerPopup
event: ({
content: {
url: 'mxc://example.org/something',
},
})
stickerPackList: ({
at(index) {
return {
addSticker(shortCode, event) {
return {
_packIndex: index,
event: {
content: {
images: {
[shortCode]: event.content,
},
},
},
};
},
hasShortCode(shortCode) {
return shortCode === 'foo';
},
};
},
packs: [
{ packName: 'pack0' },
{ packName: 'pack1' },
],
})
property var close: mockHelper.noop()
}
TestCase {
id: eventViewTest
name: 'EventViewTest'
when: windowShown
function init() {
addStickerPopup.contentItem.parent = item;
findChild(addStickerPopup, 'packChooser').currentIndex = 0;
addStickerPopup.addingSticker = false;
}
function cleanup() {
item.mockHelper.clearAll();
}
function test_addSticker() {
findChild(addStickerPopup, 'shortCodeInput').text = 'bar';
verify(!findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
const button = findChild(addStickerPopup, 'addStickerButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => matrixSdk.updateStickerPack.calledTimes() === 1);
- verify(Helpers.deepEqual(matrixSdk.updateStickerPack.lastArgs()[0].event.content.images, {
+ tryVerify(() => matrixSession.updateStickerPack.calledTimes() === 1);
+ verify(Helpers.deepEqual(matrixSession.updateStickerPack.lastArgs()[0].event.content.images, {
'bar': {
url: 'mxc://example.org/something',
}
}));
tryVerify(() => !button.enabled);
tryVerify(() => findChild(addStickerPopup, 'shortCodeInput').readOnly);
verify(addStickerPopup.close.calledTimes() === 0);
- matrixSdk.updateStickerPack.lastRetVal().resolve(true, {});
+ matrixSession.updateStickerPack.lastRetVal().resolve(true, {});
tryVerify(() => addStickerPopup.close.calledTimes() === 1);
}
function test_addStickerOtherPack() {
verify(Helpers.deepEqual(addStickerPopup.availablePackNames, ['pack0', 'pack1']));
findChild(addStickerPopup, 'packChooser').currentIndex = 1;
findChild(addStickerPopup, 'shortCodeInput').text = 'bar';
verify(!findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
const button = findChild(addStickerPopup, 'addStickerButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => matrixSdk.updateStickerPack.calledTimes() === 1);
- compare(matrixSdk.updateStickerPack.lastArgs()[0]._packIndex, 1);
+ tryVerify(() => matrixSession.updateStickerPack.calledTimes() === 1);
+ compare(matrixSession.updateStickerPack.lastArgs()[0]._packIndex, 1);
}
function test_addStickerFailed() {
findChild(addStickerPopup, 'shortCodeInput').text = 'bar';
verify(!findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
const button = findChild(addStickerPopup, 'addStickerButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => matrixSdk.updateStickerPack.calledTimes() === 1);
- verify(Helpers.deepEqual(matrixSdk.updateStickerPack.lastArgs()[0].event.content.images, {
+ tryVerify(() => matrixSession.updateStickerPack.calledTimes() === 1);
+ verify(Helpers.deepEqual(matrixSession.updateStickerPack.lastArgs()[0].event.content.images, {
'bar': {
url: 'mxc://example.org/something',
}
}));
- matrixSdk.updateStickerPack.lastRetVal().resolve(false, {});
+ matrixSession.updateStickerPack.lastRetVal().resolve(false, {});
tryVerify(() => button.enabled);
verify(addStickerPopup.close.calledTimes() === 0);
}
function test_addStickerOverride() {
findChild(addStickerPopup, 'shortCodeInput').text = 'foo';
tryVerify(() => findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
const button = findChild(addStickerPopup, 'addStickerButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => matrixSdk.updateStickerPack.calledTimes() === 1);
- verify(Helpers.deepEqual(matrixSdk.updateStickerPack.lastArgs()[0].event.content.images, {
+ tryVerify(() => matrixSession.updateStickerPack.calledTimes() === 1);
+ verify(Helpers.deepEqual(matrixSession.updateStickerPack.lastArgs()[0].event.content.images, {
'foo': {
url: 'mxc://example.org/something',
}
}));
tryVerify(() => !button.enabled);
tryVerify(() => findChild(addStickerPopup, 'shortCodeInput').readOnly);
verify(addStickerPopup.close.calledTimes() === 0);
- matrixSdk.updateStickerPack.lastRetVal().resolve(true, {});
+ matrixSession.updateStickerPack.lastRetVal().resolve(true, {});
tryVerify(() => addStickerPopup.close.calledTimes() === 1);
}
}
}
diff --git a/src/tests/quick-tests/tst_CreateRoomPage.qml b/src/tests/quick-tests/tst_CreateRoomPage.qml
index dcd7307..dd3e04f 100644
--- a/src/tests/quick-tests/tst_CreateRoomPage.qml
+++ b/src/tests/quick-tests/tst_CreateRoomPage.qml
@@ -1,268 +1,268 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
import moe.kazv.mxc.kazv 0.0 as MK
QmlHelpers.TestItem {
id: item
Kazv.CreateRoomPage {
anchors.fill: parent
id: createRoomPage
}
TestCase {
id: createRoomPageTest
name: 'CreateRoomPageTest'
when: windowShown
function initTestCase() {
if (MK.KazvUtil.kfQtMajorVersion === 6) {
createRoomPage.contentItem.clip = false;
}
waitForRendering(createRoomPage);
}
function cleanup() {
mockHelper.clearAll();
createRoomPage.inviteUserIds = [];
createRoomPage.creatingRoom = false;
createRoomPage.encryptedChangedOnce = false;
}
function test_createRoomPublic() {
mouseClick(findChild(createRoomPage, 'typePublicRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: false,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.PublicChat,
+ preset: MK.Constants.PublicChat,
encrypted: false,
}));
}
function test_createRoomPrivate() {
mouseClick(findChild(createRoomPage, 'typePrivateRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.PrivateChat,
+ preset: MK.Constants.PrivateChat,
encrypted: true,
}));
}
function test_createRoomDirect() {
mouseClick(findChild(createRoomPage, 'typeDirectRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.TrustedPrivateChat,
+ preset: MK.Constants.TrustedPrivateChat,
encrypted: true,
}));
- tryVerify(() => item.matrixSdk.addDirectRoom.calledTimes() === 0, 1000);
+ tryVerify(() => item.matrixSession.addDirectRoom.calledTimes() === 0, 1000);
}
function test_createRoomDirectInviteOne() {
mouseClick(findChild(createRoomPage, 'typeDirectRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const addInviteInput = findChild(createRoomPage, 'newInviteUserInput');
addInviteInput.text = '@foo:example.org';
const addInviteButton = findChild(createRoomPage, 'addInviteUserButton');
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: ['@foo:example.org'],
isDirect: true,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.TrustedPrivateChat,
+ preset: MK.Constants.TrustedPrivateChat,
encrypted: true,
}));
- item.matrixSdk.createRoom.lastRetVal().resolve(true, { 'roomId': '!somewhere:example.org' });
- tryVerify(() => item.matrixSdk.addDirectRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.addDirectRoom.lastArgs(), {
+ item.matrixSession.createRoom.lastRetVal().resolve(true, { 'roomId': '!somewhere:example.org' });
+ tryVerify(() => item.matrixSession.addDirectRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.addDirectRoom.lastArgs(), {
userId: '@foo:example.org',
roomId: '!somewhere:example.org'
}));
}
function test_createRoomDirectInviteMultiple() {
mouseClick(findChild(createRoomPage, 'typeDirectRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const addInviteInput = findChild(createRoomPage, 'newInviteUserInput');
const addInviteButton = findChild(createRoomPage, 'addInviteUserButton');
addInviteInput.text = '@foo:example.org';
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
addInviteInput.text = '@bar:example.org';
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: ['@foo:example.org', '@bar:example.org'],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.TrustedPrivateChat,
+ preset: MK.Constants.TrustedPrivateChat,
encrypted: true,
}));
}
function test_invite() {
// FIXME: does not pass CI
skip();
mouseClick(findChild(createRoomPage, 'typePublicRadio'));
findChild(createRoomPage, 'roomNameInput').text = 'some name';
findChild(createRoomPage, 'roomAliasInput').text = 'alias';
findChild(createRoomPage, 'roomTopicInput').text = 'some topic';
const button = findChild(createRoomPage, 'createRoomButton');
verify(button.enabled);
mouseClick(button);
// add three invites
const addInviteInput = findChild(createRoomPage, 'newInviteUserInput');
const addInviteButton = findChild(createRoomPage, 'addInviteUserButton');
addInviteInput.text = '@foo:example.org';
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
addInviteInput.text = '@bar:example.org';
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
addInviteInput.text = '@baz:example.org';
mouseClick(addInviteButton);
tryVerify(() => addInviteInput.text === '', 1000);
// remove the second invite (bar)
tryVerify(() => findChild(createRoomPage, 'inviteUserItem_1'), 1000);
const inviteItem = findChild(createRoomPage, 'inviteUserItem_1');
compare(findChild(inviteItem, 'inviteUserIdLabel').text, '@bar:example.org');
findChild(inviteItem, 'removeInviteAction').trigger();
tryVerify(() => {
const label = findChild(
findChild(createRoomPage, 'inviteUserItem_1'),
'inviteUserIdLabel'
);
return label && label.text === '@baz:example.org'
}, 50000);
- tryVerify(() => item.matrixSdk.createRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.createRoom.lastArgs(), {
+ tryVerify(() => item.matrixSession.createRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.createRoom.lastArgs(), {
isPrivate: false,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: ['@foo:example.org', '@baz:example.org'],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
- preset: MK.MatrixSdk.PublicChat,
+ preset: MK.Constants.PublicChat,
encrypted: false,
}));
}
function test_encryptedCheckBox() {
const publicRadio = findChild(createRoomPage, 'typePublicRadio');
const privateRadio = findChild(createRoomPage, 'typePrivateRadio');
const directRadio = findChild(createRoomPage, 'typeDirectRadio')
const encryptedInput = findChild(createRoomPage, 'encryptedInput');
mouseClick(publicRadio);
verify(!encryptedInput.checked);
mouseClick(privateRadio);
verify(encryptedInput.checked);
mouseClick(directRadio);
verify(encryptedInput.checked);
mouseClick(encryptedInput);
verify(!encryptedInput.checked);
mouseClick(publicRadio);
verify(!encryptedInput.checked);
mouseClick(directRadio);
verify(!encryptedInput.checked);
}
}
}
diff --git a/src/tests/quick-tests/tst_Device.qml b/src/tests/quick-tests/tst_Device.qml
index 76dfb3b..aed3575 100644
--- a/src/tests/quick-tests/tst_Device.qml
+++ b/src/tests/quick-tests/tst_Device.qml
@@ -1,69 +1,69 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.3
import QtTest 1.0
import '../../contents/ui/device-mgmt' as KazvDM
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
property var deviceItem: ({
deviceId: 'some-id',
displayName: 'name',
trustLevel: 'unseen'
})
property var pushVerificationsPage: mockHelper.noop();
KazvDM.Device {
id: device
}
TestCase {
id: deviceTest
name: 'DeviceTest'
when: windowShown
function initTestCase() {
device.item = deviceItem;
}
function init() {
item.mockHelper.clearAll();
}
function test_device() {
verify(findChild(device, 'deviceIdLabel').text === 'some-id');
verify(findChild(device, 'trustLevelLabel').text === l10n.get('device-trust-level-unseen'));
}
function test_verifyRequest() {
const action = findChild(device, 'requestVerifyAction');
verify(action.enabled);
action.trigger();
verify(!action.enabled);
verify(item.pushVerificationsPage.calledTimes() == 0);
verify(device.sendingVerification);
- item.matrixSdk.requestVerifyDevice.lastRetVal().resolve(true, {});
+ item.matrixSession.requestVerifyDevice.lastRetVal().resolve(true, {});
verify(item.pushVerificationsPage.calledTimes() == 1);
}
function test_verifyRequestFailed() {
const action = findChild(device, 'requestVerifyAction');
verify(action.enabled);
action.trigger();
verify(!action.enabled);
verify(item.pushVerificationsPage.calledTimes() == 0);
verify(device.sendingVerification);
- item.matrixSdk.requestVerifyDevice.lastRetVal().resolve(false, {});
+ item.matrixSession.requestVerifyDevice.lastRetVal().resolve(false, {});
verify(item.pushVerificationsPage.calledTimes() == 0);
verify(action.enabled);
verify(item.showPassiveNotification.calledTimes() == 1);
}
}
}
diff --git a/src/tests/quick-tests/tst_EventReactions.qml b/src/tests/quick-tests/tst_EventReactions.qml
index f687951..3c9ad59 100644
--- a/src/tests/quick-tests/tst_EventReactions.qml
+++ b/src/tests/quick-tests/tst_EventReactions.qml
@@ -1,115 +1,115 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtTest
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
function getReaction(i) {
return {
count: 2,
key: `r${i}`,
at(index) {
return index === 0 ? {
sender: `@foo${i}:example.com`,
eventId: `$xxx${i}`,
} : {
sender: `@bar${i}:example.com`,
eventId: `$yyy${i}`,
};
},
};
}
readonly property var reactions: ListModel {
ListElement {}
ListElement {}
function at(index) {
return item.getReaction(index);
}
}
readonly property var deleteEventRequestedNoConfirm: mockHelper.noop()
readonly property var eventView: QtObject {
readonly property var reactWith: item.mockHelper.noop()
}
ColumnLayout {
Kazv.EventReactions {
id: eventReactions
reactions: item.reactions
}
Kazv.EventView {
id: eventViewReaction
event: ({
type: 'm.reaction',
content: {},
isFailed: false,
})
}
Kazv.EventView {
id: eventViewReactionFailed
event: ({
type: 'm.reaction',
content: {},
isFailed: true,
})
}
}
TestCase {
id: eventViewVideoTest
name: 'EventViewVideoTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
- item.matrixSdk.userId = '@foo0:example.com';
+ item.matrixSession.userId = '@foo0:example.com';
}
function test_reactionEvent() {
compare(eventViewReaction.messageType, 'ignore');
compare(eventViewReactionFailed.messageType, 'reaction');
}
function test_reactionsReacted() {
const reaction = findChild(eventReactions, 'reaction0');
const text = findChild(reaction, 'reactionContentText');
verify(text.text.includes('r0'));
const content = findChild(reaction, 'reactionContent');
verify(content.checked);
compare(reaction.selfReactEventId, '$xxx0');
mouseClick(content);
compare(item.eventView.reactWith.calledTimes(), 0);
compare(item.deleteEventRequestedNoConfirm.calledTimes(), 1);
compare(item.deleteEventRequestedNoConfirm.lastArgs()[0], '$xxx0');
}
function test_reactionsUnreacted() {
const reaction = findChild(eventReactions, 'reaction1');
const text = findChild(reaction, 'reactionContentText');
verify(text.text.includes('r1'));
const content = findChild(reaction, 'reactionContent');
verify(!content.checked);
verify(!reaction.selfReactEventId);
mouseClick(content);
compare(item.deleteEventRequestedNoConfirm.calledTimes(), 0);
compare(item.eventView.reactWith.calledTimes(), 1);
compare(item.eventView.reactWith.lastArgs()[0], 'r1');
}
}
}
diff --git a/src/tests/quick-tests/tst_EventView.qml b/src/tests/quick-tests/tst_EventView.qml
index 7902ebc..ea92a73 100644
--- a/src/tests/quick-tests/tst_EventView.qml
+++ b/src/tests/quick-tests/tst_EventView.qml
@@ -1,702 +1,702 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.3
import QtQuick.Layouts 1.15
import QtTest 1.0
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers.js' as Helpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
property var activateUserPage: mockHelper.noop()
property var paginateBackRequested: mockHelper.noop()
property var viewEventHistoryRequested: mockHelper.noop()
property var pinEventRequested: mockHelper.noop()
property var unpinEventRequested: mockHelper.noop()
property var deleteEventRequested: mockHelper.noop()
property var timeline: ({
gaps: {},
})
property var pinnedEvents: ({
count: 2,
eventIds: {
'$123': true,
'$456': true,
},
})
property var room: ({
resendMessage: mockHelper.promise(),
removeLocalEcho: mockHelper.promise(),
ensureStateEvent: mockHelper.promise(),
messageById: (_id) => item.textEvent,
member: (_id) => ({}),
})
property var localEcho: ({
eventId: '',
sender: '',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
encrypted: false,
isState: false,
unsignedData: {},
isLocalEcho: true,
isSending: true,
isFailed: false,
txnId: 'some-txn-id',
})
property var failedLocalEcho: Object.assign({}, item.localEcho, { isSending: false, isFailed: true, })
property var textEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var htmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: '**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
formattedTime: '4:06 P.M.',
})
property var replyHtmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
replyingToEventId: '$xxx',
content: {
msgtype: 'm.text',
body: '> some quote\n**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<mx-reply><blockquote>some quote</blockquote></mx-reply><strong>some body</strong>',
},
formattedTime: '4:06 P.M.',
})
property var replyNoQuoteHtmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
replyingToEventId: '$xxx',
content: {
msgtype: 'm.text',
body: '**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
formattedTime: '4:06 P.M.',
})
property var redactedEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {},
redacted: true,
formattedTime: '4:06 P.M.',
})
property var imageEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
info: {
h: Kirigami.Units.gridUnit * 10,
w: Kirigami.Units.gridUnit * 10,
}
},
formattedTime: '4:06 P.M.',
})
property var stickerEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.sticker',
stateKey: '',
content: {
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var eventBySomeoneElse: ({
eventId: '',
sender: '@bar:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var eventTextBySomeoneElse: ({
eventId: '',
sender: '@bar:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var ignoredEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
relationType: 'm.replace',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var unknownTypeEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'moe.kazv.mxc.unknown.event',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
})
property var unknownMsgtypeEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'moe.kazv.mxc.unknown.msgtype',
body: 'some body',
},
})
property var cannotDecryptEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'moe.kazv.mxc.cannot.decrypt',
body: 'some body',
},
})
property var editedEvent: ({
eventId: '!some-event-id',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
formattedTime: '4:06 P.M.',
isEdited: true,
})
property var pinnedEvent: ({
eventId: '$123',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
content: {
msgtype: 'm.text',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var notPinnedEvent: ({
eventId: '$789',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
content: {
msgtype: 'm.text',
body: 'some body',
},
formattedTime: '4:06 P.M.',
})
property var sender: ({
membership: 'join',
userId: '@foo:tusooa.xyz',
name: 'foo',
avatarMxcUri: '',
})
property var senderOther: ({
membership: 'join',
userId: '@bar:tusooa.xyz',
name: 'bar',
avatarMxcUri: '',
})
property var eventViewComp: Component {
Kazv.EventView {
Layout.fillWidth: true
event: QtObject {
property var eventId: '$some-event-id'
function readers() { return 0; }
}
sender: item.sender
isGapped: true
}
}
property var eventComp: Component {
QtObject {
property var eventId: '$some-event-id'
function readers() { return 0; }
}
}
ColumnLayout {
anchors.fill: parent
Kazv.EventView {
Layout.fillWidth: true
id: eventView
event: item.localEcho
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewFailed
event: item.failedLocalEcho
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewText
event: item.textEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtml
event: item.htmlEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtmlReply
event: item.replyHtmlEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtmlReplyNoQuote
event: item.replyNoQuoteHtmlEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewRedacted
event: item.redactedEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewUnknownType
event: item.unknownTypeEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewUnknownMsgtype
event: item.unknownMsgtypeEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewCannotDecrypt
event: item.cannotDecryptEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewCollapseSameSender
event: item.imageEvent
sender: item.sender
prevEvent: item.redactedEvent
}
Kazv.EventView {
id: eventViewCollapseLocalEcho
event: item.localEcho
sender: item.sender
prevEvent: item.redactedEvent
}
Kazv.EventView {
id: eventViewDontCollapseDifferentSender
event: item.imageEvent
sender: item.sender
prevEvent: item.eventBySomeoneElse
}
Kazv.EventView {
id: eventViewDontCollapseLocalEchoDifferentSender
event: item.localEcho
sender: item.sender
prevEvent: item.eventBySomeoneElse
}
Kazv.EventView {
id: eventViewDontCollapseIgnoredEvent
event: item.imageEvent
sender: item.sender
prevEvent: item.ignoredEvent
}
Kazv.EventView {
id: eventViewTextBySomeoneElse
event: item.eventTextBySomeoneElse
sender: item.senderOther
}
Kazv.EventView {
id: eventViewImage
event: item.imageEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewImageCompact
compactMode: true
event: item.imageEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewSticker
event: item.stickerEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewStickerCompact
compactMode: true
event: item.stickerEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewEdited
event: item.editedEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewPinned
event: item.pinnedEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewNotPinned
event: item.notPinnedEvent
sender: item.sender
}
}
TestCase {
id: eventViewTest
name: 'EventViewTest'
when: windowShown
function init() {
- item.matrixSdk.userId = '@foo:tusooa.xyz';
+ item.matrixSession.userId = '@foo:tusooa.xyz';
eventView.event = item.localEcho;
mockHelper.clearAll();
}
function test_localEcho() {
const indicator = findChild(eventView, 'localEchoIndicator');
verify(indicator);
verify(indicator.icon.name === 'state-sync');
verify(indicator.Accessible.name === l10n.get('event-sending'));
const resendButton = findChild(eventView, 'localEchoIndicator');
verify(resendButton);
verify(!resendButton.enabled);
verify(!findChild(eventView, 'timeIndicator').visible);
}
function test_localEchoFailedRemove() {
const indicator = findChild(eventViewFailed, 'localEchoIndicator');
verify(indicator);
verify(indicator.visible);
verify(indicator.icon.name === 'state-warning');
verify(indicator.Accessible.name === l10n.get('event-resend'));
const menuComp = findChild(eventViewFailed, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const deleteAction = findChild(menu, 'deleteMenuItem');
verify(deleteAction.enabled);
deleteAction.trigger();
compare(deleteEventRequested.calledTimes(), 1);
}
function test_sentMessage() {
const indicator = findChild(eventViewText, 'localEchoIndicator');
verify(indicator);
verify(!indicator.visible);
verify(findChild(eventViewText, 'timeIndicator').visible);
}
function test_htmlMessage() {
const text = findChild(eventViewHtml, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(text.text.includes('some body'));
}
function test_htmlMessageReply() {
const text = findChild(eventViewHtmlReply, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(!text.text.includes('<blockquote>'));
}
function test_htmlMessageReplyNoQuote() {
const text = findChild(eventViewHtmlReplyNoQuote, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(!text.text.includes('<blockquote>'));
verify(text.text.includes('some body'));
}
function test_menuText() {
const menuComp = findChild(eventViewText, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => findChild(menu, 'replyToMenuItem').enabled);
tryVerify(() => findChild(menu, 'editMenuItem').enabled);
tryVerify(() => findChild(menu, 'deleteMenuItem').enabled);
}
function test_menuRedacted() {
const menuComp = findChild(eventViewRedacted, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => !findChild(menu, 'replyToMenuItem').enabled);
tryVerify(() => !findChild(menu, 'deleteMenuItem').enabled);
}
function test_fallback() {
verify(findChild(eventViewUnknownType, 'fallbackIcon'));
verify(findChild(eventViewUnknownType, 'textEventContent').text ===
l10n.get('event-fallback-text', { type: unknownTypeEvent.type }));
verify(findChild(eventViewUnknownMsgtype, 'fallbackIcon'));
verify(findChild(eventViewUnknownMsgtype, 'textEventContent').text ===
l10n.get('event-msgtype-fallback-text', { msgtype: unknownMsgtypeEvent.content.msgtype }));
verify(findChild(eventViewCannotDecrypt, 'fallbackIcon'));
verify(findChild(eventViewCannotDecrypt, 'textEventContent').text ===
l10n.get('event-cannot-decrypt-text'));
}
function test_collapseSender() {
verify(!findChild(eventViewCollapseSameSender, 'senderAvatar').visible);
verify(findChild(eventViewCollapseSameSender, 'senderCollapsedPlaceholder').visible);
verify(!findChild(eventViewCollapseLocalEcho, 'senderAvatar').visible);
verify(findChild(eventViewCollapseLocalEcho, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseDifferentSender, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseDifferentSender, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseLocalEchoDifferentSender, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseLocalEchoDifferentSender, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseIgnoredEvent, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseIgnoredEvent, 'senderCollapsedPlaceholder').visible);
}
function test_eventByOther() {
const menuComp = findChild(eventViewTextBySomeoneElse, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => !findChild(menu, 'editMenuItem').enabled);
}
function test_menuSelfDestruct() {
const menuComp = findChild(eventViewTextBySomeoneElse, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
menu.popup(item);
menu.dismiss();
const menu2 = menuComp.createObject(menuComp.parent);
menu2.popup(item);
menu2.dismiss();
}
function test_userLink() {
const text = findChild(eventViewText, 'textEventContent');
text.linkActivated('https://matrix.to/#/@mew:example.com');
tryVerify(() => item.room.ensureStateEvent.calledTimes());
verify(Helpers.deepEqual(
item.room.ensureStateEvent.lastArgs(),
['m.room.member', '@mew:example.com']));
item.room.ensureStateEvent.lastRetVal().resolve(true, {});
tryVerify(() => item.activateUserPage.calledTimes());
}
function test_senderNameOverrided() {
const name = findChild(eventViewText, 'userNicknameText');
compare(name.text, l10n.get(
'user-name-overrided',
{ overridedName: 'something', globalName: 'foo' }));
}
function test_senderNameNotOverrided() {
const name = findChild(eventViewTextBySomeoneElse, 'userNicknameText');
compare(name.text, 'bar');
}
function test_paginateBack() {
const obj = item.eventViewComp.createObject(item);
tryVerify(() => item.paginateBackRequested.calledTimes());
verify(item.paginateBackRequested.lastArgs()[0] === '$some-event-id');
item.paginateBackRequested.clear();
obj.event.eventId = '$some-other-event-id';
tryVerify(() => item.paginateBackRequested.calledTimes() === 1);
verify(item.paginateBackRequested.lastArgs()[0] === '$some-other-event-id');
item.paginateBackRequested.clear();
const newEvent = eventComp.createObject(item);
obj.event = newEvent;
tryVerify(() => item.paginateBackRequested.calledTimes() === 1);
verify(item.paginateBackRequested.lastArgs()[0] === '$some-event-id');
item.paginateBackRequested.clear();
obj.isGapped = false;
verify(item.paginateBackRequested.calledTimes() === 0);
}
function test_avatarClick() {
const avatar = findChild(eventViewText, 'senderAvatar');
mouseClick(avatar);
tryVerify(() => item.activateUserPage.calledTimes() === 1);
// verify that the user id is passed
verify(item.activateUserPage.lastArgs()[2] === '@foo:tusooa.xyz');
}
function test_imageEvent() {
verify(!findChild(eventViewImage, 'summaryLabel').visible);
verify(findChild(eventViewImage, 'bodyLabel').visible);
verify(findChild(eventViewImageCompact, 'summaryLabel').visible);
verify(!findChild(eventViewImageCompact, 'bodyLabel').visible);
verify(!findChild(eventViewSticker, 'summaryLabel').visible);
verify(findChild(eventViewSticker, 'bodyLabel').visible);
verify(findChild(eventViewStickerCompact, 'summaryLabel').visible);
verify(!findChild(eventViewStickerCompact, 'bodyLabel').visible);
const image = findChild(eventViewImageCompact, 'mainImage');
verify(image.height <= Kirigami.Units.gridUnit * 5);
}
function test_history() {
{
const menuComp = findChild(eventViewText, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'viewHistoryMenuItem');
verify(!action.enabled);
}
{
const menuComp = findChild(eventViewEdited, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'viewHistoryMenuItem');
verify(action.enabled);
action.trigger();
compare(viewEventHistoryRequested.calledTimes(), 1);
compare(viewEventHistoryRequested.lastArgs()[0], '!some-event-id');
}
}
function test_pinned() {
{
const menuComp = findChild(eventViewPinned, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(action.enabled);
action.trigger();
compare(unpinEventRequested.calledTimes(), 1);
compare(unpinEventRequested.lastArgs()[0], '$123');
compare(pinEventRequested.calledTimes(), 0);
}
{
const menuComp = findChild(eventViewNotPinned, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(action.enabled);
action.trigger();
compare(pinEventRequested.calledTimes(), 1);
compare(pinEventRequested.lastArgs()[0], '$789');
compare(pinEventRequested.calledTimes(), 1);
}
{
const menuComp = findChild(eventView, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(!action.enabled);
}
}
}
}
diff --git a/src/tests/quick-tests/tst_FileHandler.qml b/src/tests/quick-tests/tst_FileHandler.qml
index 1e2aeef..33b28ab 100644
--- a/src/tests/quick-tests/tst_FileHandler.qml
+++ b/src/tests/quick-tests/tst_FileHandler.qml
@@ -1,275 +1,275 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtTest
import '../../contents/ui' as Kazv
import 'test-helpers' as QmlHelpers
import 'test-helpers.js' as JsHelpers
import moe.kazv.mxc.kazv as MK
QmlHelpers.TestItem {
id: item
property var unencryptedEventContent: ({
body: "fileName",
msgtype: "m.image",
url: "mxc://someunencrypted",
})
property var unencryptedThumbnailEventContent: ({
body: "fileName",
msgtype: "m.image",
url: "mxc://someunencrypted",
info: {
thumbnail_url: "mxc://someunencryptedthumbnail"
}
})
property var encryptedEventContent: ({
body: "fileName",
msgtype: "m.image",
file: {
url: "mxc://someencrypted",
key: {
kty: "oct",
key_ops: ["encrypt","decrypt"],
alg: "A256CTR",
k: "somekey",
ext: true,
},
iv: "someiv",
hashes: {
"sha256": "somesha256hash"
},
v: "v2",
}
})
property var encryptedThumbnailEventContent: ({
body: "fileName",
msgtype: "m.image",
file: {
url: "mxc://someencrypted",
key: {
kty: "oct",
key_ops: ["encrypt","decrypt"],
alg: "A256CTR",
k: "somekey",
ext: true,
},
iv: "someiv",
hashes: {
"sha256": "somesha256hash"
},
v: "v2",
},
info: {
thumbnail_file: {
url: "mxc://someencryptedthumbnail",
key: {
kty: "oct",
key_ops: ["encrypt","decrypt"],
alg: "A256CTR",
k: "somekeyofthumbnail",
ext: true,
},
iv: "someivofthumbnail",
hashes: {
"sha256": "somesha256hashofthumbnail"
},
v: "v2",
}
}
})
property var kazvIOManager: QmlHelpers.KazvIOManagerMock {}
property var managerForCache: QmlHelpers.KazvIOManagerMock {
getCacheJob: (mediaId) => {
return getDownloadJob(mediaId);
}
cacheFile: () => {
return "file:///somefileurl";
}
}
- property var matrixSdk: QmlHelpers.MatrixSdkMock {}
- property var matrixSdkUnauthenticatedMediaV3: QmlHelpers.MatrixSdkMock {
+ property var matrixSession: QmlHelpers.MatrixSessionMock {}
+ property var matrixSessionUnauthenticatedMediaV3: QmlHelpers.MatrixSessionMock {
specVersions: []
property var mxcUriToHttp: mockHelper.noop()
}
- property var matrixSdkAuthenticatedMediaV1: QmlHelpers.MatrixSdkMock {
+ property var matrixSessionAuthenticatedMediaV1: QmlHelpers.MatrixSessionMock {
property var mxcUriToHttpAuthenticatedV1: mockHelper.noop()
}
property var fileHandler: Kazv.FileHandler {
eventContent: ({})
autoCache: false
kazvIOManager: item.kazvIOManager
- matrixSdk: item.matrixSdk
+ matrixSession: item.matrixSession
}
TestCase {
id: fileHandlerTest
name: 'fileHandlerTest'
when: windowShown
function init() {
fileHandler.autoCache = false;
fileHandler.eventContent = ({});
- fileHandler.matrixSdk = item.matrixSdk;
+ fileHandler.matrixSession = item.matrixSession;
fileHandler.kazvIOManager = kazvIOManager;
mockHelper.clearAll();
}
// https://spec.matrix.org/v1.16/client-server-api/#get_matrixmediav3downloadservernamemediaid
function test_unauthenticatedMediaV3() {
- fileHandler.matrixSdk = matrixSdkUnauthenticatedMediaV3;
+ fileHandler.matrixSession = matrixSessionUnauthenticatedMediaV3;
fileHandler.autoCache = true;
fileHandler.eventContent = unencryptedEventContent;
- compare(fileHandler.httpVer, MK.MatrixSdk.UnauthenticatedMediaV3);
- compare(matrixSdkUnauthenticatedMediaV3.mxcUriToHttp.calledTimes(), 1);
- compare(matrixSdkUnauthenticatedMediaV3.mxcUriToHttp.lastArgs()[0], "mxc://someunencrypted");
+ compare(fileHandler.httpVer, MK.Constants.UnauthenticatedMediaV3);
+ compare(matrixSessionUnauthenticatedMediaV3.mxcUriToHttp.calledTimes(), 1);
+ compare(matrixSessionUnauthenticatedMediaV3.mxcUriToHttp.lastArgs()[0], "mxc://someunencrypted");
compare(kazvIOManager.cacheFile.calledTimes(), 1);
compare(kazvIOManager.cacheFile.lastArgs()[3], "");
fileHandler.downloadFile("fileUrl");
compare(kazvIOManager.startNewDownloadJob.calledTimes(), 1);
compare(kazvIOManager.startNewDownloadJob.lastArgs()[4], "");
}
// https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid
function test_authenticatedMediaV1() {
- fileHandler.matrixSdk = matrixSdkAuthenticatedMediaV1;
+ fileHandler.matrixSession = matrixSessionAuthenticatedMediaV1;
fileHandler.autoCache = true;
fileHandler.eventContent = unencryptedEventContent;
- compare(fileHandler.httpVer, MK.MatrixSdk.AuthenticatedMediaV1);
- compare(fileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.calledTimes(), 1);
- compare(fileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.lastArgs()[0], "mxc://someunencrypted");
+ compare(fileHandler.httpVer, MK.Constants.AuthenticatedMediaV1);
+ compare(fileHandler.matrixSession.mxcUriToHttpAuthenticatedV1.calledTimes(), 1);
+ compare(fileHandler.matrixSession.mxcUriToHttpAuthenticatedV1.lastArgs()[0], "mxc://someunencrypted");
compare(kazvIOManager.cacheFile.calledTimes(), 1);
- compare(kazvIOManager.cacheFile.lastArgs()[3], fileHandler.matrixSdk.token);
+ compare(kazvIOManager.cacheFile.lastArgs()[3], fileHandler.matrixSession.token);
fileHandler.downloadFile("fileUrl");
compare(kazvIOManager.startNewDownloadJob.calledTimes(), 1);
- compare(kazvIOManager.startNewDownloadJob.lastArgs()[4], fileHandler.matrixSdk.token);
+ compare(kazvIOManager.startNewDownloadJob.lastArgs()[4], fileHandler.matrixSession.token);
}
function test_autoCacheEncrypted() {
fileHandler.autoCache = true;
fileHandler.eventContent = encryptedEventContent;
const cacheFunc = kazvIOManager.cacheFile;
compare(cacheFunc.calledTimes(), 1);
compare(cacheFunc.lastArgs()[0],
- matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
+ matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
compare(cacheFunc.lastArgs()[2], "somesha256hash");
compare(cacheFunc.lastArgs()[4], "somekey");
compare(cacheFunc.lastArgs()[5], "someiv");
}
function test_autoCacheUnencrypted() {
fileHandler.autoCache = true;
fileHandler.eventContent = unencryptedEventContent;
const cacheFunc = kazvIOManager.cacheFile;
compare(cacheFunc.calledTimes(), 1);
compare(cacheFunc.lastArgs()[0],
- matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
+ matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
}
// Prefer thumbnail when caching
function test_autoCacheThumbnail() {
fileHandler.autoCache = true;
fileHandler.eventContent = unencryptedThumbnailEventContent;
const cacheFunc = kazvIOManager.cacheFile;
compare(cacheFunc.calledTimes(), 1);
compare(cacheFunc.lastArgs()[0],
- matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencryptedthumbnail"));
+ matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someunencryptedthumbnail"));
}
function test_autoCacheEncryptedThumbnail() {
fileHandler.autoCache = true;
fileHandler.eventContent = encryptedThumbnailEventContent;
const cacheFunc = kazvIOManager.cacheFile;
compare(cacheFunc.calledTimes(), 1);
compare(cacheFunc.lastArgs()[0],
- matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencryptedthumbnail"));
+ matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someencryptedthumbnail"));
compare(cacheFunc.lastArgs()[2], "somesha256hashofthumbnail");
compare(cacheFunc.lastArgs()[4], "somekeyofthumbnail");
compare(cacheFunc.lastArgs()[5], "someivofthumbnail");
}
function test_downloadEncrypted() {
fileHandler.eventContent = encryptedEventContent;
fileHandler.downloadFile("fileUrl");
const downloadFunc = kazvIOManager.startNewDownloadJob;
compare(downloadFunc.calledTimes(), 1);
- compare(downloadFunc.lastArgs()[0], matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
+ compare(downloadFunc.lastArgs()[0], matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
compare(downloadFunc.lastArgs()[1], "fileUrl");
compare(downloadFunc.lastArgs()[3], "somesha256hash");
compare(downloadFunc.lastArgs()[5], "somekey");
compare(downloadFunc.lastArgs()[6], "someiv");
}
function test_downloadUnencrypted() {
fileHandler.eventContent = unencryptedEventContent;
const downloadFunc = kazvIOManager.startNewDownloadJob;
fileHandler.downloadFile("fileUrl");
compare(downloadFunc.calledTimes(), 1);
const args = downloadFunc.lastArgs();
- compare(downloadFunc.lastArgs()[0], matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
+ compare(downloadFunc.lastArgs()[0], matrixSession.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
compare(downloadFunc.lastArgs()[1], "fileUrl");
compare(downloadFunc.lastArgs()[3], "");
compare(downloadFunc.lastArgs()[5], "");
compare(downloadFunc.lastArgs()[6], "");
}
function test_eventContentChanged() {
fileHandler.eventContent = encryptedEventContent;
const encryptedFile = fileHandler.getFile();
compare(encryptedFile.mxcUri, "mxc://someencrypted");
compare(encryptedFile.sha256, "somesha256hash");
compare(encryptedFile.key, "somekey");
compare(encryptedFile.iv, "someiv");
compare(fileHandler.getThumbnail(), undefined);
fileHandler.eventContent = unencryptedEventContent;
const unencryptedFile = fileHandler.getFile();
compare(unencryptedFile.mxcUri, "mxc://someunencrypted");
compare(unencryptedFile.sha256, "");
compare(unencryptedFile.key, "");
compare(unencryptedFile.iv, "");
compare(fileHandler.getThumbnail(), undefined);
}
function test_invalidEventContent() {
fileHandler.autoCache = true;
fileHandler.eventContent = ({});
compare(kazvIOManager.cacheFile.calledTimes(), 0);
fileHandler.downloadFile("fileUrl");
compare(kazvIOManager.startNewDownloadJob.calledTimes(), 0);
}
function test_autoTrackDownloadJob() {
fileHandler.eventContent = unencryptedEventContent;
verify(fileHandler.kazvIOJob);
compare(fileHandler.kazvIOJob.jobId,
fileHandler.getMediaId(unencryptedEventContent.url));
}
function test_autoTrackCacheJob() {
fileHandler.autoCache = true;
fileHandler.kazvIOManager = managerForCache;
fileHandler.eventContent = unencryptedThumbnailEventContent;
verify(fileHandler.kazvIOJob);
compare(fileHandler.kazvIOJob.jobId,
fileHandler.getMediaId(unencryptedThumbnailEventContent.info.thumbnail_url));
}
}
}
diff --git a/src/tests/quick-tests/tst_JoinRoomPage.qml b/src/tests/quick-tests/tst_JoinRoomPage.qml
index db4831e..7573b51 100644
--- a/src/tests/quick-tests/tst_JoinRoomPage.qml
+++ b/src/tests/quick-tests/tst_JoinRoomPage.qml
@@ -1,86 +1,86 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
import moe.kazv.mxc.kazv 0.0 as MK
QmlHelpers.TestItem {
id: item
Kazv.JoinRoomPage {
id: joinRoomPage
}
TestCase {
id: joinRoomPageTest
name: 'JoinRoomPageTest'
when: windowShown
function initTestCase() {
if (MK.KazvUtil.kfQtMajorVersion === 6) {
joinRoomPage.contentItem.clip = false;
}
}
function cleanup() {
mockHelper.clearAll();
}
function test_joinRoomWithServers() {
findChild(joinRoomPage, 'idOrAliasInput').text = '#foo:example.com';
findChild(joinRoomPage, 'serversInput').text = 'example.com\nexample.org';
const button = findChild(joinRoomPage, 'joinRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.joinRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.joinRoom.lastArgs(), [
+ tryVerify(() => item.matrixSession.joinRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.joinRoom.lastArgs(), [
'#foo:example.com',
['example.com', 'example.org'],
]));
- item.matrixSdk.joinRoom.lastRetVal().resolve(true, {});
+ item.matrixSession.joinRoom.lastRetVal().resolve(true, {});
tryVerify(() => item.pageStack.removePage.calledTimes() === 1);
}
function test_joinRoomWithoutServers() {
findChild(joinRoomPage, 'idOrAliasInput').text = '#foo:example.com';
findChild(joinRoomPage, 'serversInput').text = '';
const button = findChild(joinRoomPage, 'joinRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.joinRoom.calledTimes() === 1, 1000);
- verify(JsHelpers.deepEqual(item.matrixSdk.joinRoom.lastArgs(), [
+ tryVerify(() => item.matrixSession.joinRoom.calledTimes() === 1, 1000);
+ verify(JsHelpers.deepEqual(item.matrixSession.joinRoom.lastArgs(), [
'#foo:example.com',
[],
]));
- item.matrixSdk.joinRoom.lastRetVal().resolve(true, {});
+ item.matrixSession.joinRoom.lastRetVal().resolve(true, {});
tryVerify(() => item.pageStack.removePage.calledTimes() === 1);
}
function test_joinRoomFailure() {
findChild(joinRoomPage, 'idOrAliasInput').text = '#foo:example.com';
findChild(joinRoomPage, 'serversInput').text = '';
const button = findChild(joinRoomPage, 'joinRoomButton');
verify(button.enabled);
mouseClick(button);
- tryVerify(() => item.matrixSdk.joinRoom.calledTimes() === 1, 1000);
+ tryVerify(() => item.matrixSession.joinRoom.calledTimes() === 1, 1000);
- item.matrixSdk.joinRoom.lastRetVal().resolve(false, {});
+ item.matrixSession.joinRoom.lastRetVal().resolve(false, {});
tryVerify(() => item.pageStack.removePage.calledTimes() === 0);
}
}
}
diff --git a/src/tests/quick-tests/tst_LoginPage.qml b/src/tests/quick-tests/tst_LoginPage.qml
index 448b963..4205ed1 100644
--- a/src/tests/quick-tests/tst_LoginPage.qml
+++ b/src/tests/quick-tests/tst_LoginPage.qml
@@ -1,124 +1,124 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtTest
import '../../contents/ui' as Kazv
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
property bool loadingSession: false
Kazv.LoginPage {
id: loginPage
anchors.fill: parent
inTest: true
}
function allFlows() {
return [{
type: 'm.login.sso',
}, {
type: 'm.login.token',
}, {
type: 'm.login.password',
}];
}
TestCase {
id: loginPageTest
name: 'LoginPageTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
const loginFlows = findChild(loginPage, 'loginFlows');
loginFlows.ownLoading = false;
loginFlows.discoveredServerUrl = '';
loginFlows.availableFlows = [];
if (loginFlows.currentItem !== loginFlows.userIdScreen) {
loginFlows.pop();
}
waitForRendering(loginFlows);
findChild(loginPage, 'userIdField').text = '@foo:example.org';
}
function test_noDiscover() {
const loginFlows = findChild(loginPage, 'loginFlows');
findChild(loginPage, 'serverUrlField').text = 'https://mxs.example.org';
waitForRendering(loginFlows);
wait(500);
mouseClick(findChild(loginPage, 'userIdNextButton'));
- tryVerify(() => item.matrixSdk.discoverAndGetLoginFlows.calledTimes() === 1);
- compare(item.matrixSdk.discoverAndGetLoginFlows.lastArgs()[1], 'https://mxs.example.org');
+ tryVerify(() => item.matrixSession.discoverAndGetLoginFlows.calledTimes() === 1);
+ compare(item.matrixSession.discoverAndGetLoginFlows.lastArgs()[1], 'https://mxs.example.org');
}
function test_ssoLoginFlow() {
wait(500);
const loginFlows = findChild(loginPage, 'loginFlows');
findChild(loginPage, 'serverUrlField').text = '';
mouseClick(findChild(loginPage, 'userIdNextButton'));
- tryVerify(() => item.matrixSdk.discoverAndGetLoginFlows.calledTimes() === 1);
- compare(item.matrixSdk.discoverAndGetLoginFlows.lastArgs()[1], '');
- item.matrixSdk.discoverSuccessful('https://mxs.example.org');
+ tryVerify(() => item.matrixSession.discoverAndGetLoginFlows.calledTimes() === 1);
+ compare(item.matrixSession.discoverAndGetLoginFlows.lastArgs()[1], '');
+ item.matrixSession.discoverSuccessful('https://mxs.example.org');
tryVerify(() => loginFlows.discoveredServerUrl === 'https://mxs.example.org');
- item.matrixSdk.getLoginFlowsSuccessful(allFlows());
+ item.matrixSession.getLoginFlowsSuccessful(allFlows());
tryVerify(() => findChild(loginPage, 'passwordRadio').enabled);
tryVerify(() => findChild(loginPage, 'ssoRadio').enabled);
tryVerify(() => findChild(loginPage, 'ssoRadio').visible);
waitForRendering(loginFlows);
wait(500);
mouseClick(findChild(loginPage, 'ssoRadio'));
tryVerify(() => findChild(loginPage, 'ssoButton').visible);
waitForRendering(loginFlows);
wait(500);
mouseClick(findChild(loginPage, 'ssoButton'));
tryVerify(() => findChild(loginPage, 'ssoRedirectUrlLabel').visible);
tryVerify(() => findChild(loginPage, 'ssoRedirectUrlLabel').text.startsWith('https://mxs.example.org/'));
}
function test_passwordLoginFlow() {
wait(500);
const loginFlows = findChild(loginPage, 'loginFlows');
findChild(loginPage, 'serverUrlField').text = '';
mouseClick(findChild(loginPage, 'userIdNextButton'));
- tryVerify(() => item.matrixSdk.discoverAndGetLoginFlows.calledTimes() === 1);
- compare(item.matrixSdk.discoverAndGetLoginFlows.lastArgs()[1], '');
- item.matrixSdk.discoverSuccessful('https://mxs.example.org');
+ tryVerify(() => item.matrixSession.discoverAndGetLoginFlows.calledTimes() === 1);
+ compare(item.matrixSession.discoverAndGetLoginFlows.lastArgs()[1], '');
+ item.matrixSession.discoverSuccessful('https://mxs.example.org');
tryVerify(() => loginFlows.discoveredServerUrl === 'https://mxs.example.org');
- item.matrixSdk.getLoginFlowsSuccessful(allFlows());
+ item.matrixSession.getLoginFlowsSuccessful(allFlows());
tryVerify(() => findChild(loginPage, 'passwordRadio').enabled);
tryVerify(() => findChild(loginPage, 'passwordRadio').visible);
waitForRendering(loginFlows);
wait(500);
mouseClick(findChild(loginPage, 'passwordRadio'));
tryVerify(() => findChild(loginPage, 'passwordField').visible);
findChild(loginPage, 'passwordField').text = 'somePassword';
tryVerify(() => findChild(loginPage, 'passwordLoginButton').visible);
waitForRendering(loginFlows);
wait(500);
mouseClick(findChild(loginPage, 'passwordLoginButton'));
- tryVerify(() => item.matrixSdk.login.calledTimes() === 1);
+ tryVerify(() => item.matrixSession.login.calledTimes() === 1);
}
function test_noFlowSupported() {
wait(500);
const loginFlows = findChild(loginPage, 'loginFlows');
findChild(loginPage, 'serverUrlField').text = '';
mouseClick(findChild(loginPage, 'userIdNextButton'));
- tryVerify(() => item.matrixSdk.discoverAndGetLoginFlows.calledTimes() === 1);
- compare(item.matrixSdk.discoverAndGetLoginFlows.lastArgs()[1], '');
- item.matrixSdk.discoverSuccessful('https://mxs.example.org');
+ tryVerify(() => item.matrixSession.discoverAndGetLoginFlows.calledTimes() === 1);
+ compare(item.matrixSession.discoverAndGetLoginFlows.lastArgs()[1], '');
+ item.matrixSession.discoverSuccessful('https://mxs.example.org');
tryVerify(() => loginFlows.discoveredServerUrl === 'https://mxs.example.org');
- item.matrixSdk.getLoginFlowsSuccessful([{ type: 'm.login.xxx' }]);
+ item.matrixSession.getLoginFlowsSuccessful([{ type: 'm.login.xxx' }]);
tryVerify(() => !findChild(loginPage, 'passwordRadio').enabled);
tryVerify(() => !findChild(loginPage, 'ssoRadio').enabled);
tryVerify(() => findChild(loginPage, 'noFlowIndicator').visible);
}
}
}
diff --git a/src/tests/quick-tests/tst_MatrixLink.qml b/src/tests/quick-tests/tst_MatrixLink.qml
index c19d888..ad41377 100644
--- a/src/tests/quick-tests/tst_MatrixLink.qml
+++ b/src/tests/quick-tests/tst_MatrixLink.qml
@@ -1,218 +1,217 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtTest
import QtQml.Models
import 'test-helpers' as QmlHelpers
import 'test-helpers.js' as JsHelpers
import '../../contents/ui/event-types' as Events
import moe.kazv.mxc.kazv as MK
QmlHelpers.TestItem {
id: item
property var mentionUserRequested: mockHelper.noop()
- property var matrixSdk: QmlHelpers.MatrixSdkMock {}
property var switchToRoomRequested: mockHelper.noop()
property var pushCreateRoomPage: mockHelper.noop()
property var pageStack: ({
currentItem: {
presetPage: mockHelper.noop(),
delayGoToEvent: mockHelper.noop()
}
})
property var sdkVars: ({
roomList: {
contains: (roomId) => {
const rooms = [
"!directroom:example.org",
"!joinedroomid:example.org",
"!invitedroomid:example.org",
"!leftroomid:example.org"];
return rooms.includes(roomId);
},
room: (roomId) => {
const rooms = {
"!joinedroomid:example.org": JsHelpers.factory.room({
membership: MK.MatrixRoom.Join
}),
"!invitedroomid:example.org": JsHelpers.factory.room({
membership: MK.MatrixRoom.Invite
}),
"!leftroomid:example.org": JsHelpers.factory.room({
membership: MK.MatrixRoom.Leave
})
};
return rooms[roomId];
}
}
})
property var pushJoinRoomPage: mockHelper.noop()
Events.Text {
id: textEvent
}
TestCase {
id: matrixLinkTest
name: 'matrixLinkTest'
when: windowShown
function init() {
mockHelper.clearAll();
}
function test_matrixSchemeUser() {
textEvent.openLink("matrix:u/someone:example.org");
compare(mentionUserRequested.calledTimes(), 1);
compare(mentionUserRequested.lastArgs()[0], "@someone:example.org");
}
function test_matrixToUser() {
textEvent.openLink("https://matrix.to/#/@someone:example.org");
compare(mentionUserRequested.calledTimes(), 1);
compare(mentionUserRequested.lastArgs()[0], "@someone:example.org");
}
function test_matrixSchemeUserJoined() {
textEvent.openLink("matrix:u/someonejoined:example.org?action=chat");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
}
function test_matrixSchemeUserInvited() {
textEvent.openLink("matrix:u/someoneinvited:example.org?action=chat");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!invitedroomid:example.org");
}
function test_matrixSchemeUserLeft() {
textEvent.openLink("matrix:u/someoneleft:example.org?action=chat");
compare(pushCreateRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "@someoneleft:example.org");
}
function test_matrixSchemeUserCreateDirectRoom() {
textEvent.openLink("matrix:u/someonewithoutchat:example.org?action=chat");
compare(pushCreateRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "@someonewithoutchat:example.org");
}
function test_matrixSchemeRoomId() {
textEvent.openLink("matrix:roomid/joinedroomid:example.org");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
}
function test_matrixToRoomId() {
textEvent.openLink("https://matrix.to/#/!joinedroomid:example.org");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
}
function test_matrixSchemeRoomAlias() {
textEvent.openLink("matrix:r/joinedroomalias:example.org");
- compare(matrixSdk.getRoomIdByAlias.calledTimes(), 1);
- compare(matrixSdk.getRoomIdByAlias.lastArgs()["roomAlias"], "#joinedroomalias:example.org")
- matrixSdk.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!joinedroomid:example.org" });
+ compare(matrixSession.getRoomIdByAlias.calledTimes(), 1);
+ compare(matrixSession.getRoomIdByAlias.lastArgs()["roomAlias"], "#joinedroomalias:example.org")
+ matrixSession.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!joinedroomid:example.org" });
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
}
function test_matrixToRoomAlias() {
textEvent.openLink("https://matrix.to/#/#joinedroomalias:example.org");
- compare(matrixSdk.getRoomIdByAlias.calledTimes(), 1);
- compare(matrixSdk.getRoomIdByAlias.lastArgs()["roomAlias"], "#joinedroomalias:example.org")
- matrixSdk.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!joinedroomid:example.org" });
+ compare(matrixSession.getRoomIdByAlias.calledTimes(), 1);
+ compare(matrixSession.getRoomIdByAlias.lastArgs()["roomAlias"], "#joinedroomalias:example.org")
+ matrixSession.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!joinedroomid:example.org" });
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
}
function test_matrixSchemeRoomIdUnjoined() {
textEvent.openLink("matrix:roomid/unjoinedroomid:example.org?action=join&via=server1.ca&via=server2.ca");
compare(pushJoinRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "!unjoinedroomid:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixToRoomIdUnjoined() {
textEvent.openLink("https://matrix.to/#/!unjoinedroomid:example.org?via=server1.ca&via=server2.ca");
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "!unjoinedroomid:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixSchemeRoomAliasUnjoined() {
textEvent.openLink("matrix:r/unjoinedroomalias:example.org?action=join&via=server1.ca&via=server2.ca");
- compare(matrixSdk.getRoomIdByAlias.calledTimes(), 1);
- compare(matrixSdk.getRoomIdByAlias.lastArgs()["roomAlias"], "#unjoinedroomalias:example.org")
- matrixSdk.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!unjoinedroomid:example.org" });
+ compare(matrixSession.getRoomIdByAlias.calledTimes(), 1);
+ compare(matrixSession.getRoomIdByAlias.lastArgs()["roomAlias"], "#unjoinedroomalias:example.org")
+ matrixSession.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!unjoinedroomid:example.org" });
compare(pushJoinRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "#unjoinedroomalias:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixToRoomAliasUnjoined() {
textEvent.openLink("https://matrix.to/#/#unjoinedroomalias:example.org?action=join&via=server1.ca&via=server2.ca");
- compare(matrixSdk.getRoomIdByAlias.calledTimes(), 1);
- compare(matrixSdk.getRoomIdByAlias.lastArgs()["roomAlias"], "#unjoinedroomalias:example.org")
- matrixSdk.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!unjoinedroomid:example.org" });
+ compare(matrixSession.getRoomIdByAlias.calledTimes(), 1);
+ compare(matrixSession.getRoomIdByAlias.lastArgs()["roomAlias"], "#unjoinedroomalias:example.org")
+ matrixSession.getRoomIdByAlias.lastRetVal().resolve(true, { "roomId": "!unjoinedroomid:example.org" });
compare(pushJoinRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "#unjoinedroomalias:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixSchemeEvent() {
textEvent.openLink("matrix:roomid/joinedroomid:example.org/e/eventid");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
compare(pageStack.currentItem.delayGoToEvent.calledTimes(), 1);
compare(pageStack.currentItem.delayGoToEvent.lastArgs()[0], "$eventid");
}
function test_matrixToEvent() {
textEvent.openLink("https://matrix.to/#/!joinedroomid:example.org/$eventid");
compare(switchToRoomRequested.calledTimes(), 1);
compare(switchToRoomRequested.lastArgs()[0], "!joinedroomid:example.org");
compare(pageStack.currentItem.delayGoToEvent.calledTimes(), 1);
compare(pageStack.currentItem.delayGoToEvent.lastArgs()[0], "$eventid");
}
function test_matrixSchemeEventUnjoined() {
textEvent.openLink("matrix:roomid/unjoinedroomid:example.org/e/eventid?via=server1.ca&via=server2.ca");
compare(pushJoinRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "!unjoinedroomid:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixToEventUnjoined() {
textEvent.openLink("https://matrix.to/#/!unjoinedroomid:example.org/$eventid?via=server1.ca&via=server2.ca");
compare(pushJoinRoomPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.calledTimes(), 1);
compare(pageStack.currentItem.presetPage.lastArgs()[0], "!unjoinedroomid:example.org");
compare(pageStack.currentItem.presetPage.lastArgs()[1], ["server1.ca", "server2.ca"]);
}
function test_matrixSchemeRoomAliasFailed() {
textEvent.openLink("matrix:r/room:example.org");
- compare(matrixSdk.getRoomIdByAlias.calledTimes(), 1);
- compare(matrixSdk.getRoomIdByAlias.lastArgs()["roomAlias"], "#room:example.org")
- matrixSdk.getRoomIdByAlias.lastRetVal().resolve(false, { errorCode: 'M_NOT_FOUND', error: 'Room alias #room:example.org not found.' });
+ compare(matrixSession.getRoomIdByAlias.calledTimes(), 1);
+ compare(matrixSession.getRoomIdByAlias.lastArgs()["roomAlias"], "#room:example.org")
+ matrixSession.getRoomIdByAlias.lastRetVal().resolve(false, { errorCode: 'M_NOT_FOUND', error: 'Room alias #room:example.org not found.' });
compare(switchToRoomRequested.calledTimes(), 0);
compare(showPassiveNotification.calledTimes(), 1);
compare(showPassiveNotification.lastArgs()[0], l10n.get('get-room-id-by-alias-failed-prompt',
{ errorCode: 'M_NOT_FOUND', errorMsg: 'Room alias #room:example.org not found.' }));
}
}
}
diff --git a/src/tests/quick-tests/tst_RoomListViewItemDelegate.qml b/src/tests/quick-tests/tst_RoomListViewItemDelegate.qml
index a9c553d..ad99a7d 100644
--- a/src/tests/quick-tests/tst_RoomListViewItemDelegate.qml
+++ b/src/tests/quick-tests/tst_RoomListViewItemDelegate.qml
@@ -1,313 +1,313 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.3
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
import moe.kazv.mxc.kazv 0.0 as MK
QmlHelpers.TestItem {
id: item
property var roomInvite: JsHelpers.factory.room({
membership: MK.MatrixRoom.Invite,
unreadNotificationCount: 0,
tombstoned: true,
})
property var roomJoin: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
unreadNotificationCount: 0,
})
property var roomLeave: JsHelpers.factory.room({
membership: MK.MatrixRoom.Leave,
unreadNotificationCount: 0,
})
property var roomWithUnreadNotificationCount: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
unreadNotificationCount: 2,
})
property var roomTombstone: JsHelpers.factory.room({
tombstoned: true,
unreadNotificationCount: 0,
})
property var timelineUnread: QtObject {
property var count: 5
function at(i) {
if (i < 2) {
return {
isLocalEcho: true,
readers: () => ({ count: 0 }),
};
}
if (i < 3) {
return {
isLocalEcho: false,
sender: '@bar:example.org',
eventId: `$event${i}`,
readers: () => ({ count: 0 }),
};
}
return {
isLocalEcho: false,
sender: '@foo:example.org',
eventId: `$event${i}`,
readers: () => ({ count: 0 }),
};
}
}
property var timelineLastSentIsMine: QtObject {
property var count: 5
function at(i) {
if (i < 2) {
return {
isLocalEcho: true,
readers: () => ({ count: 0 }),
};
}
if (i < 3) {
return {
isLocalEcho: false,
sender: '@foo:example.org',
eventId: `$event${i}`,
readers: () => ({ count: 0 }),
};
}
return {
isLocalEcho: false,
sender: '@bar:example.org',
eventId: `$event${i}`,
readers: () => ({ count: 0 }),
};
}
}
property var timelineRead: QtObject {
property var count: 5
function at(i) {
if (i < 2) {
return {
isLocalEcho: true,
readers: () => ({ count: 0 }),
};
}
if (i < 3) {
return {
isLocalEcho: false,
sender: '@bar:example.org',
eventId: `$event${i}`,
readers: () => ({
count: 1,
at() {
return { userId: '@foo:example.org' };
},
}),
};
}
return {
isLocalEcho: false,
sender: '@foo:example.org',
eventId: `$event${i}`,
readers: () => ({ count: 0 }),
};
}
}
property string testRoomId: 'room-test'
signal switchToRoomRequested(string roomId)
SignalSpy {
id: switchToRoomSpy
target: item
signalName: 'switchToRoomRequested'
}
ColumnLayout {
anchors.fill: parent
Kazv.RoomListViewItemDelegate {
id: delegateInvite
item: roomInvite
}
Kazv.RoomListViewItemDelegate {
id: delegateJoin
item: roomJoin
}
Kazv.RoomListViewItemDelegate {
id: delegateLeave
item: roomLeave
}
Kazv.RoomListViewItemDelegate {
id: delegateRoom
item: JsHelpers.factory.room({
roomId: testRoomId
})
}
Kazv.RoomListViewItemDelegate {
id: delegateRoomWithName
item: QtObject {
property var roomId: testRoomId
property var name: 'some name'
}
}
Kazv.RoomListViewItemDelegate {
id: delegateRoomWithHeroes
item: QtObject {
property var roomId: testRoomId
property var heroEvents: [{
type: 'm.room.member',
state_key: '@foo:example.com',
content: {
displayname: 'foo',
},
}, {
type: 'm.room.member',
state_key: '@bar:example.com',
content: {
displayname: 'bar',
},
}]
}
}
Kazv.RoomListViewItemDelegate {
id: delegateUnread
item: Object.assign({}, roomJoin, {
timeline () {
return timelineUnread;
},
})
}
Kazv.RoomListViewItemDelegate {
id: delegateWithUnreadNotifications
item: Object.assign({}, roomWithUnreadNotificationCount, {
timeline () {
return timelineUnread;
},
})
}
Kazv.RoomListViewItemDelegate {
id: delegateTombstone
item: roomTombstone
}
}
TestCase {
id: roomListViewItemDelegateTest
name: 'RoomListViewItemDelegateTest'
when: windowShown
function initTestCase() {
- item.matrixSdk.userId = '@foo:example.org';
+ item.matrixSession.userId = '@foo:example.org';
item.sdkVars.userGivenNicknameMap.map = {
'@foo:example.com': 'something',
};
}
function test_inviteIndicator() {
const indicator = findChild(delegateInvite, 'inviteIndicator');
verify(indicator);
verify(indicator.visible);
const leaveIndicator = findChild(delegateInvite, 'leaveIndicator');
verify(leaveIndicator);
verify(!leaveIndicator.visible);
}
function test_inviteIndicatorJoin() {
const indicator = findChild(delegateJoin, 'inviteIndicator');
verify(indicator);
verify(!indicator.visible);
const leaveIndicator = findChild(delegateJoin, 'leaveIndicator');
verify(leaveIndicator);
verify(!leaveIndicator.visible);
}
function test_inviteIndicatorLeave() {
const indicator = findChild(delegateLeave, 'inviteIndicator');
verify(indicator);
verify(!indicator.visible);
const leaveIndicator = findChild(delegateLeave, 'leaveIndicator');
verify(leaveIndicator);
verify(leaveIndicator.visible);
}
function test_lastUnreadMessage() {
const room = {};
compare(delegateJoin.getLastUnreadMessage(timelineUnread, timelineUnread.count, room).eventId, '$event2');
compare(delegateJoin.getLastUnreadMessage(timelineLastSentIsMine, timelineLastSentIsMine.count, room), undefined);
compare(delegateJoin.getLastUnreadMessage(timelineRead, timelineRead.count, room), undefined);
room.localReadMarker = '$event2';
compare(delegateJoin.getLastUnreadMessage(timelineUnread, timelineUnread.count, room), undefined);
}
function test_returnRoom() {
compare(switchToRoomSpy.count, 0);
mouseClick(delegateRoom);
tryCompare(switchToRoomSpy, 'count', 1);
verify(JsHelpers.deepEqual(switchToRoomSpy.signalArguments[0], [testRoomId]));
}
function test_roomName() {
compare(
findChild(delegateRoomWithName, 'roomDisplayNameLabel').text,
l10n.get('room-list-view-room-item-title-name', { name: 'some name' }));
compare(
findChild(delegateRoomWithHeroes, 'roomDisplayNameLabel').text,
l10n.get('room-list-view-room-item-title-heroes', {
hero: l10n.get(
'user-name-overrided',
{ overridedName: 'something', globalName: 'foo' }),
secondHero: 'bar',
otherNum: 1,
}));
}
function test_unreadIndicators() {
verify(!findChild(delegateWithUnreadNotifications, 'unreadIndicator').visible);
verify(findChild(delegateWithUnreadNotifications, 'unreadNotificationCount').visible);
verify(findChild(delegateUnread, 'unreadIndicator').visible);
verify(!findChild(delegateUnread, 'unreadNotificationCount').visible);
}
function test_tombstoneIndictor() {
const indicator = findChild(delegateTombstone, 'tombstoneIndicator');
verify(indicator);
verify(indicator.visible);
const indicatorJoin = findChild(delegateJoin, 'tombstoneIndicator');
verify(indicatorJoin);
verify(!indicatorJoin.visible);
}
}
}
diff --git a/src/tests/quick-tests/tst_RoomPage.qml b/src/tests/quick-tests/tst_RoomPage.qml
index 4ceb10e..e549b2b 100644
--- a/src/tests/quick-tests/tst_RoomPage.qml
+++ b/src/tests/quick-tests/tst_RoomPage.qml
@@ -1,200 +1,200 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.3
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
import moe.kazv.mxc.kazv 0.0 as MK
QmlHelpers.TestItem {
id: item
property var roomTimeline: QtObject {
property var count: 5
function at(i) {
if (i < 2) {
return {
isLocalEcho: true,
};
}
if (i < 3) {
return {
isLocalEcho: false,
sender: '@foo:example.org',
eventId: `$event${i}`,
};
}
return {
isLocalEcho: false,
sender: '@bar:example.org',
eventId: `$event${i}`,
};
}
}
property var roomInvite: JsHelpers.factory.room({
membership: MK.MatrixRoom.Invite,
})
property var roomJoin: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
paginateBackFrom: mockHelper.promise(),
pinnedEventsTimeline: () => ({
count: 0,
}),
})
property var roomLeave: JsHelpers.factory.room({
membership: MK.MatrixRoom.Leave,
})
property var roomWithPinned: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
paginateBackFrom: mockHelper.promise(),
pinnedEventsTimeline: () => ({
count: 1,
}),
})
property var roomScrollToLatest: JsHelpers.factory.room({
timeline: () => Qt.createComponent("QtQml.Models", "ListModel").createObject()
})
Component {
id: pageInviteComp
Kazv.RoomPage {
id: pageInvite
room: roomInvite
}
}
Component {
id: pageJoinComp
Kazv.RoomPage {
id: pageJoin
room: roomJoin
}
}
Component {
id: pageLeaveComp
Kazv.RoomPage {
id: pageLeave
room: roomLeave
}
}
Component {
id: pageWithPinnedComp
Kazv.RoomPage {
id: pageWithPinned
room: roomWithPinned
}
}
Component {
id: pageScrollToLatestComp
Kazv.RoomPage {
id: pageScrollToLatest
room: roomScrollToLatest
}
}
Loader {
id: pageLoader
anchors.fill: item
}
TestCase {
id: roomPageTest
name: 'RoomPageTest'
when: windowShown
function init() {
- matrixSdk.userId = '@foo:example.org';
+ matrixSession.userId = '@foo:example.org';
mockHelper.clearAll();
}
function test_inviteRoom() {
pageLoader.sourceComponent = pageInviteComp;
const sendMessageBox = findChild(pageLoader.item, 'sendMessageBox');
verify(sendMessageBox);
verify(!sendMessageBox.enabled);
}
function test_joinRoom() {
pageLoader.sourceComponent = pageJoinComp;
const sendMessageBox = findChild(pageLoader.item, 'sendMessageBox');
verify(sendMessageBox);
verify(sendMessageBox.enabled);
}
function test_leaveRoom() {
pageLoader.sourceComponent = pageLeaveComp;
const sendMessageBox = findChild(pageLoader.item, 'sendMessageBox');
verify(sendMessageBox);
verify(!sendMessageBox.enabled);
}
function test_receiptableEventId() {
pageLoader.sourceComponent = pageJoinComp;
verify(pageLoader.item.getLastReceiptableEventId(item.roomTimeline, item.roomTimeline.count) === '$event3');
}
function test_paginateBack() {
pageLoader.sourceComponent = pageJoinComp;
pageLoader.item.paginateBackRequested('$1');
tryVerify(() => roomJoin.paginateBackFrom.calledTimes() === 1);
verify(roomJoin.paginateBackFrom.lastArgs()[0] === '$1');
// one pagination under way, do not call it twice
pageLoader.item.paginateBackRequested('$1');
verify(roomJoin.paginateBackFrom.calledTimes() === 1);
// paginating from another event is not affected
pageLoader.item.paginateBackRequested('$2');
tryVerify(() => roomJoin.paginateBackFrom.calledTimes() === 2);
verify(roomJoin.paginateBackFrom.lastArgs()[0] === '$2');
// after the promise resolves, we can paginate from this id again
roomJoin.paginateBackFrom.retVals[0].resolve(true);
pageLoader.item.paginateBackRequested('$1');
tryVerify(() => roomJoin.paginateBackFrom.calledTimes() === 3);
verify(roomJoin.paginateBackFrom.lastArgs()[0] === '$1');
}
function test_pinnedAction() {
pageLoader.sourceComponent = pageJoinComp;
verify(!findChild(pageLoader.item, 'pinnedEventsAction').visible);
verify(!findChild(pageLoader.item, 'pinnedEventsAction').enabled);
pageLoader.sourceComponent = pageWithPinnedComp;
verify(findChild(pageLoader.item, 'pinnedEventsAction').visible);
verify(findChild(pageLoader.item, 'pinnedEventsAction').enabled);
}
function test_scrollToLatest() {
pageLoader.sourceComponent = pageScrollToLatestComp;
const action = findChild(pageLoader.item, 'scrollToLatestAction');
const roomTimelineView = findChild(pageLoader.item, 'roomTimelineView');
verify(action.visible);
verify(!action.enabled);
while (item.height >= roomTimelineView.contentHeight) {
roomTimelineView.model.append({});
roomTimelineView.forceLayout();
}
roomTimelineView.positionViewAtEnd();
verify(action.enabled);
action.triggered();
verify(!action.enabled);
verify(roomTimelineView.atYEnd);
}
}
}
diff --git a/src/tests/quick-tests/tst_RoomSettingsPage.qml b/src/tests/quick-tests/tst_RoomSettingsPage.qml
index 709c1f8..92a94cd 100644
--- a/src/tests/quick-tests/tst_RoomSettingsPage.qml
+++ b/src/tests/quick-tests/tst_RoomSettingsPage.qml
@@ -1,335 +1,335 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import '../../contents/ui/room-settings' as KazvRS
import moe.kazv.mxc.kazv 0.0 as MK
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
property var roomUnencrypted: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
encrypted: false,
sendStateEvent: mockHelper.promise(),
})
property var roomEncrypted: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
encrypted: true,
})
property var roomLeft: JsHelpers.factory.room({
membership: MK.MatrixRoom.Leave,
forgetRoom: mockHelper.promise(),
})
property var roomJoined: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
leaveRoom: mockHelper.promise(),
topic: '',
})
property var roomWithMembers: JsHelpers.factory.room({
name: 'some name',
roomId: '!someid:example.com',
members: () => ({
at(index) {
return {
userId: `@u${index}:example.com`,
name: `user ${index}`,
};
},
count: 5
}),
setName: mockHelper.promise(),
})
property var roomWithoutName: JsHelpers.factory.room({
name: '',
roomId: '!someid:example.com',
})
property var roomWithTopic: JsHelpers.factory.room({
membership: MK.MatrixRoom.Join,
topic: 'some topic',
setTopic: mockHelper.promise(),
})
ColumnLayout {
KazvRS.RoomSettingsPage {
id: pageUnencrypted
room: item.roomUnencrypted
}
KazvRS.RoomSettingsPage {
id: pageEncrypted
room: item.roomEncrypted
}
KazvRS.RoomSettingsPage {
id: pageLeft
room: item.roomLeft
}
KazvRS.RoomSettingsPage {
id: pageJoined
room: item.roomJoined
}
KazvRS.RoomSettingsPage {
id: pageWithMembers
room: item.roomWithMembers
}
KazvRS.RoomSettingsPage {
id: pageWithoutName
room: item.roomWithoutName
}
KazvRS.RoomSettingsPage {
id: pageWithTopic
room: item.roomWithTopic
}
}
TestCase {
id: roomSettingsPageTest
name: 'RoomSettingsPageTest'
when: windowShown
function initTestCase() {
pageJoined.contentItem.clip = false;
pageWithTopic.contentItem.clip = false;
pageWithMembers.contentItem.clip = false;
- item.matrixSdk.userId = '@foo:example.org';
+ item.matrixSession.userId = '@foo:example.org';
}
function init() {
pageJoined.submittingTopic = false;
pageJoined.editingTopic = false;
pageWithTopic.submittingTopic = false;
pageWithTopic.editingTopic = false;
pageWithMembers.submittingName = false;
pageWithMembers.editingName = false;
mockHelper.clearAll();
}
function test_encryptionIndicator() {
compare(findChild(pageUnencrypted, 'encryptionIndicator').text, l10n.get('room-settings-not-encrypted'));
verify(findChild(pageUnencrypted, 'enableEncryptionButton').visible);
compare(findChild(pageEncrypted, 'encryptionIndicator').text, l10n.get('room-settings-encrypted'));
verify(!findChild(pageEncrypted, 'enableEncryptionButton').visible);
}
function test_enableEncryption() {
pageUnencrypted.encryptionPopup.accepted();
tryVerify(() => roomUnencrypted.sendStateEvent.lastArgs()[0].type === 'm.room.encryption');
tryVerify(() => roomUnencrypted.sendStateEvent.lastArgs()[0].state_key === '');
verify(!findChild(pageUnencrypted, 'enableEncryptionButton').enabled);
roomUnencrypted.sendStateEvent.lastRetVal().resolve(true, {});
tryVerify(() => findChild(pageUnencrypted, 'enableEncryptionButton').enabled);
}
function test_forgetRoom() {
const forgetRoomButton = findChild(pageLeft, 'forgetRoomButton');
const confirmPopup = findChild(pageLeft, 'confirmForgetRoomPopup');
verify(forgetRoomButton.visible);
verify(!findChild(pageJoined, 'forgetRoomButton').visible);
confirmPopup.accepted();
tryVerify(() => roomLeft.forgetRoom.calledTimes() === 1);
roomLeft.forgetRoom.lastRetVal().resolve(true, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_forgetRoomFailure() {
const confirmPopup = findChild(pageLeft, 'confirmForgetRoomPopup');
confirmPopup.accepted();
roomLeft.forgetRoom.lastRetVal().resolve(false, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_leaveRoom() {
const leaveRoomButton = findChild(pageJoined, 'leaveRoomButton');
const confirmPopup = findChild(pageJoined, 'confirmLeaveRoomPopup');
verify(leaveRoomButton.visible);
confirmPopup.accepted();
tryVerify(() => roomJoined.leaveRoom.calledTimes() === 1);
roomJoined.leaveRoom.lastRetVal().resolve(true, {});
console.log(roomJoined.membership);
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_leaveRoomFailure() {
const confirmPopup = findChild(pageJoined, 'confirmLeaveRoomPopup');
confirmPopup.accepted();
roomJoined.leaveRoom.lastRetVal().resolve(false, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_roomMembersAvatars() {
const repeater = findChild(pageWithMembers, 'roomMembersAvatarsRepeater');
tryVerify(() => repeater.count === 4);
const more = findChild(pageWithMembers, 'roomMembersAvatarsMore');
verify(more.visible);
}
function test_roomWithName() {
const nameLabel = findChild(pageWithMembers, 'roomNameLabel');
const idLabel = findChild(pageWithMembers, 'roomIdLabel');
verify(nameLabel.text === 'some name');
verify(idLabel.text === '!someid:example.com');
}
function test_roomWithoutName() {
const nameLabel = findChild(pageWithoutName, 'roomNameLabel');
const idLabel = findChild(pageWithMembers, 'roomIdLabel');
compare(nameLabel.text, l10n.get('room-settings-name-missing'));
compare(idLabel.text, '!someid:example.com');
}
function test_roomWithoutTopic() {
const topicLabel = findChild(pageJoined, 'roomTopicLabel');
verify(topicLabel.font.italic);
compare(topicLabel.text, l10n.get('room-settings-topic-missing'));
}
function test_roomWithTopic() {
const topicLabel = findChild(pageWithTopic, 'roomTopicLabel');
verify(!topicLabel.font.italic);
compare(topicLabel.text, 'some topic');
}
function test_editRoomTopic() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
verify(saveTopicButton.visible);
verify(saveTopicButton.enabled);
const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
verify(discardTopicButton.visible);
verify(discardTopicButton.enabled);
mouseClick(saveTopicButton);
tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
compare(roomWithTopic.setTopic.lastArgs()[0], 'other topic');
verify(!saveTopicButton.enabled);
verify(!discardTopicButton.enabled);
roomWithTopic.setTopic.lastRetVal().resolve(true, {});
tryVerify(() => editButton.visible);
verify(!saveTopicButton.visible);
}
function test_editRoomTopicFailed() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
mouseClick(saveTopicButton);
tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
roomWithTopic.setTopic.lastRetVal().resolve(false, {});
verify(saveTopicButton.enabled);
verify(textArea.visible);
}
function test_editRoomTopicDiscard() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
mouseClick(discardTopicButton);
compare(roomWithTopic.setTopic.calledTimes(), 0);
verify(!textArea.visible);
mouseClick(editButton);
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
}
function test_addRoomTopic() {
const editButton = findChild(pageJoined, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageJoined, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === '');
}
function test_editRoomName() {
const editButton = findChild(pageWithMembers, 'editNameButton');
mouseClick(editButton);
const textArea = findChild(pageWithMembers, 'roomNameEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some name');
textArea.text = 'other name';
const saveNameButton = findChild(pageWithMembers, 'saveNameButton');
verify(saveNameButton.visible);
verify(saveNameButton.enabled);
const discardNameButton = findChild(pageWithMembers, 'discardNameButton');
verify(discardNameButton.visible);
verify(discardNameButton.enabled);
mouseClick(saveNameButton);
tryVerify(() => roomWithMembers.setName.calledTimes() === 1);
compare(roomWithMembers.setName.lastArgs()[0], 'other name');
verify(!saveNameButton.enabled);
verify(!discardNameButton.enabled);
roomWithMembers.setName.lastRetVal().resolve(true, {});
tryVerify(() => editButton.visible);
verify(!saveNameButton.visible);
}
function test_editRoomNameFailed() {
const editButton = findChild(pageWithMembers, 'editNameButton');
mouseClick(editButton);
const textArea = findChild(pageWithMembers, 'roomNameEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some name');
textArea.text = 'other name';
const saveNameButton = findChild(pageWithMembers, 'saveNameButton');
mouseClick(saveNameButton);
tryVerify(() => roomWithMembers.setName.calledTimes() === 1);
roomWithMembers.setName.lastRetVal().resolve(false, {});
verify(saveNameButton.enabled);
verify(textArea.visible);
}
function test_editRoomNameDiscard() {
const editButton = findChild(pageWithMembers, 'editNameButton');
mouseClick(editButton);
const textArea = findChild(pageWithMembers, 'roomNameEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some name');
textArea.text = 'other name';
const discardNameButton = findChild(pageWithMembers, 'discardNameButton');
mouseClick(discardNameButton);
compare(roomWithMembers.setName.calledTimes(), 0);
verify(!textArea.visible);
mouseClick(editButton);
tryVerify(() => textArea.visible);
verify(textArea.text === 'some name');
}
}
}
diff --git a/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml b/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
index e0f20f2..db08303 100644
--- a/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
+++ b/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
@@ -1,87 +1,87 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtTest
import moe.kazv.mxc.kazv as MK
import org.kde.kirigami as Kirigami
import '../../contents/ui/room-settings' as KazvRS
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
KazvRS.RoomStickerPackItemDelegate {
id: packItem
}
TestCase {
id: roomStickerPackItemDelegateTest
name: 'RoomStickerPackItemDelegateTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
packItem.stickerPack = {
packName: 'some pack',
stateKey: 'some-pack',
roomId: '!foo:example.com',
isRoomState: true,
};
packItem.stickerRoomsEvent = {
content: {
},
};
}
function test_usePack() {
const usePackAction = findChild(packItem, 'usePackAction');
verify(!usePackAction.checked);
usePackAction.trigger();
- tryVerify(() => matrixSdk.sendAccountData.calledTimes() === 1);
+ tryVerify(() => matrixSession.sendAccountData.calledTimes() === 1);
verify(JsHelpers.deepEqual(
- matrixSdk.sendAccountData.lastArgs()[1],
+ matrixSession.sendAccountData.lastArgs()[1],
{
rooms: {
'!foo:example.com': {
'some-pack': {},
},
},
},
));
}
function test_disusePack() {
packItem.stickerRoomsEvent = {
content: {
rooms: {
'!foo:example.com': {
'some-pack': {},
},
},
},
};
const usePackAction = findChild(packItem, 'usePackAction');
verify(usePackAction.checked);
usePackAction.trigger();
- tryVerify(() => matrixSdk.sendAccountData.calledTimes() === 1);
+ tryVerify(() => matrixSession.sendAccountData.calledTimes() === 1);
verify(JsHelpers.deepEqual(
- matrixSdk.sendAccountData.lastArgs()[1],
+ matrixSession.sendAccountData.lastArgs()[1],
{
rooms: {
'!foo:example.com': {
},
},
},
));
}
}
}
diff --git a/src/tests/quick-tests/tst_SecuritySettings.qml b/src/tests/quick-tests/tst_SecuritySettings.qml
index d5151ae..66d2d52 100644
--- a/src/tests/quick-tests/tst_SecuritySettings.qml
+++ b/src/tests/quick-tests/tst_SecuritySettings.qml
@@ -1,49 +1,49 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtTest
import '../../contents/ui' as Kazv
import '../../contents/ui/settings' as KazvSettings
import moe.kazv.mxc.kazv 0.0 as MK
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
KazvSettings.SecuritySettings {
id: securitySettings
anchors.fill: parent
}
TestCase {
id: securitySettingsTest
name: 'SecuritySettingsTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
}
function test_importKeys() {
const button = findChild(securitySettings, 'importKeysButton');
mouseClick(button);
const dialog = findChild(securitySettings, 'fileDialog');
dialog.accept();
securitySettings.fileToImport = 'file:///tmp/test';
const input = findChild(securitySettings.passwordInputOverlay, 'passwordInput');
input.text = 'test';
securitySettings.passwordInputOverlay.accepted();
securitySettings.passwordInputOverlay.close();
- compare(item.matrixSdk.importFromKeyBackupFile.calledTimes(), 1);
- compare(item.matrixSdk.importFromKeyBackupFile.lastArgs().fileUrl, 'file:///tmp/test');
- compare(item.matrixSdk.importFromKeyBackupFile.lastArgs().password, 'test');
+ compare(item.matrixSession.importFromKeyBackupFile.calledTimes(), 1);
+ compare(item.matrixSession.importFromKeyBackupFile.lastArgs().fileUrl, 'file:///tmp/test');
+ compare(item.matrixSession.importFromKeyBackupFile.lastArgs().password, 'test');
}
}
}
diff --git a/src/tests/quick-tests/tst_SendMessageBox.qml b/src/tests/quick-tests/tst_SendMessageBox.qml
index f7b5c5a..29bd6eb 100644
--- a/src/tests/quick-tests/tst_SendMessageBox.qml
+++ b/src/tests/quick-tests/tst_SendMessageBox.qml
@@ -1,245 +1,245 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtTest 1.0
import org.kde.kirigami 2.13 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
function makeRoom () {
return {
timeline () {
return {
count: 0,
gaps: {},
at () { return {}; },
};
},
messageById () { return {}; },
member () { return {}; },
localDraft: '',
setLocalDraft (localDraft) {
this.localDraft = localDraft;
},
updateLocalDraftNow () {
},
members () {
return membersModel;
},
setTyping: mockHelper.noop(),
sendTextMessage: mockHelper.noop(),
roomId: '!some-event-id',
encrypted: true,
};
}
property var room: makeRoom()
property var membersModel: ListModel {
ListElement { userId: '@foo:example.com' }
ListElement { userId: '@bar:example.com' }
property string filter
property var count: 2
function at(index) {
return get(index);
}
}
Kazv.SendMessageBox {
id: sendMessageBox
y: 400
room: item.room
}
TestCase {
id: sendMessageBoxTest
name: 'SendMessageBoxTest'
when: windowShown
function init() {
membersModel.count = 2;
findChild(sendMessageBox, 'draftMessage').text = '';
mockHelper.clearAll();
}
function test_empty() {
const textArea = findChild(sendMessageBox, 'draftMessage');
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtEnd() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test';
textArea.cursorPosition = textArea.text.length;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some test @foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtEndSpacingBefore() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test\n';
textArea.cursorPosition = textArea.text.length;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some test\n@foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtBeginning() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test';
textArea.cursorPosition = 0;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com some test');
verify(textArea.cursorPosition === '@foo:example.com '.length);
}
function test_cursorAtBeginningSpacingAfter() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = '\nsome test';
textArea.cursorPosition = 0;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com\nsome test');
verify(textArea.cursorPosition === '@foo:example.com\n'.length);
}
function test_cursorInMiddleNoSpacing() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
textArea.cursorPosition = 1;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 's @foo:example.com ome\ntest');
verify(textArea.cursorPosition === 's @foo:example.com '.length);
}
function test_cursorInMiddleSpacingBefore() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
textArea.cursorPosition = 5;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some\n@foo:example.com test');
verify(textArea.cursorPosition === 'some\n@foo:example.com '.length);
}
function test_cursorInMiddleSpacingAfter() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
textArea.cursorPosition = 4;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some @foo:example.com\ntest');
verify(textArea.cursorPosition === 'some @foo:example.com\n'.length);
}
function test_cursorInMiddleSpacingAround() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\n\ntest';
textArea.cursorPosition = 5;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some\n@foo:example.com\ntest');
verify(textArea.cursorPosition === 'some\n@foo:example.com\n'.length);
}
function test_filter() {
const data = [
['some\n\ntest', 5, ''],
['some\n\n@test\n', 11, '@test'],
['some\n\n@test\n', 10, '@tes'],
['some\n\n@test喵 ', 12, '@test喵'],
// separated by a whitespace
['some\n\n@test\n', 12, ''],
['some\n\n@test\t', 12, ''],
['some\n\n@test ', 12, ''],
// multiple @s
['@test@233', 9, '@233'],
];
const textArea = findChild(sendMessageBox, 'draftMessage');
data.forEach(([text, pos, expected]) => {
textArea.text = text;
textArea.cursorPosition = pos;
compare(textArea.filter, expected);
});
}
function test_completion() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = '@ex';
textArea.cursorPosition = 3;
const completionPopup = findChild(sendMessageBox, 'completionPopup');
tryVerify(() => completionPopup.opened);
const listView = findChild(completionPopup, 'completionListView');
tryVerify(() => listView);
tryVerify(() => findChild(listView, 'completionItem0'));
tryVerify(() => findChild(listView, 'completionItem1'));
const first = findChild(listView, 'completionItem0');
const second = findChild(listView, 'completionItem1');
verify(findChild(first, 'userIdLabel').text === '@foo:example.com');
verify(findChild(second, 'userIdLabel').text === '@bar:example.com');
mouseClick(second);
tryVerify(() => !completionPopup.opened);
const expected = '@bar:example.com ';
tryCompare(textArea, 'text', expected);
compare(textArea.cursorPosition, expected.length);
}
function test_zeroCompletion() {
membersModel.count = 0;
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = '@ex';
textArea.cursorPosition = 3;
const completionPopup = findChild(sendMessageBox, 'completionPopup');
tryVerify(() => !completionPopup.opened);
}
function test_emptySend() {
mouseClick(findChild(sendMessageBox, 'sendButton'));
verify(room.sendTextMessage.calledTimes() === 0);
const textArea = findChild(sendMessageBox, 'draftMessage');
mouseClick(textArea);
tryVerify(() => textArea.activeFocus);
keySequence('Ctrl+Enter');
verify(room.sendTextMessage.calledTimes() === 0);
keySequence('Ctrl+Return');
verify(room.sendTextMessage.calledTimes() === 0);
}
function test_replyWithFile() {
sendMessageBox.draftRelType = 'm.in_reply_to';
sendMessageBox.draftRelatedTo = '!some-event-id';
const sendFileButton = findChild(sendMessageBox, 'sendMediaFileButton');
const fileDialog = findChild(sendMessageBox, 'fileDialog');
verify(sendFileButton.enabled);
verify(!fileDialog.visible);
mouseClick(sendFileButton);
verify(fileDialog.visible);
fileDialog.accept();
verify(!fileDialog.visible);
compare(kazvIOManager.startNewUploadJob.calledTimes(), 1);
verify(JsHelpers.deepEqual(kazvIOManager.startNewUploadJob.lastArgs(), {
- serverUrl: matrixSdk.serverUrl,
+ serverUrl: matrixSession.serverUrl,
fileUrl: Qt.url(''),
- token: matrixSdk.token,
+ token: matrixSession.token,
roomId: room.roomId,
roomList: sdkVars.roomList,
encrypted: room.encrypted,
draftRelType: 'm.in_reply_to',
draftRelatedTo: '!some-event-id'
}));
}
}
}
diff --git a/src/tests/quick-tests/tst_UserPage.qml b/src/tests/quick-tests/tst_UserPage.qml
index 64dedf2..b34fa65 100644
--- a/src/tests/quick-tests/tst_UserPage.qml
+++ b/src/tests/quick-tests/tst_UserPage.qml
@@ -1,402 +1,402 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import './test-helpers' as QmlHelpers
import 'test-helpers.js' as JsHelpers
import moe.kazv.mxc.kazv 0.0 as MK
QmlHelpers.TestItem {
id: upper
readonly property var gnMap: upper.sdkVars.userGivenNicknameMap
property var powerLevelMapping: ({
'@mew:example.com': 100,
})
property var room: QtObject {
signal onPowerLevelsChanged()
function userPowerLevel(userId) {
return powerLevelMapping[userId];
}
property var membership: MK.MatrixRoom.Join
property var setUserPowerLevel: mockHelper.promise()
property var ensureStateEvent: mockHelper.promise()
property var kickUser: mockHelper.promise()
property var banUser: mockHelper.promise()
property var unbanUser: mockHelper.promise()
property var setSelfName: mockHelper.promise()
}
property var userPage: null
property var userPageComp: Component {
Kazv.UserPage {
userId: '@mew:example.com'
user: ({ userId: '@mew:example.com', name: 'mew' })
room: upper.room
}
}
property var userPageWithoutName: null
property var userPageWithoutNameComp: Component {
Kazv.UserPage {
userId: '@mew:example.com'
user: ({ userId: '@mew:example.com', name: '' })
}
}
ColumnLayout {
id: layout
}
TestCase {
id: userPageTest
name: 'UserPageTest'
when: windowShown
function init() {
- upper.matrixSdk.userId = '@foo:example.com';
+ upper.matrixSession.userId = '@foo:example.com';
gnMap.map = {};
mockHelper.clearAll();
room.membership = MK.MatrixRoom.Join;
userPage = userPageComp.createObject(layout);
userPageWithoutName = userPageWithoutNameComp.createObject(layout);
layout.children = [userPage, userPageWithoutName];
if (MK.KazvUtil.kfQtMajorVersion === 6) {
userPage.contentItem.clip = false;
}
}
function cleanup() {
layout.children = [];
userPage.destroy();
userPage = null;
}
function test_powerLevel() {
verify(findChild(userPage, 'powerLevelLabel').visible);
verify(findChild(userPage, 'powerLevelLabel').text.includes('100'));
verify(!findChild(userPage, 'newPowerLevelInput').visible);
const editButton = findChild(userPage, 'editPowerLevelButton');
verify(editButton.visible);
verify(!editButton.enabled);
tryVerify(() => userPage.room.ensureStateEvent.calledTimes() === 2);
tryVerify(() => userPage.room.ensureStateEvent.args[1][0] === 'm.room.power_levels', 1000);
tryVerify(() => userPage.room.ensureStateEvent.retVals[1], 1000);
userPage.room.ensureStateEvent.retVals[1].resolve(true, {});
tryVerify(() => userPage.powerLevelsLoaded);
tryVerify(() => editButton.enabled);
tryVerify(() => editButton.visible);
mouseClick(editButton);
const textField = findChild(userPage, 'newPowerLevelInput');
tryVerify(() => textField.visible);
textField.text = '50';
const saveButton = findChild(userPage, 'savePowerLevelButton');
verify(saveButton.visible);
mouseClick(saveButton);
tryVerify(() => room.setUserPowerLevel.calledTimes(), 1000);
tryVerify(() => room.setUserPowerLevel.lastRetVal(), 1000);
// Text field is not closed until the request succeeded
verify(textField.visible);
verify(textField.readOnly);
verify(room.setUserPowerLevel.lastArgs()[0] === '@mew:example.com');
verify(room.setUserPowerLevel.lastArgs()[1] === 50);
// the buttons are disabled until the request responded
verify(!saveButton.enabled);
verify(!findChild(userPage, 'discardPowerLevelButton').enabled);
room.setUserPowerLevel.lastRetVal().resolve(true, {});
tryVerify(() => !textField.visible, 1000);
// Text field is editable when we try to edit the power level again
mouseClick(editButton);
tryVerify(() => textField.visible, 1000);
verify(!textField.readOnly);
}
function test_powerLevelEditFailure() {
tryVerify(() => userPage.room.ensureStateEvent.retVals[1], 1000);
userPage.room.ensureStateEvent.retVals[1].resolve(true, {});
const editButton = findChild(userPage, 'editPowerLevelButton');
tryVerify(() => editButton.enabled);
mouseClick(editButton);
const textField = findChild(userPage, 'newPowerLevelInput');
tryVerify(() => textField.visible, 1000);
textField.text = '50';
const saveButton = findChild(userPage, 'savePowerLevelButton');
mouseClick(saveButton);
tryVerify(() => room.setUserPowerLevel.lastRetVal(), 1000);
// Text field is not closed until the request succeeded
room.setUserPowerLevel.lastRetVal().resolve(false, {});
// text field is no longer readonly if failed
tryVerify(() => !textField.readOnly, 1000);
// show error message
verify(upper.showPassiveNotification.calledTimes());
// the buttons are enabled when the request responded
verify(saveButton.enabled);
verify(findChild(userPage, 'discardPowerLevelButton').enabled);
}
function test_banUser() {
// Ban user is only available if membership ! = "ban"
userPage.user = ({ membership: "join", userId: '@mew:example.com' });
const banButton = findChild(userPage, 'banUserButton');
verify(banButton.visible);
verify(banButton.enabled);
const reasonDialog = findChild(userPage, 'banUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'banUserReasonInput');
mouseClick(banButton);
verify(!reasonDialog.opened);
verify(textField.visible);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !banButton.enabled, 1000);
tryVerify(() => room.banUser.lastRetVal(), 1000);
verify(room.banUser.lastArgs()[0] === '@mew:example.com');
verify(room.banUser.lastArgs()[1] === 'some reason');
room.banUser.lastRetVal().resolve(true, {});
tryVerify(() => textField.text === '', 1000);
tryVerify(() => banButton.enabled, 1000);
}
function test_banUserFailure() {
// Ban user is only available if membership ! = "ban"
userPage.user = ({ membership: "join" });
const banButton = findChild(userPage, 'banUserButton');
verify(banButton.visible);
verify(banButton.enabled);
const reasonDialog = findChild(userPage, 'banUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'banUserReasonInput');
mouseClick(banButton);
verify(!reasonDialog.opened);
verify(textField.visible);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !banButton.enabled, 1000);
tryVerify(() => room.banUser.lastRetVal(), 1000);
verify(room.banUser.lastArgs()[0] === '@mew:example.com');
verify(room.banUser.lastArgs()[1] === 'some reason');
room.banUser.lastRetVal().resolve(false, {});
tryVerify(() => upper.showPassiveNotification.calledTimes(), 1000);
// text field is not cleared when failed
tryVerify(() => textField.text === 'some reason', 1000);
tryVerify(() => banButton.enabled, 1000);
}
function test_unbanUser() {
// Unban user is only available if membership == "ban"
userPage.user = ({ membership: "ban" });
const unbanButton = findChild(userPage, 'unbanUserButton');
verify(unbanButton.visible);
verify(unbanButton.enabled);
mouseClick(unbanButton);
tryVerify(() => !unbanButton.enabled, 1000);
verify(room.unbanUser.lastArgs()[0] == '@mew:example.com');
verify(room.unbanUser.lastRetVal());
room.unbanUser.lastRetVal().resolve(true, {});
tryVerify(() => unbanButton.enabled, 1000);
}
function test_unbanUserFailure() {
// Unban user is only available if membership == "ban"
userPage.user = ({ membership: "ban" });
const unbanButton = findChild(userPage, 'unbanUserButton');
verify(unbanButton.visible);
verify(unbanButton.enabled);
mouseClick(unbanButton);
tryVerify(() => !unbanButton.enabled, 1000);
verify(room.unbanUser.lastArgs()[0] == '@mew:example.com');
verify(room.unbanUser.lastRetVal());
room.unbanUser.lastRetVal().resolve(false, {});
tryVerify(() => upper.showPassiveNotification.calledTimes(), 1000);
tryVerify(() => unbanButton.enabled, 1000);
}
function test_kickUser() {
const kickButton = findChild(userPage, 'kickUserButton');
verify(kickButton.visible);
verify(kickButton.enabled);
const reasonDialog = findChild(userPage, 'kickUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'kickUserReasonInput');
mouseClick(kickButton);
verify(!reasonDialog.opened);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !kickButton.enabled, 1000);
tryVerify(() => room.kickUser.lastRetVal(), 1000);
verify(room.kickUser.lastArgs()[0] === '@mew:example.com');
verify(room.kickUser.lastArgs()[1] === 'some reason');
room.kickUser.lastRetVal().resolve(true, {});
tryVerify(() => textField.text === '', 1000);
// still available only because this is a mock call, no real action
// is performed on the user
tryVerify(() => kickButton.enabled, 1000);
}
function test_overridedName() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
gnMap.map = { '@mew:example.com': 'something' };
tryCompare(input, 'text', 'something');
}
function test_noOverridedNameForSelf() {
userPage.userId = '@foo:example.com';
verify(!findChild(userPage, 'nameOverrideInput').visible);
}
function test_updateOverridedName() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
verify(input.enabled);
input.text = 'something';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', 'something']));
gnMap.setAndUpload.lastRetVal().resolve(true, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
function test_updateOverridedNameFailed() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
verify(input.enabled);
input.text = 'something';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', 'something']));
gnMap.setAndUpload.lastRetVal().resolve(false, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
function test_removeOverridedName() {
gnMap.map = { '@mew:example.com': 'something' };
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, 'something');
input.text = '';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', null]));
gnMap.setAndUpload.lastRetVal().resolve(true, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
function test_showUserNameAndId() {
const nameLabel = findChild(userPage, 'userNameLabel');
const idLabel = findChild(userPage, 'userIdLabel');
verify(nameLabel.text === 'mew');
verify(idLabel.text === '@mew:example.com');
verify(idLabel.visible);
}
function test_showUserIdOnly() {
const nameLabel = findChild(userPageWithoutName, 'userNameLabel');
const idLabel = findChild(userPageWithoutName, 'userIdLabel');
verify(nameLabel.text === '@mew:example.com');
verify(!idLabel.visible);
}
function test_nonSelfName() {
const selfNameInput = findChild(userPage, 'selfNameInput');
verify(!selfNameInput.visible);
}
function test_nonJoinedRoomSelfName() {
userPage.userId = '@foo:example.com';
userPage.user.userId = '@foo:example.com';
upper.room.membership = MK.MatrixRoom.Invite;
const selfNameInput = findChild(userPage, 'selfNameInput');
verify(!selfNameInput.visible);
}
function test_selfName() {
userPage.userId = '@foo:example.com';
userPage.user.userId = '@foo:example.com';
const selfNameInput = findChild(userPage, 'selfNameInput');
verify(selfNameInput.visible);
verify(selfNameInput.text === 'mew');
selfNameInput.text = 'some others';
const button = findChild(userPage, 'saveSelfNameButton');
verify(button.enabled);
mouseClick(button);
tryVerify(() => room.setSelfName.calledTimes() === 1);
verify(!button.enabled);
verify(!selfNameInput.enabled);
verify(room.setSelfName.lastArgs()[0] === 'some others');
room.setSelfName.lastRetVal().resolve(/* succ = */ true, {});
tryVerify(() => button.enabled);
verify(selfNameInput.enabled);
}
function test_updateSelfNameFailed() {
userPage.userId = '@foo:example.com';
userPage.user.userId = '@foo:example.com';
const selfNameInput = findChild(userPage, 'selfNameInput');
selfNameInput.text = 'some others';
const button = findChild(userPage, 'saveSelfNameButton');
verify(button.enabled);
mouseClick(button);
tryVerify(() => room.setSelfName.calledTimes() === 1);
room.setSelfName.lastRetVal().resolve(/* succ = */ false, {});
tryVerify(() => button.enabled);
verify(selfNameInput.enabled);
verify(showPassiveNotification.calledTimes() === 1);
}
}
}
diff --git a/src/tests/test-model.cpp b/src/tests/test-model.cpp
index d4f5c58..1a986e5 100644
--- a/src/tests/test-model.cpp
+++ b/src/tests/test-model.cpp
@@ -1,82 +1,77 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "test-model.hpp"
#include "matrix-sdk.hpp"
using namespace Kazv;
auto deviceKeyInfoFor(std::string deviceId)
{
return DeviceKeyInfo{
deviceId,
deviceId + "ed25519",
deviceId + "curve25519",
deviceId + "name",
};
}
SdkModel makeTestModel()
{
SdkModel model;
model.client.serverUrl = "example.com";
model.client.userId = "@bob:example.com";
model.client.token = "exampletoken";
model.client.deviceId = "exampledevice";
model.client.loggedIn = true;
model.client.deviceLists.deviceLists = {
{"@test1:test1.org", {
{"device1", deviceKeyInfoFor("device1")},
{"device2", deviceKeyInfoFor("device2")},
}},
};
RoomModel room;
room.roomId = "!foo:tusooa.xyz";
auto event = Event{json{
{"content", {{"foo", "bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Sending},
{"some-other-txn-id", event, LocalEchoDesc::Failed},
};
model.client.roomList.rooms = model.client.roomList.rooms.set(
"!foo:tusooa.xyz",
room
);
return model;
}
-MatrixSdk *makeTestSdk(SdkModel model)
-{
- return new MatrixSdk(model, /* testing = */ true);
-}
-
Event makeRoomMember(std::string userId, std::string displayName, std::string avatarUrl)
{
auto content = json::object({{"membership", "join"}});
if (!displayName.empty()) {
content["displayname"] = displayName;
}
if (!avatarUrl.empty()) {
content["avatar_url"] = avatarUrl;
}
auto eventId = "$user-ev" + std::to_string(std::hash<std::string>()(userId));
return Event{json{
{"state_key", userId},
{"type", "m.room.member"},
{"content", content},
{"event_id", eventId},
{"room_id", "$dummy:tusooa.xyz"},
}};
}
diff --git a/src/tests/test-model.hpp b/src/tests/test-model.hpp
index 61eeac9..f9f9cc1 100644
--- a/src/tests/test-model.hpp
+++ b/src/tests/test-model.hpp
@@ -1,18 +1,58 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
-
+#include <db-store.hpp>
+#include <matrix-session.hpp>
+#include <matrix-session-controller-p.hpp>
+#include <qt-rand-adapter.hpp>
+#include <qt-job-handler.hpp>
#include <sdk-model.hpp>
class MatrixSdk;
Kazv::SdkModel makeTestModel();
-MatrixSdk *makeTestSdk(Kazv::SdkModel model);
+struct SessionSetup
+{
+ RandomInterface randomGenerator;
+ QObject obj;
+ // QObject objSecondary;
+ QtJobHandler jobHandler;
+ LagerStoreEventEmitter ee;
+ MatrixSessionSdkT sdk;
+ lager::state<VerificationTrackerModel> verificationTrackerState;
+ // SecondaryRootT secondaryRoot;
+ DbStore dbStore;
+ MatrixSession session;
+
+ inline SessionSetup(SdkModel m)
+ : randomGenerator(QtRandAdapter{})
+ , obj()
+ // , objSecondary()
+ , jobHandler(&obj)
+ , ee{lager::with_qt_event_loop{obj}}
+ , sdk(makeSdk(
+ m,
+ jobHandler,
+ ee,
+ QtPromiseHandler(obj),
+ zug::identity,
+ withRandomGenerator(randomGenerator)
+ ))
+ // , secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{this}, std::move(m)))
+ , session(
+ sdk.context(),
+ sdk.client(),
+ ee.watchable(),
+ verificationTrackerState,
+ [this]() -> DbStore & { return dbStore; }
+ )
+ {}
+};
Kazv::Event makeRoomMember(std::string userId, std::string displayName = "", std::string avatarUrl = "");

File Metadata

Mime Type
text/x-diff
Expires
Sun, Oct 11, 1:51 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785843
Default Alt Text
(515 KB)

Event Timeline