Page MenuHomePhorge

No OneTemporary

Size
174 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/CreateRoomPage.qml b/src/contents/ui/CreateRoomPage.qml
index a6c0a05..f4c264a 100644
--- a/src/contents/ui/CreateRoomPage.qml
+++ b/src/contents/ui/CreateRoomPage.qml
@@ -1,190 +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.ClosableScrollablePage {
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(
/* 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
: typePrivate.checked
? MK.MatrixSdk.PrivateChat
: MK.MatrixSdk.PublicChat,
/* encrypted = */ encrypted.checked,
)
onResolved: {
if (success) {
+ if (typeDirect.checked && createRoomPage.inviteUserIds.length === 1) {
+ matrixSdk.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/EventView.qml b/src/contents/ui/EventView.qml
index f0b1989..5930a55 100644
--- a/src/contents/ui/EventView.qml
+++ b/src/contents/ui/EventView.qml
@@ -1,279 +1,280 @@
/*
* 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 Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.13 as Kirigami
import 'event-types' as Types
Item {
id: eventView
property var event
property var prevEvent
property var prevEventType: prevEvent ? getMessageType(prevEvent) : 'ignore'
property var sender
property var stateKeyUser
property var isSelected: false
property var displayTime
property var messageType: getMessageType(event)
property var iconSize: Kirigami.Units.iconSizes.large
property var inlineBadgeSize: Kirigami.Units.iconSizes.smallMedium
property var senderNameSize: Kirigami.Units.gridUnit * 1.2
property var isGapped: false
property var compactMode: false
property var calculatedHeight: (loader.item ? loader.item.implicitHeight : 0)
height: calculatedHeight
implicitHeight: calculatedHeight
Layout.minimumHeight: calculatedHeight
Layout.preferredHeight: calculatedHeight
Component {
id: textEV
Types.Text {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: emoteEV
Types.Emote {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: noticeEV
Types.Notice {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: stateEV
Types.State {
event: eventView.event
sender: eventView.sender
stateKeyUser: eventView.stateKeyUser
isSelected: eventView.isSelected
}
}
Component {
id: imageEV
Types.Image {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: fileEV
Types.File {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: videoEV
Types.Video {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: audioEV
Types.Audio {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: unknownEV
Types.Fallback {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: redactedEV
Types.Redacted {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: reactionEV
Types.Reaction {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: ignoreEV
Item {
}
}
Loader {
sourceComponent: getSource(messageType)
id: loader
+ objectName: 'loader'
anchors.left: eventView.left
anchors.right: eventView.right
anchors.top: eventView.top
}
function getSource(t) {
switch (t) {
case 'text':
return textEV;
case 'emote':
return emoteEV;
case 'notice':
return noticeEV;
case 'state':
return stateEV;
case 'image':
return imageEV;
case 'file':
return fileEV;
case 'video':
return videoEV;
case 'audio':
return audioEV;
case 'redacted':
return redactedEV;
case 'reaction':
return reactionEV;
case 'ignore':
return ignoreEV;
default:
return unknownEV;
}
}
function getMessageType(e) {
if (e.redacted) {
return 'redacted';
}
if (e.isState) {
return 'state';
}
// ignore edits of other events
if (e.relationType === 'm.replace') {
return 'ignore';
}
switch (e.type) {
case 'm.room.message':
switch (e.content.msgtype) {
case 'm.text':
return 'text';
case 'm.emote':
return 'emote';
case 'm.notice':
return 'notice';
case 'm.image':
return 'image';
case 'm.file':
return 'file';
case 'm.audio':
return 'audio';
case 'm.video':
return 'video';
case 'm.location':
return 'location';
default:
return 'unknown';
}
case 'm.sticker':
return 'image';
case 'm.room.redaction':
return 'ignore';
case 'm.reaction':
// Only display a reaction if it is a failed local echo
// (otherwise it will be stuck forever and we have no way to delete it)
return e.isFailed ? 'reaction' : 'ignore';
default:
return 'unknown';
}
}
function paginateBack() {
paginateBackRequested(event.eventId);
}
function reactWith(reactionText) {
room.sendReaction(reactionText, event.eventId);
}
onIsGappedChanged: maybePaginateBack()
onEventChanged: maybePaginateBack()
Component.onCompleted: maybePaginateBack()
Connections {
target: eventView.event
function onEventIdChanged() {
eventView.maybePaginateBack();
}
}
function maybePaginateBack() {
if (isGapped) {
paginateBack();
}
}
}
diff --git a/src/contents/ui/JoinRoomPage.qml b/src/contents/ui/JoinRoomPage.qml
index 5a1c9ca..a96f1c6 100644
--- a/src/contents/ui/JoinRoomPage.qml
+++ b/src/contents/ui/JoinRoomPage.qml
@@ -1,75 +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.ClosableScrollablePage {
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)
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/RoomPage.qml b/src/contents/ui/RoomPage.qml
index 0aeae8e..5fbd242 100644
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -1,268 +1,283 @@
/*
* 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 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
signal mentionUserRequested(string userId)
signal replaceDraftRequested(string newDraft)
signal paginateBackRequested(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 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) {
sendMessageBox.draftRelType = relType;
sendMessageBox.draftRelatedTo = eventId;
}
property var joinRoomHandler: Kazv.AsyncHandler {
trigger: () => matrixSdk.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) {
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);
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');
}
}
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/event-types/TextTemplate.qml b/src/contents/ui/event-types/TextTemplate.qml
index 07be1b0..ef8e9ab 100644
--- a/src/contents/ui/event-types/TextTemplate.qml
+++ b/src/contents/ui/event-types/TextTemplate.qml
@@ -1,115 +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])
+
+ 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) {
- Qt.openUrlExternally(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)) {
+ 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/kazv-util.cpp b/src/kazv-util.cpp
index 2ca9382..26e2dc8 100644
--- a/src/kazv-util.cpp
+++ b/src/kazv-util.cpp
@@ -1,39 +1,44 @@
/*
* 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 <QUrl>
#include <QMimeDatabase>
#include "matrix-link.hpp"
#include "kazv-util.hpp"
KazvUtil::KazvUtil(QObject *parent)
: QObject(parent)
, m_kfQtMajorVersion(KAZV_KF_QT_MAJOR_VERSION)
{}
KazvUtil::~KazvUtil() = default;
QString KazvUtil::escapeHtml(const QString &orig) const
{
return orig.toHtmlEscaped();
}
QString KazvUtil::mimeTypeForUrl(const QUrl &url) const
{
return QMimeDatabase().mimeTypeForUrl(url).name();
}
QString KazvUtil::matrixLinkUserId(const QString &url) const
{
MatrixLink link = QUrl(url);
if (link.isUser()) {
return link.identifiers()[0];
}
return QStringLiteral("");
}
+
+MatrixLink *KazvUtil::matrixLink(const QString &url) const
+{
+ return new MatrixLink(QUrl(url));
+}
diff --git a/src/kazv-util.hpp b/src/kazv-util.hpp
index 732dcf4..4df9bf8 100644
--- a/src/kazv-util.hpp
+++ b/src/kazv-util.hpp
@@ -1,36 +1,39 @@
/*
* 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 "matrix-link.hpp"
+
#include <QObject>
#include <QString>
#include <QQmlEngine>
class KazvUtil : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(int kfQtMajorVersion CONSTANT MEMBER m_kfQtMajorVersion)
public:
KazvUtil(QObject *parent = 0);
~KazvUtil() override;
public Q_SLOTS:
QString escapeHtml(const QString &orig) const;
QString mimeTypeForUrl(const QUrl &url) const;
/// @return The user id for a given matrix link, or the empty string
/// if @c url is not a matrix link pointing to a user
QString matrixLinkUserId(const QString &url) const;
+ MatrixLink *matrixLink(const QString &url) const;
private:
int m_kfQtMajorVersion;
};
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
index 9c8aea5..971b1cc 100644
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -1,422 +1,424 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title = { -kt-app-name }
about-page-title = 关于 { -kt-app-name }
about-copyright = (c) 2020- the Kazv Project
about-display-name = { -kt-app-name } { $version }
about-short-description = 各平台同一的 Matrix 客户端和即时通讯软件
about-license = 以 AGPL 3 或以后版本授权
about-used-libraries = 使用了的库
about-authors = 作者
about-author-email-action = 写电邮
about-author-website-action = 访问网站
about-author-task-maintainer = 维护者
about-author-task-developer = 开发者
user-name-with-id = { $name } ({ $userId })
user-name-overrided = { $overridedName } ({ $globalName })
global-drawer-title = { -kt-app-name }
global-drawer-action-switch-account = 切换账号
global-drawer-action-hard-logout = 登出
global-drawer-action-save-session = 保存当前会话
global-drawer-action-configure-shortcuts = 配置快捷键
global-drawer-action-settings = 设置
global-drawer-action-create-room = 创建房间
global-drawer-action-join-room = 加入房间
global-drawer-action-about = 关于 { -kt-app-name }
action-settings-page-title = 配置快捷键
action-settings-shortcut-prompt = 快捷键:{ $shortcut }
action-settings-shortcut-none = (无)
action-settings-shortcut-edit-action = 编辑
action-settings-shortcut-remove-action = 清除
action-settings-shortcut-conflict-modal-title = 冲突的快捷键
action-settings-shortcut-conflict = 快捷键 { $shortcut } 跟别的指令有冲突。<br>若要继续,别的指令的快捷键会被清除。<br><br>冲突的指令有:<br>{ $conflictingAction }
action-settings-shortcut-conflict-continue = 继续
action-settings-shortcut-conflict-cancel = 取消
empty-room-page-title = 没有选中房间
empty-room-page-description = 当前没有选中的房间。
login-page-title = 登录
login-page-userid-prompt = 用户 id:
login-page-userid-input-placeholder = 例如: @foo:example.org
login-page-password-prompt = 密码:
login-page-login-button = 登录
login-page-close-button = 关闭
login-page-existing-sessions-prompt = 从已有会话中选一个:
login-page-alternative-password-login-prompt = 或者用用户 id 和密码启动新会话:
login-page-restore-session-button = 恢复会话
login-page-request-failed-prompt = 登录失败。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
login-page-discover-failed-enter-prompt = 不能检测此用户所在的服务器,或者服务器不可用。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。请手动输入服务器链接。
login-page-server-url-placeholder = 例如: https://example.org
login-page-server-url-prompt = 服务器链接(可选):
session-load-failure-not-found = 找不到会话 { $sessionName }。
session-load-failure-format-unknown = 会话 { $sessionName } 包含不支持的格式。是由未来版本的 { -kt-app-name } 保存的吗?
session-load-failure-cannot-backup = 无法备份会话 { $sessionName }。
session-load-failure-lock-failed = 会话 { $sessionName } 正在被别的程序使用。
session-load-failure-cannot-open-file = 无法打开会话 { $sessionName } 的存档文件。
session-load-failure-deserialize-failed = 无法打开会话 { $sessionName }。是被损坏了或者是由未来版本的 { -kt-app-name } 保存的吗?
main-page-title = { -kt-app-name } - { $userId }
main-page-recent-tab-title = 最近
main-page-favourites-tab-title = 最爱
main-page-people-tab-title = 人们
main-page-rooms-tab-title = 房间
main-page-room-filter-prompt = 过滤房间...
room-list-view-room-item-title-name = { $name }
room-list-view-room-item-title-heroes = { $hero } { $otherNum ->
[0] { "" }
[1] 和 { $secondHero }
*[other] 和别的 { $otherNum } 个人
}
room-list-view-room-item-title-id = 未命名房间({ $roomId })
room-list-view-room-item-fav-action = 设为最爱
room-list-view-room-item-unread-indicator = 未读
room-list-view-room-item-unread-notification-count = { $count }
room-list-view-room-item-unread-notification-count-text = { $count } 条未读通知
room-tags-fav-action-notification = 把 { $name } 设为了最爱
room-tags-fav-action-notification-failed = 不能把 { $name } 设为最爱。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-unfav-action-notification = 把 { $name } 从最爱中移除了
room-tags-unfav-action-notification-failed = 不能把 { $name } 从最爱中移除。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-add-tag-action-notification = 把标签 { $tag } 添加到了 { $name }
room-tags-add-tag-action-notification-failed = 不能把标签 { $tag } 添加到 { $name }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-remove-tag-action-notification = 把标签 { $tag } 从 { $name } 移除了
room-tags-remove-tag-action-notification-failed = 不能把标签 { $tag } 从 { $name } 移除。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-list-view-room-item-more-action = 更多...
room-list-view-room-item-invited = (邀请)
room-list-view-room-item-left = (已离开)
room-list-view-room-item-tombstone = (已废弃)
room-settings-action = 房间设置...
room-settings-page-title = { $room } 的房间设置
room-settings-tags = 房间标签
room-settings-favourited = 设为最爱
room-settings-remove-tag = 移除标签
room-settings-add-tag = 添加标签
room-settings-members-action = 房间成员...
room-settings-banned-members-action = 被封禁的成员...
room-settings-enable-encryption-prompt-dialog-title = 启用加密
room-settings-enable-encryption-prompt-dialog-prompt = 一旦在本房间中启用了加密,就不能再禁用了。确定吗?
room-settings-enable-encryption-action = 启用加密
room-settings-encrypted = 本房间中的消息是端对端加密了的。
room-settings-not-encrypted = 本房间中的消息没有端对端加密。
room-settings-encryption-enabled-notification = 本房间中的加密已经启用。
room-settings-encryption-failed-to-enable-notification = 不能在本房间中启用加密。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-settings-name-missing = 这个房间没有名字。
room-settings-topic-missing = 这个房间没有话题。
room-settings-edit-name-action = 编辑房间名字
room-settings-edit-topic-action = 编辑话题
room-settings-save-name-action = 保存房间名字
room-settings-save-topic-action = 保存话题
room-settings-discard-name-action = 放弃房间名字
room-settings-discard-topic-action = 放弃话题
room-settings-set-name-failed-prompt = 不能设置房间名字。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-settings-set-topic-failed-prompt = 不能设置话题。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-action = 贴纸包...
room-sticker-packs-page-title = { $room } 中的贴纸包
room-sticker-packs-use-action = 使用贴纸包
room-sticker-packs-use-failed = 设置使用的贴纸包失败。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-page-add-action = 添加贴纸包
room-sticker-packs-page-add-popup-title = 添加贴纸包
room-sticker-packs-page-add-success = 已添加贴纸包。
room-sticker-packs-page-add-failed = 无法添加贴纸包。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-page-add-popup-name-prompt = 贴纸包名称:
room-sticker-packs-page-add-popup-state-key-prompt = 状态键:
room-member-list-page-title = { $room } 的成员
room-leave-action = 离开房间
room-leave-confirm-popup-title = 离开房间
room-leave-confirm-popup-message = 你确定要离开这个房间吗?
room-leave-confirm-popup-confirm-action = 离开
room-leave-confirm-popup-cancel-action = 留下
room-forget-action = 忘记房间
room-forget-confirm-popup-title = 忘记房间
room-forget-confirm-popup-message = 你确定要忘记这个房间吗?
room-forget-confirm-popup-confirm-action = 忘记
room-forget-confirm-popup-cancel-action = 取消
send-message-box-input-placeholder = 在此输入您的讯息...
send-message-box-send = 发送
send-message-box-send-file = 发送文件
send-message-box-reply-to = 回复给
send-message-box-edit = 编辑
send-message-box-remove-reply-to-action = 移除回复关系
send-message-box-remove-replace-action = 取消编辑
send-message-box-stickers = 发送贴纸...
send-message-box-stickers-popup-title = 发送贴纸
sticker-picker-user-stickers = 我的贴纸
sticker-picker-room-sticker-pack-name = {$room} 中的 {$stateKey}
sticker-picker-room-default-sticker-pack-name = {$room} 中的默认包
room-timeline-load-more-action = 加载更多
room-timeline-scroll-to-latest-action = 滚动到最新的消息
room-invite-accept-action = 接受邀请
room-invite-reject-action = 拒绝邀请
room-invite-popup-title = 被邀请了
room-invite-popup-text = 你被邀请到这个房间了。
room-invite-popup-text-with-inviter = 你被 { $inviterName } 邀请到这个房间了。
room-pinned-events-action = { $count } 条置顶消息...
room-pinned-events-page-title = { $room } 的置顶消息
## 状态事件
## 通用参数:
## gender = 发送者的性别(male/female/neutral)
## stateKeyUser = state key 用户的名字
## stateKeyUserGender = state key 用户的性别
member-state-joined-room = 加入了房间。
member-state-changed-name-and-avatar = 修改了名字和头像。
member-state-changed-name = 修改了名字。
member-state-changed-avatar = 修改了头像。
member-state-invited = 把 { $stateKeyUser } 邀请到了本房间。
member-state-left = 离开了房间。
member-state-kicked = 踢出了 { $stateKeyUser }。
member-state-banned = 封禁了 { $stateKeyUser }。
member-state-unbanned = 解封了 { $stateKeyUser }。
state-room-created = 创建了房间。
state-room-name-changed = 把房间名字改成了 { $newName }。
state-room-topic-changed = 把房间话题改成了 { $newTopic }。
state-room-avatar-changed = 修改了房间头像。
state-room-pinned-events-changed = 修改了房间的置顶讯息。
state-room-alias-changed = 修改了房间的别名。
state-room-join-rules-changed = 修改了房间的加入规则。
state-room-power-levels-changed = 修改了房间的权限。
state-room-encryption-activated = 对本房间启用了加密。
event-message-image-sent = 发送了图片「{ $body }」。
event-message-sticker-sent = 发送了贴纸「{ $body }」。
event-summary-image-sent = 发送了图片「{ $body }」。
event-summary-sticker-sent = 发送了贴纸「{ $body }」。
event-message-file-sent = 发送了文件「{ $body }」。
event-message-video-sent = 发送了视频「{ $body }」。
event-summary-video-sent = 发送了视频「{ $body }」。
event-message-audio-sent = 发送了音频「{ $body }」。
event-message-audio-play-audio = 播放音频
event-sending = 发送中...
event-send-failed = 发送失败
event-resend = 重试发送这个事件
event-deleted = (已删除)
event-delete = 删除
event-delete-failed = 删除事件出错。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
event-view-source = 查看源码...
event-view-history = 查看编辑历史...
event-source-popup-title = 事件源码
event-source-decrypted = 解密的事件源码
event-source-original = 原始的事件源码
event-history-popup-title = 事件编辑历史
event-reply-action = 回复
event-popup-action = 更多关于这个事件...
event-reacted-with = 回应了「{ $key }」
event-react-action = 回应...
event-react-popup-title = 回应一条消息
event-react-accept-action = 回应
event-react-cancel-action = 取消
event-react-with-prompt = 回应以:
event-react-button-description = 回应以 { $key }
event-edit-action = 编辑
event-encrypted = 这条消息已加密
event-msgtype-fallback-text = 未知的消息类型:{ $msgtype }
event-fallback-text = 未知事件:{ $type }
event-cannot-decrypt-text = (加密内容)
event-read-indicator-more = +{ $rest }
event-read-indicator-list-title = { $numUsers } 个用户已读
event-edited-indicator = (编辑过了)
event-pin-action = 在房间置顶
event-unpin-action = 从房间取消置顶
event-pin-confirmation-title = 在房间置顶
event-pin-confirmation = 确定要置顶这条消息吗?
event-pin-confirm-action = 置顶
event-pin-cancel-action = 不置顶
event-pin-success-prompt = 消息在房间置顶了。
event-pin-failed-prompt = 无法置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
event-unpin-confirmation-title = 从房间取消置顶
event-unpin-confirmation = 确定要取消置顶这条消息吗?
event-unpin-confirm-action = 取消置顶
event-unpin-cancel-action = 不取消置顶
event-unpin-success-prompt = 消息从房间取消置顶了。
event-unpin-failed-prompt = 无法取消置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
media-file-menu-option-view = 查看
media-file-menu-option-save-as = 保存为
media-file-menu-add-sticker-action = 添加到贴纸...
add-sticker-popup-title = 添加到贴纸
add-sticker-popup-pack-prompt = 添加到:
add-sticker-popup-short-code-prompt = 短代码:
add-sticker-popup-short-code-exists-warning = 贴纸包里已经有这个短代码了。上面的贴纸会覆盖已有的。
add-sticker-popup-add-sticker-button = 添加
add-sticker-popup-cancel-button = 取消
add-sticker-popup-failed-prompt = 无法添加贴纸。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
kazv-io-download-success-prompt = 下载成功
kazv-io-download-failure-prompt = 下载失败:{ $detail }
kazv-io-failure-detail-user-cancel = 用户已取消
kazv-io-failure-detail-network-error = 网络错误
kazv-io-failure-detail-open-file-error = 打开文件错误
kazv-io-failure-detail-write-file-error = 写入文件错误
kazv-io-failure-detail-hash-error = 哈息值校验失败
kazv-io-failure-detail-response-error = 接收到无效的响应
kazv-io-failure-detail-kazv-error = 未知错误,请将此报告为漏洞
kazv-io-upload-failure-prompt = 上传失败:{ $detail }
kazv-io-downloading-prompt = 正在下载:{ $fileName }
kazv-io-uploading-prompt = 正在上传:{ $fileName }
kazv-io-prompt-close = 好的
kazv-io-pause = 暂停
kazv-io-resume = 继续
kazv-io-cancel = 取消
kazv-io-progress = { $progress }/100
create-room-page-title = 创建房间
create-room-page-type-prompt = 房间类型:
create-room-page-type-public = 公开(每个人都可加入)
create-room-page-type-private = 私有(仅受邀请用户可加入)
create-room-page-type-direct = 私聊(与私有一样,但初始受邀请用户具有管理权限)
create-room-page-name-prompt = 房间名称(可选):
create-room-page-name-placeholder = 无名
create-room-page-alias-prompt = 房间别名(可选):
create-room-page-alias-placeholder = #foo:example.org
create-room-page-topic-prompt = 房间主题(可选):
create-room-page-topic-placeholder = 无题
create-room-page-allow-federate-prompt = 允许别的服务器上的用户加入
create-room-page-action-create-room = 创建房间
create-room-page-success-prompt = 房间已创建。
create-room-page-failed-prompt = 无法创建房间。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
create-room-invite-userids-prompt = 受邀请的用户的 Matrix id:
create-room-page-add-invite-prompt = 添加一个新的受邀请用户:
create-room-page-add-invite-placeholder = Matrix id,例如 @foo:example.org
create-room-page-action-add-invite = 添加
create-room-page-action-remove-invite = 移除
create-room-page-encrypted-prompt = 启用端对端加密
join-room-page-title = 加入房间
join-room-page-id-or-alias-prompt = 房间 id 或别名:
join-room-page-id-or-alias-placeholder = #foo:example.org 或 !abcdef:example.org
join-room-page-servers-prompt = 经由服务器(可选,用换行分割):
join-room-page-servers-placeholder =
example.org
example.com
join-room-page-action-join-room = 加入房间
join-room-page-success-prompt = 成功加入房间 { $room }。
join-room-page-failed-prompt = 无法加入房间 { $room }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
leave-room-success-prompt = 成功离开房间 { $room }。
leave-room-failed-prompt = 无法离开房间 { $room }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
forget-room-success-prompt = 成功忘记房间 { $room }。
forget-room-failed-prompt = 无法忘记房间 { $room }。错误代码: { $errorCode }。错误讯息: { $errorMsg }。
device-trust-level-unseen = 未曾见过
device-trust-level-seen = 见过
device-trust-level-verified = 已验证
device-trust-level-blocked = 已屏蔽
device-set-trust-level = 设置信任等级...
device-set-trust-level-dialog-title = 设置信任等级
device-set-trust-level-dialog-name-label = 设备名:{ $name }
device-set-trust-level-dialog-id-label = 设备id:{ $id }
device-set-trust-level-dialog-ed25519-key-label = Ed25519公钥:{ $key }
device-set-trust-level-dialog-curve25519-key-label = Curve25519公钥:{ $key }
device-set-trust-level-dialog-save = 保存
device-set-trust-level-dialog-cancel = 取消
settings-page-title = 设置
settings-save = 保存设置
settings-profile-load-failed-prompt = 无法加载用户资料。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
settings-profile-change-avatar = 改变头像...
settings-profile-display-name = 显示名:
settings-profile-save-failed-prompt = 无法保存用户资料。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
settings-cache-directory = 缓存目录:
typing-indicator = { $typingUser } { $otherNum ->
[0] 正在输入...
[1] 和 { $secondTypingUser } 正在输入...
*[other] 和另外 { $otherNum } 人正在输入...
}
notification-message = <b>{ $user }:</b> { $message }
notification-message-no-content = <b>{ $user }</b> 给你发了一条讯息。
notification-open = 打开
user-page-power-level = 权限等级:{ $powerLevel }
user-page-edit-power-level-action = 编辑
user-page-save-power-level-action = 保存
user-page-discard-power-level-action = 丢弃
user-page-set-power-level-failed-prompt = 无法设置权限等级。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
user-page-kick-user-action = 踢出
user-page-kick-user-confirm-dialog-title = 踢出用户
user-page-kick-user-confirm-dialog-content = 确定要把 { $name }({ $userId })踢出 { $roomName } 吗?
user-page-kick-user-reason-prompt = 原因(可选):
user-page-kick-user-failed-prompt = 无法踢出用户。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
user-page-ban-user-action = 封禁
user-page-ban-user-confirm-dialog-title = 封禁用户
user-page-ban-user-confirm-dialog-content = 确定要在 { $roomName } 中封禁 { $name }({ $userId })吗?
user-page-ban-user-reason-prompt = 原因(可选):
user-page-ban-user-failed-prompt = 无法封禁用户。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-unban-user-action = 解禁
user-page-unban-user-failed-prompt = 无法解禁用户。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-overrided-name-placeholder = 自定义显示名...
user-page-save-name-override-action = 保存
user-page-update-name-override-failed-prompt = 无法设置自定义显示名。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-self-name-prompt = 自己在房间里的显示名:
user-page-self-name-placeholder = 自己在房间里的显示名...
user-page-save-self-name-action = 保存
user-page-update-self-name-failed-prompt = 无法设置自己在房间里的显示名。错误码:{ $error }。错误讯息:{ $errorMsg }。
room-invite-page-title = 邀请用户到 { $room }
room-invite-page-invite-failed-prompt = 无法邀请用户。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-invite-invitee-matrix-id-placeholder = 用户的 Matrix id,比如 @foo:example.org
room-invite-invitee-matrix-id-prompt = 要邀请的用户:
room-invite-page-invite-button = 邀请
room-invite-action = 邀请到房间...
room-explore-state-action = 查看完整房间状态...
room-state-page-title = { $room } 的状态
room-state-page-state-key = 状态键:「<code>{ $stateKey }</code>」
confirm-upload-popup-title = 确认上传
confirm-upload-popup-prompt = 即将上传「{ $file }」({ $current } / { $total })。
confirm-upload-popup-prompt-single-file = 即将上传「{ $file }」。
confirm-upload-popup-accept-button = 上传
confirm-upload-popup-cancel-button = 取消
action-cut = 剪切
action-copy = 复制
action-paste = 粘贴
action-undo = 撤销
action-redo = 重做
action-delete = 删除
action-select-all = 全选
logout-failed-prompt = 登出失败,请检查您的网络后重试。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
confirm-logout-popup-title = 确认登出
confirm-logout-popup-prompt = 你确定要登出吗?
confirm-logout-popup-accept-button = 是
confirm-logout-popup-cancel-button = 取消
emoji-smileys-and-emotion = 笑容与情绪
emoji-people-and-body = 人与身体
emoji-animals-and-nature = 动物与自然
emoji-food-and-drink = 食物与饮料
emoji-travel-and-places = 旅行与地点
emoji-activities = 活动
emoji-objects = 物体
emoji-symbols = 符号
emoji-flags = 旗帜
confirm-deletion-popup-title = 删除事件
confirm-deletion-popup-message = 你确定要删除这个事件吗?
confirm-deletion-popup-confirm-action = 删除
confirm-deletion-popup-cancel-action = 取消
+
+get-room-id-by-alias-failed-prompt = 无法通过房间别名获取房间 id。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
\ No newline at end of file
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
index 6ab57c4..157806c 100644
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -1,444 +1,446 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title = { -kt-app-name }
about-page-title = About { -kt-app-name }
about-copyright = (c) 2020- the Kazv Project
about-display-name = { -kt-app-name } { $version }
about-short-description = Convergent Matrix client and instant messaging app
about-license = licensed under AGPL 3 or later
about-used-libraries = Used libraries
about-authors = Authors
about-author-email-action = Write an email
about-author-website-action = Access website
about-author-task-maintainer = Maintainer
about-author-task-developer = Developer
user-name-with-id = { $name } ({ $userId })
user-name-overrided = { $overridedName } ({ $globalName })
global-drawer-title = { -kt-app-name }
global-drawer-action-switch-account = Switch account
global-drawer-action-hard-logout = Logout
global-drawer-action-save-session = Save current session
global-drawer-action-configure-shortcuts = Configure shortcuts
global-drawer-action-settings = Settings
global-drawer-action-create-room = Create room
global-drawer-action-join-room = Join room
global-drawer-action-about = About { -kt-app-name }
action-settings-page-title = Configure shortcuts
action-settings-shortcut-prompt = Shortcut: { $shortcut }
action-settings-shortcut-none = (none)
action-settings-shortcut-edit-action = Edit
action-settings-shortcut-remove-action = Clear
action-settings-shortcut-conflict-modal-title = Conflicting shortcuts
action-settings-shortcut-conflict = The shortcut { $shortcut } has conflicts with other actions. <br>If you continue, the shortcuts for other actions will be cleared. <br><br>Conflicting actions: <br>{ $conflictingAction }
action-settings-shortcut-conflict-continue = Continue
action-settings-shortcut-conflict-cancel = Cancel
empty-room-page-title = No rooms selected
empty-room-page-description = There is no room selected now.
login-page-title = Log in
login-page-userid-prompt = User id:
login-page-userid-input-placeholder = E.g.: @foo:example.org
login-page-password-prompt = Password:
login-page-login-button = Log in
login-page-close-button = Close
login-page-existing-sessions-prompt = Choose from one of the existing sessions:
login-page-alternative-password-login-prompt = Or start a new session with user id and password:
login-page-restore-session-button = Restore session
login-page-request-failed-prompt = Login failed. Error code: { $errorCode }. Error message: { $errorMsg }.
login-page-discover-failed-enter-prompt = Unable to detect the server this user is on, or the server is unavailable. Error code: { $errorCode }. Error message: { $errorMsg }. Please enter the server url manually.
login-page-server-url-placeholder = E.g.: https://example.org
login-page-server-url-prompt = Server url (optional):
session-load-failure-not-found = The session { $sessionName } is not found.
session-load-failure-format-unknown = The session { $sessionName } contains an unsupported format. Is it saved using a future version of { -kt-app-name }?
session-load-failure-cannot-backup = Cannot make a backup of the session { $sessionName }.
session-load-failure-lock-failed = The session { $sessionName } is being used by another program.
session-load-failure-cannot-open-file = Unable to open the store file for the session { $sessionName }.
session-load-failure-deserialize-failed = The session { $sessionName } cannot be opened. Is it corrupted or saved using a future version of { -kt-app-name }?
main-page-title = { -kt-app-name } - { $userId }
main-page-recent-tab-title = Recent
main-page-favourites-tab-title = Favourites
main-page-people-tab-title = People
main-page-rooms-tab-title = Rooms
main-page-room-filter-prompt = Filter rooms by...
room-list-view-room-item-title-name = { $name }
room-list-view-room-item-title-heroes = { $hero } { $otherNum ->
[0] { "" }
[1] and { $secondHero }
*[other] and { $otherNum } others
}
room-list-view-room-item-title-id = Unnamed room ({ $roomId })
room-list-view-room-item-fav-action = Set as favourite
room-list-view-room-item-unread-indicator = Unread
room-list-view-room-item-unread-notification-count = { $count }
room-list-view-room-item-unread-notification-count-text = { $count } unread {
$count ->
[1] message
*[other] messages
}
room-tags-fav-action-notification = Set { $name } as favourite
room-tags-fav-action-notification-failed = Cannot set { $name } as favourite. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-unfav-action-notification = Removed { $name } from favourites
room-tags-unfav-action-notification-failed = Cannot remove { $name } from favourites. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-add-tag-action-notification = Added tag { $tag } to { $name }
room-tags-add-tag-action-notification-failed = Cannot add tag { $tag } to { $name }. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-remove-tag-action-notification = Removed tag { $tag } from { $name }
room-tags-remove-tag-action-notification-failed = Cannot remove tag { $tag } from { $name }. Error code: { $errorCode }. Error message: { $errorMsg }.
room-list-view-room-item-more-action = More...
room-list-view-room-item-invited = (Invited)
room-list-view-room-item-left = (Left)
room-list-view-room-item-tombstone = (Deprecated)
room-settings-action = Room settings...
room-settings-page-title = Room settings for { $room }
room-settings-tags = Room tags
room-settings-favourited = Set as favourite
room-settings-remove-tag = Remove tag
room-settings-add-tag = Add tag
room-settings-members-action = Room members...
room-settings-banned-members-action = Banned members...
room-settings-enable-encryption-prompt-dialog-title = Enabling encryption
room-settings-enable-encryption-prompt-dialog-prompt = Once you enable encryption in this room, you cannot disable it again. Are you sure?
room-settings-enable-encryption-action = Enable encryption
room-settings-encrypted = Messages in this room are end-to-end encrypted.
room-settings-not-encrypted = Messages in this room are not end-to-end-encrypted.
room-settings-encryption-enabled-notification = Encryption is now enabled in this room.
room-settings-encryption-failed-to-enable-notification = Cannot enable encryption in this room. Error code: { $errorCode }. Error message: { $errorMsg }.
room-settings-name-missing = This room dose not have a name.
room-settings-topic-missing = This room does not have a topic.
room-settings-edit-name-action = Edit name
room-settings-edit-topic-action = Edit topic
room-settings-save-name-action = Save name
room-settings-save-topic-action = Save topic
room-settings-discard-name-action = Discard name
room-settings-discard-topic-action = Discard topic
room-settings-set-name-failed-prompt = Cannot set name. Error code: { $errorCode }. Error message: { $errorMsg }.
room-settings-set-topic-failed-prompt = Cannot set topic. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-action = Sticker packs...
room-sticker-packs-page-title = Sticker packs in { $room }
room-sticker-packs-use-action = Use sticker pack
room-sticker-packs-use-failed = Failed to set sticker packs in use. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-page-add-action = Add sticker pack
room-sticker-packs-page-add-popup-title = Add sticker pack
room-sticker-packs-page-add-success = Sticker pack added.
room-sticker-packs-page-add-failed = Cannot add sticker pack. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-page-add-popup-name-prompt = Sticker pack name:
room-sticker-packs-page-add-popup-state-key-prompt = State key:
room-member-list-page-title = Members of { $room }
room-leave-action = Leave room
room-leave-confirm-popup-title = Leaving room
room-leave-confirm-popup-message = Are you sure you want to leave this room?
room-leave-confirm-popup-confirm-action = Leave
room-leave-confirm-popup-cancel-action = Stay
room-forget-action = Forget room
room-forget-confirm-popup-title = Forgetting room
room-forget-confirm-popup-message = Are you sure you want to forget this room?
room-forget-confirm-popup-confirm-action = Forget
room-forget-confirm-popup-cancel-action = Cancel
send-message-box-input-placeholder = Type your message here...
send-message-box-send = Send
send-message-box-send-file = Send file
send-message-box-reply-to = Replying to
send-message-box-edit = Editing
send-message-box-remove-reply-to-action = Remove reply-to relationship
send-message-box-remove-replace-action = Cancel editing
send-message-box-stickers = Send a sticker...
send-message-box-stickers-popup-title = Send a sticker
sticker-picker-user-stickers = My stickers
sticker-picker-room-sticker-pack-name = {$stateKey} in {$room}
sticker-picker-room-default-sticker-pack-name = Default pack in {$room}
room-timeline-load-more-action = Load more
room-timeline-scroll-to-latest-action = Scroll to latest message
room-invite-accept-action = Accept invite
room-invite-reject-action = Reject invite
room-invite-popup-title = Invited
room-invite-popup-text = You are invited to join this room.
room-invite-popup-text-with-inviter = You are invited to join this room by { $inviterName }.
room-pinned-events-action = { $count } pinned { $count ->
[1] message
*[other] messages
}...
room-pinned-events-page-title = Pinned messages of { $room }
## State events
## Common parameters:
## gender = gender of the sender (male/female/neutral)
## stateKeyUser = name of the state key user
## stateKeyUserGender = gender of the state key user
member-state-joined-room = joined the room.
member-state-changed-name-and-avatar = changed { $gender ->
[male] his
[female] her
*[neutral] their
} name and avatar.
member-state-changed-name = changed { $gender ->
[male] his
[female] her
*[neutral] their
} name.
member-state-changed-avatar = changed { $gender ->
[male] his
[female] her
*[neutral] their
} avatar.
member-state-invited = invited { $stateKeyUser } to the room.
member-state-left = left the room.
member-state-kicked = kicked { $stateKeyUser }.
member-state-banned = banned { $stateKeyUser }.
member-state-unbanned = unbanned { $stateKeyUser }.
state-room-created = created the room.
state-room-name-changed = changed the name of the room to { $newName }.
state-room-topic-changed = changed the topic of the room to { $newTopic }.
state-room-avatar-changed = changed the avatar of the room.
state-room-pinned-events-changed = changed the pinned events of the room.
state-room-alias-changed = changed the aliases of the room.
state-room-join-rules-changed = changed join rules of the room.
state-room-power-levels-changed = changed power levels of the room.
state-room-encryption-activated = enabled encryption for this room.
event-message-image-sent = sent an image "{ $body }".
event-message-sticker-sent = sent a sticker "{ $body }".
event-summary-image-sent = sent an image "{ $body }".
event-summary-sticker-sent = sent a sticker "{ $body }".
event-message-file-sent = sent a file "{ $body }".
event-message-video-sent = sent a video "{ $body }".
event-summary-video-sent = sent a video "{ $body }".
event-message-audio-sent = sent an audio "{ $body }".
event-message-audio-play-audio = Play audio
event-sending = Sending...
event-send-failed = Failed to send
event-resend = Retry sending this event
event-deleted = (Deleted)
event-delete = Delete
event-delete-failed = Error deleting event. Error code: { $errorCode }. Error message: { $errorMsg }.
event-view-source = View source...
event-view-history = View edit history...
event-source-popup-title = Event source
event-source-decrypted = Decrypted event source
event-source-original = Original event source
event-history-popup-title = Event edit history
event-reply-action = Reply
event-popup-action = More about this event...
event-reacted-with = Reacted with "{ $key }"
event-react-action = React...
event-react-popup-title = React to a message
event-react-accept-action = React
event-react-cancel-action = Cancel
event-react-with-prompt = React with:
event-react-button-description = React with { $key }
event-edit-action = Edit
event-encrypted = This message is encrypteed
event-msgtype-fallback-text = Unknown message type: { $msgtype }
event-fallback-text = Unknown event: { $type }
event-cannot-decrypt-text = (Encrypted content)
event-read-indicator-more = +{ $rest }
event-read-indicator-list-title = Read by { $numUsers ->
[1] 1 user
*[other] { $numUsers } users
}
event-edited-indicator = (edited)
event-pin-action = Pin to room
event-unpin-action = Unpin from room
event-pin-confirmation-title = Pin to room
event-pin-confirmation = Are you sure you want to pin this message?
event-pin-confirm-action = Pin
event-pin-cancel-action = Do not pin
event-pin-success-prompt = Message pinned to room.
event-pin-failed-prompt = Unable to pin message. Error code: { $errorCode }. Error message: { $errorMsg }.
event-unpin-confirmation-title = Unpin from room
event-unpin-confirmation = Are you sure you want to unpin this message?
event-unpin-confirm-action = Unpin
event-unpin-cancel-action = Do not unpin
event-unpin-success-prompt = Message unpinned from room.
event-unpin-failed-prompt = Unable to unpin message. Error code: { $errorCode }. Error message: { $errorMsg }.
media-file-menu-option-view = View
media-file-menu-option-save-as = Save as
media-file-menu-add-sticker-action = Add to sticker...
add-sticker-popup-title = Add to sticker
add-sticker-popup-pack-prompt = Add to pack:
add-sticker-popup-short-code-prompt = Short code:
add-sticker-popup-short-code-exists-warning = The short code already exists in this pack. The sticker above will override the existing one.
add-sticker-popup-add-sticker-button = Add
add-sticker-popup-cancel-button = Cancel
add-sticker-popup-failed-prompt = Unable to add sticker. Error code: { $errorCode }. Error message: { $errorMsg }.
kazv-io-download-success-prompt = Download successful
kazv-io-download-failure-prompt = Download failure: { $detail }
kazv-io-failure-detail-user-cancel = User canceled
kazv-io-failure-detail-network-error = Network error
kazv-io-failure-detail-open-file-error = Open file error
kazv-io-failure-detail-write-file-error = Write file error
kazv-io-failure-detail-hash-error = Hash check error
kazv-io-failure-detail-response-error = Get an invalid response
kazv-io-failure-detail-kazv-error = Unknow Error, please report this as bug.
kazv-io-upload-failure-prompt = Upload failure: { $detail }
kazv-io-downloading-prompt = Downloading: { $fileName }
kazv-io-uploading-prompt = Uploading: { $fileName }
kazv-io-prompt-close = Got it
kazv-io-pause = Pause
kazv-io-resume = Resume
kazv-io-cancel = Cancel
kazv-io-progress = { $progress }/100
create-room-page-title = Create room
create-room-page-type-prompt = Room type:
create-room-page-type-public = Public (everyone can join)
create-room-page-type-private = Private (only invited users can join)
create-room-page-type-direct = Direct message (same as private, but those initially invited get admin permissions)
create-room-page-name-prompt = Room name (optional):
create-room-page-name-placeholder = No name
create-room-page-alias-prompt = Room alias (optional):
create-room-page-alias-placeholder = #foo:example.org
create-room-page-topic-prompt = Room topic (optional):
create-room-page-topic-placeholder = No topic
create-room-page-allow-federate-prompt = Allow users from other servers to join
create-room-page-action-create-room = Create room
create-room-page-success-prompt = Room created.
create-room-page-failed-prompt = Unable to create room. Error code: { $errorCode }. Error message: { $errorMsg }.
create-room-invite-userids-prompt = Matrix ids of users to invite:
create-room-page-add-invite-prompt = Add a new user to invite:
create-room-page-add-invite-placeholder = Matrix id, e.g. @foo:example.org
create-room-page-action-add-invite = Add
create-room-page-action-remove-invite = Remove
create-room-page-encrypted-prompt = Enable end-to-end encryption
join-room-page-title = Join room
join-room-page-id-or-alias-prompt = Room id or alias:
join-room-page-id-or-alias-placeholder = #foo:example.org or !abcdef:example.org
join-room-page-servers-prompt = Via servers (optional, separated by newlines):
join-room-page-servers-placeholder =
example.org
example.com
join-room-page-action-join-room = Join room
join-room-page-success-prompt = Successfully joined room { $room }.
join-room-page-failed-prompt = Unable to join room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
leave-room-success-prompt = Successfully left room { $room }.
leave-room-failed-prompt = Unable to leave room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
forget-room-success-prompt = Successfully forgot room { $room }.
forget-room-failed-prompt = Unable to forget room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
device-trust-level-unseen = Unseen
device-trust-level-seen = Seen
device-trust-level-verified = Verified
device-trust-level-blocked = Blocked
device-set-trust-level = Set trust level...
device-set-trust-level-dialog-title = Set trust level
device-set-trust-level-dialog-name-label = Device name: { $name }
device-set-trust-level-dialog-id-label = Device id: { $id }
device-set-trust-level-dialog-ed25519-key-label = Ed25519 public key: { $key }
device-set-trust-level-dialog-curve25519-key-label = Curve25519 public key: { $key }
device-set-trust-level-dialog-save = Save
device-set-trust-level-dialog-cancel = Cancel
settings-page-title = Settings
settings-save = Save settings
settings-profile-load-failed-prompt = Unable to load profile. Error code: { $errorCode }. Error message: { $errorMsg }.
settings-profile-change-avatar = Change avatar...
settings-profile-display-name = Display name:
settings-profile-save-failed-prompt = Unable to save profile. Error code: { $errorCode }. Error message: { $errorMsg }.
settings-cache-directory = Cache directory:
typing-indicator = { $typingUser } { $otherNum ->
[0] is typing...
[1] and { $secondTypingUser } are typing...
*[other] and { $otherNum } others are typing...
}
notification-message = <b>{ $user }:</b> { $message }
notification-message-no-content = <b>{ $user }</b> sent you a message.
notification-open = Open
user-page-power-level = Power level: { $powerLevel }
user-page-edit-power-level-action = Edit
user-page-save-power-level-action = Save
user-page-discard-power-level-action = Discard
user-page-set-power-level-failed-prompt = Unable to set power level. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-kick-user-action = Kick
user-page-kick-user-confirm-dialog-title = Kicking user
user-page-kick-user-confirm-dialog-content = Are you sure you want to kick { $name } ({ $userId }) out of { $roomName }?
user-page-kick-user-reason-prompt = Reason (optional):
user-page-kick-user-failed-prompt = Unable to kick user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-ban-user-action = Ban
user-page-ban-user-confirm-dialog-title = Banning user
user-page-ban-user-confirm-dialog-content = Are you sure you want to ban { $name } ({ $userId }) in { $roomName }?
user-page-ban-user-reason-prompt = Reason (optional):
user-page-ban-user-failed-prompt = Unable to ban user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-unban-user-action = Unban
user-page-unban-user-failed-prompt = Unable to unban user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-overrided-name-placeholder = Custom display name...
user-page-save-name-override-action = Save
user-page-update-name-override-failed-prompt = Unable to set custom display name. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-self-name-prompt = Own display name in room:
user-page-self-name-placeholder = Own display name in room...
user-page-save-self-name-action = Save
user-page-update-self-name-failed-prompt = Unable to set own display name in room. Error code: { $errorCode }. Error message: { $errorMsg }.
room-invite-page-title = Inviting user to { $room }
room-invite-page-invite-failed-prompt = Unable to invite user. Error code: { $errorCode }. Error message: { $errorMsg }.
room-invite-invitee-matrix-id-placeholder = Matrix id of the user, e.g. @foo:example.org
room-invite-invitee-matrix-id-prompt = User to invite:
room-invite-page-invite-button = Invite
room-invite-action = Invite to room...
room-explore-state-action = View full room state...
room-state-page-title = Room state of { $room }
room-state-page-state-key = State key: "<code>{ $stateKey }</code>"
confirm-upload-popup-title = Confirm upload
confirm-upload-popup-prompt = You are about to upload "{ $file }" ({ $current } / { $total }).
confirm-upload-popup-prompt-single-file = You are about to upload "{ $file }".
confirm-upload-popup-accept-button = Upload
confirm-upload-popup-cancel-button = Cancel
action-cut = Cut
action-copy = Copy
action-paste = Paste
action-undo = Undo
action-redo = Redo
action-delete = Delete
action-select-all = Select All
logout-failed-prompt = Logout failed. Please check your network and try again. Error code: { $errorCode }, error message: { $errorMsg }.
confirm-logout-popup-title = Confirm logout
confirm-logout-popup-prompt = Are you sure to logout?
confirm-logout-popup-accept-button = Yes
confirm-logout-popup-cancel-button = Cancel
emoji-smileys-and-emotion = Smiley && Emotion
emoji-people-and-body = People && Body
emoji-animals-and-nature = Animals && Nature
emoji-food-and-drink = Food && Drink
emoji-travel-and-places = Travel && Places
emoji-activities = Activities
emoji-objects = Objects
emoji-symbols = Symbols
emoji-flags = Flags
confirm-deletion-popup-title = Delete event
confirm-deletion-popup-message = Are you sure you want to delete this event?
confirm-deletion-popup-confirm-action = Delete
confirm-deletion-popup-cancel-action = Cancel
+
+get-room-id-by-alias-failed-prompt = Unable to get room id by alias. Error code: { $errorCode }. Error message: { $errorMsg }.
\ No newline at end of file
diff --git a/src/matrix-link.cpp b/src/matrix-link.cpp
index ad02ec4..5f6d57e 100644
--- a/src/matrix-link.cpp
+++ b/src/matrix-link.cpp
@@ -1,149 +1,154 @@
/*
* 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 <QUrl>
#include <QUrlQuery>
#include <QDebug>
#include <QString>
#include "matrix-link.hpp"
using namespace Qt::Literals::StringLiterals;
std::pair<MatrixLink::Type, QString> parseMatrixToComp(QString comp)
{
comp = QUrl(u"foo:#"_s + comp).fragment(QUrl::FullyDecoded);
if (comp.startsWith(u'@')) {
return {MatrixLink::User, comp};
} else if (comp.startsWith(u'#')) {
return {MatrixLink::RoomAlias, comp};
} else if (comp.startsWith(u'!')) {
return {MatrixLink::RoomId, comp};
} else if (comp.startsWith(u'$')) {
return {MatrixLink::Event, comp};
} else {
return {MatrixLink::None, comp};
}
}
QStringList parseMatrixToQuery(QString query)
{
auto q = QUrlQuery(query);
return q.allQueryItemValues(u"via"_s);
}
std::pair<MatrixLink::Type, QString> parseMatrixSchemeComp(
const QString &type, const QString &id) {
if (type == u"u"_s) {
return {MatrixLink::User, u"@"_s + id};
} else if (type == u"r"_s) {
return {MatrixLink::RoomAlias, u"#"_s + id};
} else if (type == u"roomid"_s) {
return {MatrixLink::RoomId, u"!"_s + id};
} else if (type == u"e"_s) {
return {MatrixLink::Event, u"$"_s + id};
} else {
return {MatrixLink::None, id};
}
}
QString parseMatrixSchemeAction(QString query)
{
auto q = QUrlQuery{query};
auto actions = q.allQueryItemValues(u"action"_s);
if (actions.isEmpty()) {
return u""_s;
} else {
return actions.last();
}
}
MatrixLink::MatrixLink(const QUrl &url)
{
// matrix.to link
if (url.scheme() == u"https"_s
&& url.host() == u"matrix.to"_s) {
auto frag = url.fragment();
auto parts = frag.split(u'?');
QString ids = parts[0];
QString query = parts.size() > 1 ? parts[1] : u""_s;
auto comps = ids.split(u'/');
if (comps.empty() || !comps[0].isEmpty()) {
return;
}
comps.pop_front();
for (auto comp : comps) {
m_identifiers.push_back(parseMatrixToComp(comp));
}
m_routingServers = parseMatrixToQuery(query);
} else if (url.scheme() == u"matrix"_s) { // Matrix URI scheme
auto path = url.path();
auto query = url.query();
auto segments = path.split(u'/');
if (segments.size() != 2 && segments.size() != 4) {
return;
}
m_identifiers.push_back(
parseMatrixSchemeComp(segments[0], segments[1]));
if (segments.size() == 4) {
m_identifiers.push_back(
parseMatrixSchemeComp(segments[2], segments[3]));
}
m_routingServers = parseMatrixToQuery(query);
m_action = parseMatrixSchemeAction(query);
}
}
bool MatrixLink::isValid() const
{
return !m_identifiers.empty()
&& m_identifiers[0].first != None
&& m_identifiers[0].first != Event
&& (m_identifiers.size() == 1 || (
m_identifiers[1].first == Event
&& (m_identifiers[0].first == RoomId
|| m_identifiers[0].first == RoomAlias)));
}
bool MatrixLink::isUser() const
{
return isValid() && m_identifiers[0].first == User;
}
bool MatrixLink::isRoom() const
{
return isValid() && (
m_identifiers[0].first == RoomId
|| m_identifiers[0].first == RoomAlias);
}
+bool MatrixLink::isRoomAlias() const
+{
+ return isValid() && (m_identifiers[0].first == RoomAlias);
+}
+
bool MatrixLink::isEvent() const
{
return isValid() && m_identifiers.size() == 2;
}
QStringList MatrixLink::identifiers() const
{
QStringList ret;
for (auto [_, id] : m_identifiers) {
ret.push_back(id);
}
return ret;
}
QStringList MatrixLink::routingServers() const
{
return m_routingServers;
}
QString MatrixLink::action() const
{
return m_action;
}
diff --git a/src/matrix-link.hpp b/src/matrix-link.hpp
index 544d6b2..b4fbde5 100644
--- a/src/matrix-link.hpp
+++ b/src/matrix-link.hpp
@@ -1,52 +1,56 @@
/*
* 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 <tuple>
#include <vector>
#include <QObject>
#include <QStringList>
class QUrl;
/**
* A link that represents a Matrix object.
*/
-class MatrixLink
+class MatrixLink : public QObject
{
- Q_GADGET
+ Q_OBJECT
+ Q_PROPERTY(bool isValid READ isValid CONSTANT)
Q_PROPERTY(bool isUser READ isUser CONSTANT)
Q_PROPERTY(bool isRoom READ isRoom CONSTANT)
+ Q_PROPERTY(bool isRoomAlias READ isRoomAlias CONSTANT)
Q_PROPERTY(bool isEvent READ isEvent CONSTANT)
Q_PROPERTY(QStringList identifiers READ identifiers CONSTANT)
Q_PROPERTY(QStringList routingServers READ routingServers CONSTANT)
+ Q_PROPERTY(QString action READ action CONSTANT)
public:
enum Type {
None,
User,
RoomId,
RoomAlias,
Event,
};
MatrixLink(const QUrl &url);
bool isValid() const;
bool isUser() const;
bool isRoom() const;
+ bool isRoomAlias() const;
bool isEvent() const;
QStringList identifiers() const;
QStringList routingServers() const;
QString action() const;
private:
std::vector<std::pair<Type, QString>> m_identifiers;
QStringList m_routingServers;
QString m_action;
};
diff --git a/src/matrix-room-list.cpp b/src/matrix-room-list.cpp
index ebb2535..92f6d63 100644
--- a/src/matrix-room-list.cpp
+++ b/src/matrix-room-list.cpp
@@ -1,194 +1,199 @@
/*
* 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 <zug/into_vector.hpp>
#include <zug/transducer/cat.hpp>
#include <lager/commit.hpp>
#include <lager/constant.hpp>
#include <lager/lenses/optional.hpp>
#include "matrix-room-list.hpp"
#include "matrix-room.hpp"
#include "matrix-user-given-attrs-map.hpp"
#include "matrix-utils.hpp"
#include "helper.hpp"
using namespace Kazv;
static Timestamp latestEventTimestamp(const RoomModel &room)
{
if (room.timeline.empty()) {
return 0;
}
auto latestEventId = room.timeline[room.timeline.size() - 1];
auto latestEvent = room.messages[latestEventId];
return latestEvent.originServerTs();
}
MatrixRoomList::MatrixRoomList(Kazv::Client client, QString tagId, QString filter, QObject *parent)
: KazvAbstractListModel(parent)
, m_client(client)
, m_tagId(tagId)
, m_tagIdCursor(lager::make_sensor([this] { return m_tagId.toStdString(); }))
, m_filter(lager::make_state(filter.toStdString(), lager::automatic_tag{}))
, m_userGivenNicknameMap(userGivenNicknameMapFor(m_client).map(&Event::content))
, m_roomIds(lager::with(m_tagIdCursor, m_client.rooms(), m_filter, m_client.roomIdsByTagId(), m_userGivenNicknameMap)
.map([](const auto &tagIdStdStr, const auto &allRooms, const auto &filter, const auto &roomsByTagMap, const auto &userGivenNicknameMap) {
auto toId = zug::map([](const auto &pair) {
return pair.first;
});
auto roomName = [](const auto &room) {
auto content = room.stateEvents[{"m.room.name", ""}].content().get();
if (content.contains("name") && content["name"].is_string()) {
return content["name"].template get<std::string>();
}
return std::string();
};
auto roomHeroNames = [m=userGivenNicknameMap](const auto &room) {
auto heroEvents = room.heroMemberEvents();
return intoImmer(
immer::flex_vector<std::string>(),
zug::map([m](const Event &ev) {
auto userId = ev.stateKey();
auto content = ev.content().get();
auto res = std::vector<std::string>{};
if (content.contains("displayname") && content["displayname"].is_string()) {
res.push_back(content["displayname"].template get<std::string>());
}
if (m.get().contains(userId)
&& m.get()[userId].is_string()) {
res.push_back(m.get()[userId].template get<std::string>());
}
return res;
})
| zug::cat,
heroEvents
);
};
auto applyFilter = zug::filter([&filter, &allRooms, &roomName, &roomHeroNames](const auto &id) {
if (filter.empty()) {
return true;
}
const auto &room = allRooms[id];
// Use exact match for room id
if (room.roomId == filter) {
return true;
}
auto name = roomName(room);
if (!name.empty()) {
// Use substring match for name search
return name.find(filter) != std::string::npos;
}
// The room has no name, use hero names for the search
auto heroes = roomHeroNames(room);
// If any of the room hero matches the filter, consider it a match
return std::any_of(heroes.begin(), heroes.end(),
[&filter](const auto &name) {
return name.find(filter) != std::string::npos;
})
|| std::any_of(room.heroIds.begin(), room.heroIds.end(),
[&filter](const auto &id) {
return id.find(filter) != std::string::npos;
});
});
auto sortByTimestampDesc = [allRooms](std::vector<std::string> container) {
std::sort(
container.begin(),
container.end(),
[allRooms](const std::string &idA, const std::string &idB) {
const auto &roomA = allRooms[idA];
const auto &roomB = allRooms[idB];
auto aIsInvite = roomA.membership == Invite;
auto bIsInvite = roomB.membership == Invite;
auto aIsFavourite = roomA.tags().count("m.favourite") == 1;
auto bIsFavourite = roomB.tags().count("m.favourite") == 1;
if (aIsInvite != bIsInvite) {
/**
* if my membership in A is invite,
* then the membership in B is not invite,
* so A should come first;
* otherwise B should come first
**/
return aIsInvite;
} else if (aIsFavourite != bIsFavourite) {
return aIsFavourite;
} else {
return latestEventTimestamp(roomA)
> latestEventTimestamp(roomB);
}
}
);
return immer::flex_vector<std::string>(container.begin(), container.end());
};
if (tagIdStdStr.empty()) {
return sortByTimestampDesc(zug::into_vector(
toId | applyFilter,
allRooms
));
} else {
return sortByTimestampDesc(zug::into_vector(
toId | applyFilter,
roomsByTagMap[tagIdStdStr]
));
}
}))
, LAGER_QT(filter)(m_filter.xform(strToQt, qStringToStd))
, LAGER_QT(roomIds)(m_roomIds.xform(zug::map(
[](auto container) {
return zug::into(QStringList{}, strToQt, std::move(container));
})))
{
initCountCursor(m_roomIds.xform(containerSize));
}
MatrixRoomList::~MatrixRoomList() = default;
void MatrixRoomList::setTagId(QString tagId)
{
m_tagId = tagId;
lager::commit(m_tagIdCursor);
}
MatrixRoom *MatrixRoomList::at(int index) const
{
qDebug() << "Room at index " << index << " requested";
return new MatrixRoom(
m_client.roomByCursor(
lager::with(m_roomIds, lager::make_constant(index))
.xform(zug::map([](auto ids, auto i) {
try {
return ids.at(i);
} catch (const std::out_of_range &) {
return std::string{};
}
}))),
m_client.userId());
}
QString MatrixRoomList::roomIdAt(int index) const
{
using namespace Kazv::CursorOp;
return +m_roomIds[index][lager::lenses::or_default].xform(strToQt);
}
MatrixRoom *MatrixRoomList::room(QString roomId) const
{
return new MatrixRoom(
m_client.room(roomId.toStdString()),
m_client.userId(),
userGivenNicknameMapFor(m_client)
);
}
+
+bool MatrixRoomList::contains(QString roomId) const
+{
+ return roomIds().contains(roomId);
+}
diff --git a/src/matrix-room-list.hpp b/src/matrix-room-list.hpp
index 4527570..310026f 100644
--- a/src/matrix-room-list.hpp
+++ b/src/matrix-room-list.hpp
@@ -1,48 +1,49 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <lager/sensor.hpp>
#include <lager/state.hpp>
#include <lager/extra/qt.hpp>
#include <client/client.hpp>
#include "kazv-abstract-list-model.hpp"
Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
class MatrixRoom;
class MatrixUserGivenAttrsMap;
class MatrixRoomList : public KazvAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Client m_client;
QString m_tagId;
lager::sensor<std::string> m_tagIdCursor;
lager::state<std::string, lager::automatic_tag> m_filter;
lager::reader<Kazv::JsonWrap> m_userGivenNicknameMap;
lager::reader<immer::flex_vector<std::string>> m_roomIds;
public:
explicit MatrixRoomList(Kazv::Client client, QString tagId = QString(), QString filter = QString(), QObject *parent = 0);
~MatrixRoomList() override;
Q_INVOKABLE void setTagId(QString tagId);
LAGER_QT_CURSOR(QString, filter);
LAGER_QT_READER(QStringList, roomIds);
Q_INVOKABLE MatrixRoom *at(int index) const;
Q_INVOKABLE QString roomIdAt(int index) const;
Q_INVOKABLE MatrixRoom *room(QString roomId) const;
+ Q_INVOKABLE bool contains(QString roomId) const;
};
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index 11303fc..ba5cd97 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,906 +1,936 @@
/*
* 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 <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 <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 "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 "kazv-session-lock-guard.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!"}; }
};
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 MatrixSdkPrivate
{
MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard);
MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard);
bool testing;
std::string userDataDir;
std::unique_ptr<KazvSessionLockGuard> lockGuard;
RandomInterface randomGenerator;
QThread *thread;
QObject *obj;
QtJobHandler *jobHandler;
LagerStoreEventEmitter ee;
LagerStoreEventEmitter::Watchable watchable;
SdkT sdk;
QTimer saveTimer;
using SecondaryRootT = decltype(sdk.createSecondaryRoot(std::declval<lager::with_qt_event_loop>()));
SecondaryRootT secondaryRoot;
Client clientOnSecondaryRoot;
NotificationHandler notificationHandler;
void runIoContext() {
thread->start();
}
void stopIoContext() {
thread->quit();
}
void maybeSerialize()
{
if (!testing) {
serializeClientToFile(clientOnSecondaryRoot);
}
}
void serializeClientToFile(Client c);
};
// 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";
}
};
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");
}
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard)
: testing(testing)
, userDataDir{kazvUserDataDir().toStdString()}
, lockGuard(std::move(lockGuard))
, randomGenerator(QtRandAdapter{})
, 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())
{
obj->moveToThread(thread);
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard)
: testing(testing)
, userDataDir{kazvUserDataDir().toStdString()}
, lockGuard(std::move(lockGuard))
, randomGenerator(QtRandAdapter{})
, 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())
{
obj->moveToThread(thread);
}
MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent)
: QObject(parent)
, m_userDataDir(kazvUserDataDir())
, m_d(std::move(d))
{
init();
connect(this, &MatrixSdk::trigger,
this, [](KazvEvent e) {
qDebug() << "receiving trigger:";
if (std::holds_alternative<LoginSuccessful>(e)) {
qDebug() << "Login successful";
}
});
}
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())
);
});
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>()), parent)
{
}
MatrixSdk::MatrixSdk(SdkModel model, bool testing, QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model), std::unique_ptr<KazvSessionLockGuard>()), 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;
+}
+
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::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)
{
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::make_unique<MatrixSdkPrivate>(this, testing, std::move(lockGuard)));
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 != QStringLiteral("text")) {
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";
} catch (const std::exception &e) {
qDebug() << "Error when loading session:" << QString::fromStdString(e.what());
Q_EMIT loadSessionFinished(sessionName, SessionDeserializeFailed);
return;
}
QMetaObject::invokeMethod(this, [this, model=std::move(model), lockGuard=std::move(lockGuard)]() mutable {
emplace(std::move(model), std::move(lockGuard));
});
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);
}
bool MatrixSdk::deleteSession(QString sessionName) {
using StdPath = std::filesystem::path;
qDebug() << "in deleteSession(), sessionName=" << sessionName;
auto userDataDir = StdPath(kazvUserDataDir().toStdString());
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>());
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
{
return m_d->randomGenerator;
}
bool MatrixSdk::shouldNotify(MatrixEvent *event) const
{
// Do not notify own event
if (event->sender() == userId()) {
return false;
}
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));
+ });
+ }));
+}
+
void MatrixSdk::setUserDataDir(const std::string &userDataDir)
{
m_d->userDataDir = userDataDir;
}
#include "matrix-sdk.moc"
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
index a2f2813..fa21d7a 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -1,318 +1,326 @@
/*
* 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 <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")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
class MatrixSdkTest;
class MatrixSdkSessionsTest;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
class KazvSessionLockGuard;
struct MatrixSdkPrivate;
std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId);
class MatrixSdk : public QObject
{
Q_OBJECT
QML_ELEMENT
QString m_userDataDir;
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;
+
Kazv::RandomInterface &randomGenerator() const;
private:
// Replaces the store with another one
void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard);
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 logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
void sessionChanged();
void loadSessionFinished(QString sessionName, MatrixSdk::LoadSessionResult result);
public Q_SLOTS:
void login(const QString &userId, const QString &password, 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);
+
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/tests/matrix-link-test.cpp b/src/tests/matrix-link-test.cpp
index 4adcaf9..768a71a 100644
--- a/src/tests/matrix-link-test.cpp
+++ b/src/tests/matrix-link-test.cpp
@@ -1,138 +1,156 @@
/*
* 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 <vector>
#include <QtTest>
#include "matrix-link.hpp"
using namespace Qt::Literals::StringLiterals;
class MatrixLinkTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testInvalidLink();
void testUserLink();
void testRoomLink();
+ void testRoomAliasLink();
void testEventLink();
void testRoutingServers();
void testAction();
};
void MatrixLinkTest::testInvalidLink()
{
std::vector<QUrl> links = {
QUrl(u"https://matrix.to/#"_s),
QUrl(u"https://matrix.to/#/"_s),
QUrl(u"https://matrix.to/#//"_s),
QUrl(u"https://matrix.to/#///"_s),
QUrl(u"http://matrix.to/#/%40alice%3Aexample.org"_s),
QUrl(u"https://lol.lol/#/%40alice%3Aexample.org"_s),
QUrl(u"https://matrix.to/#%40alice%3Aexample.org"_s),
QUrl(u"matrix:/"_s),
QUrl(u"matrix://"_s),
QUrl(u"matrix:///"_s),
QUrl(u"matrix:r"_s),
QUrl(u"matrix:r/somewhere:example.org/e"_s),
QUrl(u"matrix:r/somewhere:example.org/e/event/e"_s),
};
for (auto link : links) {
MatrixLink ml(link);
QVERIFY(!ml.isValid());
}
}
void MatrixLinkTest::testUserLink()
{
std::vector<QUrl> links = {
QUrl(u"https://matrix.to/#/%40alice%3Aexample.org"_s),
QUrl(u"https://matrix.to/#/@alice%3Aexample.org"_s),
QUrl(u"https://matrix.to/#/%40alice:example.org"_s),
QUrl(u"https://matrix.to/#/@alice:example.org"_s),
QUrl(u"matrix:u/alice:example.org"_s),
QUrl(u"matrix:u/alice%3Aexample.org"_s),
};
for (auto link : links) {
MatrixLink ml(link);
QVERIFY(ml.isUser());
QCOMPARE(ml.identifiers(), QStringList{u"@alice:example.org"_s});
}
}
void MatrixLinkTest::testRoomLink()
{
std::vector<std::tuple<QUrl, QString>> links = {
{QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/%23somewhere:example.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/#somewhere%3Aexample.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/#somewhere:example.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere%3Aexample.org?via=elsewhere.ca"_s), u"!somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere:example.org?via=elsewhere.ca"_s), u"!somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere%3Aexample.org"_s), u"!somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere:example.org"_s), u"!somewhere:example.org"_s},
{QUrl(u"matrix:r/somewhere:example.org"_s), u"#somewhere:example.org"_s},
{QUrl{u"matrix:r/somewhere%3Aexample.org"_s}, u"#somewhere:example.org"_s},
{QUrl(u"matrix:roomid/somewhere:example.org"_s), u"!somewhere:example.org"_s},
{QUrl(u"matrix:roomid/somewhere%3Aexample.org"_s), u"!somewhere:example.org"_s},
};
for (auto [link, pointed] : links) {
MatrixLink ml(link);
QVERIFY(ml.isRoom());
QCOMPARE(ml.identifiers(), QStringList{pointed});
}
}
+void MatrixLinkTest::testRoomAliasLink()
+{
+ std::vector<std::tuple<QUrl, QString>> links = {
+ {QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org"_s), u"#somewhere:example.org"_s},
+ {QUrl(u"https://matrix.to/#/%23somewhere:example.org"_s), u"#somewhere:example.org"_s},
+ {QUrl(u"https://matrix.to/#/#somewhere%3Aexample.org"_s), u"#somewhere:example.org"_s},
+ {QUrl(u"https://matrix.to/#/#somewhere:example.org"_s), u"#somewhere:example.org"_s},
+ {QUrl(u"matrix:r/somewhere:example.org"_s), u"#somewhere:example.org"_s},
+ {QUrl{u"matrix:r/somewhere%3Aexample.org"_s}, u"#somewhere:example.org"_s},
+ };
+ for (auto [link, pointed] : links) {
+ MatrixLink ml(link);
+ QVERIFY(ml.isRoomAlias());
+ QCOMPARE(ml.identifiers(), QStringList{pointed});
+ }
+}
+
void MatrixLinkTest::testEventLink()
{
std::vector<std::tuple<QUrl, QString>> links = {
{QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event%3Aexample.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event:example.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere%3Aexample.org/%24event%3Aexample.org?via=elsewhere.ca"_s), u"!somewhere:example.org"_s},
{QUrl(u"https://matrix.to/#/!somewhere%3Aexample.org/$event:example.org?via=elsewhere.ca"_s), u"!somewhere:example.org"_s},
{QUrl(u"matrix:r/somewhere:example.org/e/event:example.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"matrix:r/somewhere%3Aexample.org/e/event:example.org"_s), u"#somewhere:example.org"_s},
{QUrl(u"matrix:roomid/somewhere:example.org/e/event:example.org"_s), u"!somewhere:example.org"_s},
{QUrl(u"matrix:roomid/somewhere%3Aexample.org/e/event:example.org"_s), u"!somewhere:example.org"_s},
};
for (auto [link, pointed] : links) {
MatrixLink ml(link);
QVERIFY(ml.isRoom());
QCOMPARE(ml.identifiers(), (QStringList{pointed, u"$event:example.org"_s}));
}
}
void MatrixLinkTest::testRoutingServers()
{
QCOMPARE(MatrixLink(QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event:example.org"_s)).routingServers(), (QStringList{}));
QCOMPARE(MatrixLink(QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event:example.org?via=a.org"_s)).routingServers(), (QStringList{u"a.org"_s}));
QCOMPARE(MatrixLink(QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event:example.org?via=a.org&via=b.org"_s)).routingServers(), (QStringList{u"a.org"_s, u"b.org"_s}));
QCOMPARE(MatrixLink(QUrl(u"https://matrix.to/#/%23somewhere%3Aexample.org/%24event:example.org?foo=bar&via=a.org&via=b.org"_s)).routingServers(), (QStringList{u"a.org"_s, u"b.org"_s}));
QCOMPARE(MatrixLink(QUrl(u"matrix:r/somewhere:example.org/e/event:example.org"_s)).routingServers(), (QStringList{}));
QCOMPARE(MatrixLink(QUrl(u"matrix:r/somewhere:example.org/e/event:example.org?via=a.org"_s)).routingServers(), (QStringList{u"a.org"_s}));
QCOMPARE(MatrixLink(QUrl(u"matrix:r/somewhere:example.org/e/event:example.org?via=a.org&via=b.org"_s)).routingServers(), (QStringList{u"a.org"_s, u"b.org"_s}));
QCOMPARE(MatrixLink(QUrl(u"matrix:r/somewhere:example.org/e/event:example.org?foo=bar&via=a.org&via=b.org"_s)).routingServers(), (QStringList{u"a.org"_s, u"b.org"_s}));
}
void MatrixLinkTest::testAction()
{
QCOMPARE(MatrixLink(QUrl(u"matrix:u/alice:example.org?action=chat"_s)).action(), u"chat"_s);
QCOMPARE(MatrixLink(QUrl(u"matrix:r/somewhere:example.org?action=join&via=a.org"_s)).action(), u"join");
}
QTEST_MAIN(MatrixLinkTest)
#include "matrix-link-test.moc"
diff --git a/src/tests/matrix-room-list-test.cpp b/src/tests/matrix-room-list-test.cpp
index 9507b2c..05e77a1 100644
--- a/src/tests/matrix-room-list-test.cpp
+++ b/src/tests/matrix-room-list-test.cpp
@@ -1,235 +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());
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());
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());
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());
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());
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());
+ 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-sdk-test.cpp b/src/tests/matrix-sdk-test.cpp
index 73336ae..c38221e 100644
--- a/src/tests/matrix-sdk-test.cpp
+++ b/src/tests/matrix-sdk-test.cpp
@@ -1,113 +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 <matrix-device-list.hpp>
#include <matrix-device.hpp>
#include <matrix-promise.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
class MatrixSdkTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testDevicesOfUser();
void testSetDeviceTrustLevel();
void testValidateHomeserverUrl();
void testSpecVersion();
+ void testDirectRoomIds();
};
void MatrixSdkTest::testDevicesOfUser()
{
auto model = makeTestModel();
std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
auto devices = toUniquePtr(sdk->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()
{
auto model = makeTestModel();
std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
sdk->startThread();
auto promise = toUniquePtr(
sdk->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")));
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()
{
auto v = [](const QString &u) {
return QString::fromStdString(MatrixSdk::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()
{
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));
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));
}
+void MatrixSdkTest::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)};
+ const auto rooms = QStringList{u"!somewhere1:example.org"_s, u"!somewhere2:example.org"_s};
+ const auto directRoomIds = sdk->directRoomIds(u"@mew:example.org"_s);
+ QCOMPARE(QSet(directRoomIds.begin(), directRoomIds.end()),
+ QSet(rooms.begin(), rooms.end()));
+}
+
QTEST_MAIN(MatrixSdkTest)
#include "matrix-sdk-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
index 70e6460..25c6bf6 100644
--- a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
+++ b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
@@ -1,55 +1,74 @@
/*
* 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 addDirectRoom: mockHelper.promise([
+ 'userId',
+ 'roomId'
+ ])
+ property var getRoomIdByAlias: mockHelper.promise([
+ 'roomAlias'
+ ])
property var sessions: []
property var specVersions: ["v1.11"]
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/tst_CreateRoomPage.qml b/src/tests/quick-tests/tst_CreateRoomPage.qml
index 572feff..dcd7307 100644
--- a/src/tests/quick-tests/tst_CreateRoomPage.qml
+++ b/src/tests/quick-tests/tst_CreateRoomPage.qml
@@ -1,261 +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(), {
isPrivate: false,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
preset: MK.MatrixSdk.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(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
preset: MK.MatrixSdk.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(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: [],
isDirect: false,
allowFederate: true,
powerLevelContentOverride: {},
preset: MK.MatrixSdk.TrustedPrivateChat,
encrypted: true,
}));
+ tryVerify(() => item.matrixSdk.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(), {
isPrivate: true,
name: 'some name',
alias: 'alias',
topic: 'some topic',
invites: ['@foo:example.org'],
isDirect: true,
allowFederate: true,
powerLevelContentOverride: {},
preset: MK.MatrixSdk.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(), {
+ 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(), {
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,
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(), {
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,
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_MatrixLink.qml b/src/tests/quick-tests/tst_MatrixLink.qml
new file mode 100644
index 0000000..c19d888
--- /dev/null
+++ b/src/tests/quick-tests/tst_MatrixLink.qml
@@ -0,0 +1,218 @@
+/*
+ * 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(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(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(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(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(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.' }));
+ }
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 5:38 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785044
Default Alt Text
(174 KB)

Event Timeline