Page MenuHomePhorge

No OneTemporary

Size
94 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/event-types/Fallback.qml b/src/contents/ui/event-types/Fallback.qml
index 9165a55..3e907d5 100644
--- a/src/contents/ui/event-types/Fallback.qml
+++ b/src/contents/ui/event-types/Fallback.qml
@@ -1,42 +1,52 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
Types.TextTemplate {
id: upper
text: getText()
+ property var requestKeyAction: Kirigami.Action {
+ text: l10n.get('event-request-key-action')
+ onTriggered: matrixSession.requestShareRoomSessionKey(event)
+ }
+
+ property bool isUndecryptable: (
+ event.content.msgtype === 'moe.kazv.mxc.cannot.decrypt'
+ || event.content.msgtype === 'xyz.tusooa.kazv.not.yet.decrypted'
+ )
+ innerBubble.menuContent: isUndecryptable ? [requestKeyAction] : []
+
Kirigami.Icon {
objectName: 'fallbackIcon'
source: /* iconName: */ 'emblem-question'
Layout.preferredHeight: inlineBadgeSize
Layout.preferredWidth: inlineBadgeSize
}
function getText() {
if (event.type === 'm.room.message') {
- if (event.content.msgtype === 'moe.kazv.mxc.cannot.decrypt' ||
- event.content.msgtype === 'xyz.tusooa.kazv.not.yet.decrypted') {
+ if (isUndecryptable) {
return l10n.get('event-cannot-decrypt-text');
} else {
return l10n.get('event-msgtype-fallback-text', {
msgtype: event.content.msgtype,
});
}
} else {
return l10n.get('event-fallback-text', {
type: event.type,
});
}
}
}
diff --git a/src/contents/ui/event-types/TextTemplate.qml b/src/contents/ui/event-types/TextTemplate.qml
index 87852c1..7709d2c 100644
--- a/src/contents/ui/event-types/TextTemplate.qml
+++ b/src/contents/ui/event-types/TextTemplate.qml
@@ -1,203 +1,204 @@
/*
* 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 alias innerBubble: bubble
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: () => matrixSession.getRoomIdByAlias(link.identifiers[0])
onResolved: {
if (success) {
if (sdkVars.roomList.contains(data.roomId)) {
switchToRoomRequested(data.roomId);
} else {
joinRoom(link.identifiers[0], link.routingServers);
}
} else {
showPassiveNotification(l10n.get('get-room-id-by-alias-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function getMaybeFormattedText() {
if (isHtmlFormatted) {
const stylesheet = `
<style>
del {
text-decoration: line-through;
}
a[href^="https://matrix.to/#/@"] {
background-color: ${Kirigami.Theme.positiveBackgroundColor};
}
</style>
`;
const formattedBody = event.content.formatted_body;
if (event.replyingToEventId && formattedBody.startsWith('<mx-reply>')) {
const index = formattedBody.indexOf('</mx-reply>');
return stylesheet + formattedBody.slice(index + '</mx-reply>'.length);
} else {
return stylesheet + formattedBody;
}
} else {
return event.content.body;
}
}
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md#operations-on-matrix-uris
function openLink(link) {
const matrixLink = MK.KazvUtil.matrixLink(link);
// Not a valid matrix uri
if (!matrixLink.isValid) {
Qt.openUrlExternally(link);
return;
}
const action = matrixLink.action;
const servers = matrixLink.routingServers;
const joinRoom = (roomId, servers) => {
pushJoinRoomPage();
pageStack.currentItem.presetPage(roomId, servers);
};
// Handle link to a user
if (matrixLink.isUser) {
const userId = matrixLink.identifiers[0];
if (action !== 'chat') {
mentionUserRequested(userId);
return;
}
// If the room with the user already exists, switch to the room.
// Otherwise try to create the room.
for (const roomId of matrixSession.directRoomIds(userId)) {
const invitedOrJoined = (roomId) => {
const membership = sdkVars.roomList.room(roomId).membership;
return (membership == MK.MatrixRoom.Invite) || (membership == MK.MatrixRoom.Join);
};
if (sdkVars.roomList.contains(roomId) && invitedOrJoined(roomId)) {
switchToRoomRequested(roomId);
return;
}
}
pushCreateRoomPage();
pageStack.currentItem.presetPage(userId);
return;
}
// Handle link to a event
if (matrixLink.isEvent) {
const roomId = matrixLink.identifiers[0];
const eventId = matrixLink.identifiers[1];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
pageStack.currentItem.delayGoToEvent(eventId, 500);
} else {
joinRoom(roomId, servers);
}
return;
}
// Handle link to a room
if (matrixLink.isRoom) {
// If user has joined the room, open it.
// Otherwise try to join it.
if (matrixLink.isRoomAlias) {
openRoomAliasLink.link = matrixLink;
openRoomAliasLink.joinRoom = joinRoom;
openRoomAliasLink.call();
return;
}
const roomId = matrixLink.identifiers[0];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
} else {
joinRoom(roomId, servers);
}
return;
}
}
}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
index fecd813..93d882d 100644
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -1,479 +1,480 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title-with-user-id = { -kt-app-name } - { $userId }
app-title-not-logged-in = { -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-verifications = 设备验证
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 = 服务器链接(可选):
login-page-next-action = 下一步
login-page-server-label = 连接到 { $serverUrl }
login-page-login-flow-choice = 选择一种登录方式:
login-page-login-flow-password = 密码
login-page-login-flow-sso = 单点登录
login-page-login-with-sso-button = 进行单点登录
login-page-login-sso-redirect-url-label = 你应该看到浏览器打开了一个网页。遵从那个页面上的指示。如果没看到,那就手动打开下面的 URL:
login-page-change-server-button = 切换服务器
login-page-no-flow-supported = 这个服务器并不提供任何 { -kt-app-name } 支持的登录流程。
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 = 发送贴纸
save-draft-confirmation-title = 保存草稿
save-draft-confirmation = 你有一个未发送的草稿,需要保存草稿以便在稍后恢复它吗?
save-draft-confirm-action = 保存
save-draft-discard-action = 丢弃
save-draft-cancel-action = 维持现状
discard-draft-confirmation-title = 丢弃草稿
discard-draft-confirmation = 你有一个未发送的草稿,你的操作将会丢弃它。
discard-draft-confirm-action = 丢弃
discard-draft-keep-action = 保留
discard-draft-cancel-action = 维持现状
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 }。
+event-request-key-action = 请求加密密钥
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 }。
sticker-remove-action = 移除贴纸...
sticker-remove-confirmation-title = 移除贴纸
sticker-remove-confirmation-message = 确定要移除贴纸「{ $shortCode }」吗?
sticker-remove-confirm-action = 移除
sticker-remove-cancel-action = 不移除
sticker-remove-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-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 }。
user-devices = 设备
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 = 取消
device-verify-action = 验证设备...
device-verification-request-failed = 无法发送验证请求。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
device-item-title-deleted = (已删除) { $deviceId }
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 = 缓存目录:
settings-select-cache-directory = 选择缓存目录...
settings-security-import-keys-action = 从备份文件导入密钥...
settings-security-import-keys-password-input-title = 输入密码
settings-security-import-keys-password-input-label = 当你创建备份时,提示了你要设置一个用来加密备份文件的密码。输入你当时设置的那个密码:
settings-security-import-keys-confirm-action = 导入
settings-security-import-keys-cancel-action = 不导入
settings-security-import-keys-success-notification = 导入了 { $imported } 个密钥。
settings-security-import-keys-failure-notification = 无法加载密钥备份文件。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
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 }。
verifications-page-title = 设备验证
verification-state-they-requested = 请求验证了此设备。
verification-state-waiting = 正在等待另一方。
verification-state-waiting-after-verified = 正在等待另一方。
verification-state-confirm-code = 请确认这些代码是否和另一个设备上一致。
verification-state-done = 过程已经完成。
verification-state-cancelled = 过程被取消了。
verification-action-ready = 继续
verification-action-cancel = 取消
verification-action-confirm-sas-match = 是一致的
verification-action-deny-sas-match = 并不一致
verification-show-numbers = 转而显示数字
verification-show-emojis = 转而显示表情文字
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
index 82d82f3..47026de 100644
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -1,501 +1,502 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title-with-user-id = { -kt-app-name } - { $userId }
app-title-not-logged-in = { -kt-app-name } - Not Logged In
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-verifications = Device Verification
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):
login-page-next-action = Next
login-page-server-label = Connecting to { $serverUrl }
login-page-login-flow-choice = Choose a way to log in:
login-page-login-flow-password = Password
login-page-login-flow-sso = Single Sign On
login-page-login-with-sso-button = Log in using Single Sign On
login-page-login-sso-redirect-url-label = You should see a web page open in your browser. Follow the instructions on that page. If not, manually open the following URL:
login-page-change-server-button = Change server
login-page-no-flow-supported = This server does not have any login flow that { -kt-app-name } supports.
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
save-draft-confirmation-title = Save draft
save-draft-confirmation = You have an unsent draft, do you want to save it to restore it later?
save-draft-confirm-action = Save it
save-draft-discard-action = Discard it
save-draft-cancel-action = Do nothing
discard-draft-confirmation-title = Discard draft
discard-draft-confirmation = You have an unsent draft, your operation will dicard it.
discard-draft-confirm-action = Discard it
discard-draft-keep-action = Keep it
discard-draft-cancel-action = Do nothing
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 }.
+event-request-key-action = Request encryption key
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 }.
sticker-remove-action = Remove sticker...
sticker-remove-confirmation-title = Remove sticker
sticker-remove-confirmation-message = Are you sure you want to remove the sticker "{ $shortCode }"?
sticker-remove-confirm-action = Remove
sticker-remove-cancel-action = Do not remove
sticker-remove-failed-prompt = Unable to remove 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-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 }.
user-devices = Devices
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
device-verify-action = Verify device...
device-verification-request-failed = Unable to send verification request. Error code: { $errorCode }. Error message: { $errorMsg }.
device-item-title-deleted = (Deleted) { $deviceId }
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:
settings-select-cache-directory = Select cache directory...
settings-security-import-keys-action = Import keys from a backup file...
settings-security-import-keys-password-input-title = Enter password
settings-security-import-keys-password-input-label = When you created the backup, you were prompted to set a password to encrypt the backup file. Enter the password you set at that time:
settings-security-import-keys-confirm-action = Import
settings-security-import-keys-cancel-action = Do not import
settings-security-import-keys-success-notification = Imported { $imported } keys.
settings-security-import-keys-failure-notification = Unable to load key backup file. Error code: { $errorCode }. Error message: { $errorMsg }.
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 }.
verifications-page-title = Device Verification
verification-state-they-requested = requested to verify this device.
verification-state-waiting = Waiting for the other party.
verification-state-waiting-after-verified = Waiting for the other party.
verification-state-confirm-code = Please confirm whether the codes match the other device.
verification-state-done = The process is done.
verification-state-cancelled = The process is cancelled.
verification-action-ready = Continue
verification-action-cancel = Cancel
verification-action-confirm-sas-match = They match
verification-action-deny-sas-match = They do not match
verification-show-numbers = Show numbers instead
verification-show-emojis = Show emojis instead
diff --git a/src/matrix-session.cpp b/src/matrix-session.cpp
index b4af0e1..d7ef922 100644
--- a/src/matrix-session.cpp
+++ b/src/matrix-session.cpp
@@ -1,680 +1,695 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "matrix-session.hpp"
#include "matrix-utils.hpp"
#include "matrix-room-list.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "device-mgmt/matrix-device-list.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack-list-p.hpp"
#include "matrix-user-given-attrs-map.hpp"
#include "matrix-verification-list.hpp"
#include "sso-login-process.hpp"
#include "db-store.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
#include <csapi/login.hpp>
#include <csapi/directory.hpp>
#include <client/alias.hpp>
#include <QFile>
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const std::string clientName = "kazv";
MatrixSession::MatrixSession(
MatrixSessionContextT context,
Client client,
LagerStoreEventEmitter::Watchable watchable,
lager::reader<VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent
)
: QObject(parent)
, m_context(std::move(context))
, m_clientOnSecondaryRoot(std::move(client))
, m_watchable(std::move(watchable))
, m_notificationHandler(m_clientOnSecondaryRoot.notificationHandler())
, m_verificationTrackerState(std::move(verificationTrackerState))
, m_ssoLoginProcess()
, m_dbStoreGetter(std::move(dbStoreGetter))
, LAGER_QT(serverUrl)(m_clientOnSecondaryRoot.serverUrl().xform(strToQt))
, LAGER_QT(userId)(m_clientOnSecondaryRoot.userId().xform(strToQt))
, LAGER_QT(token)(m_clientOnSecondaryRoot.token().xform(strToQt))
, LAGER_QT(deviceId)(m_clientOnSecondaryRoot.deviceId().xform(strToQt))
, LAGER_QT(specVersions)(m_clientOnSecondaryRoot.supportVersions())
{
m_watchable.afterAll(
[this](KazvTrigger e) {
Q_EMIT this->trigger(e);
});
m_watchable.after<LoginSuccessful>(
[this](LoginSuccessful e) {
Q_EMIT this->loginSuccessful(e);
});
m_watchable.after<LoginFailed>(
[this](LoginFailed e) {
Q_EMIT this->loginFailed(
QString::fromStdString(e.errorCode),
QString::fromStdString(e.error)
);
});
m_watchable.after<ReceivingRoomTimelineEvent>(
[this](ReceivingRoomTimelineEvent e) {
Q_EMIT this->receivedMessage(
QString::fromStdString(e.roomId),
QString::fromStdString(e.event.id())
);
});
}
MatrixSession::~MatrixSession() = default;
QString MatrixSession::mxcUriToHttp(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
}
QString MatrixSession::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
}
MatrixDeviceList *MatrixSession::devicesOfUser(QString userId) const
{
return new MatrixDeviceList(m_clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
}
bool isIllFormatSpecVersion(const QString &version)
{
const bool isLegacy = version.startsWith(u"r"_s);
if (isLegacy && version.split(u'.').size() == 3) {
return false;
}
if (!isLegacy && version.split(u'.').size() == 2) {
return false;
}
return true;
}
// Return true if v1 is at least as new as v2, false if v1 is older than v2
// Return false if v1 or v2 is ill-format
bool compareSpecVersion(QString v1, QString v2)
{
// Check parameters format
if (isIllFormatSpecVersion(v1) || isIllFormatSpecVersion(v2)) {
return false;
}
const bool v1IsLegacy = v1.startsWith(u"r"_s);
const bool v2IsLegacy = v2.startsWith(u"r"_s);
if (v1IsLegacy != v2IsLegacy) {
return v2IsLegacy;
}
v1.remove(0, 1);
v2.remove(0, 1);
auto v1VersionNumbers = v1.split(u'.');
auto v2VersionNumbers = v2.split(u'.');
for (int i = 0; i < v1VersionNumbers.size(); i++) {
auto v1VerNum = v1VersionNumbers[i].toInt();
auto v2VerNum = v2VersionNumbers[i].toInt();
if (v1VerNum != v2VerNum) {
return v1VerNum > v2VerNum;
}
}
// v1 is equal to v2
return true;
}
// Return true if version in the range [minVer, maxVer]
// Return false if any parameter is ill-format
bool compareSpecVersionRange(const QString &version,
const QString &minVer, const QString &maxVer)
{
if (isIllFormatSpecVersion(version)
|| isIllFormatSpecVersion(minVer)
|| isIllFormatSpecVersion(maxVer)) {
return false;
}
if (compareSpecVersion(version, minVer) && compareSpecVersion(maxVer, version)) {
return true;
}
return false;
}
bool MatrixSession::checkSpecVersion(QString version) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&version](auto v) {
return compareSpecVersion(QString::fromStdString(v), version);
}) != specVersions().end();
}
bool MatrixSession::checkSpecVersionRange(QString minVer, QString maxVer) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&minVer, maxVer](auto v) {
return compareSpecVersionRange(QString::fromStdString(v), minVer, maxVer);
}) != specVersions().end();
}
QStringList MatrixSession::directRoomIds(QString userId) const
{
auto content = m_clientOnSecondaryRoot.accountData().get()["m.direct"].content().get();
auto roomIds = QStringList{};
for (auto i : content[userId.toStdString()]) {
roomIds.push_back(QString::fromStdString(i.get<std::string>()));
}
return roomIds;
}
MatrixVerificationList *MatrixSession::verificationList() const
{
return new MatrixVerificationList(m_clientOnSecondaryRoot, m_verificationTrackerState);
}
std::string MatrixSession::validateHomeserverUrl(const QString &url)
{
if (url.isEmpty()) {
return std::string();
}
auto u = QUrl::fromUserInput(url);
if (!u.isValid()) {
return std::string();
}
if (u.scheme() == u"http"_s) {
qCInfo(kazvLog) << "url" << u << "is http. Force switching to https.";
u.setScheme(u"https"_s);
} else if (u.scheme() != u"https"_s) {
qCWarning(kazvLog) << "url" << u << "is not http/https.";
return std::string();
}
return u.toString().toStdString();
}
void MatrixSession::login(const QString &userId, const QString &password, const QString &homeserverUrl)
{
auto loginFunc = [userId, password](const Client &client, const std::string &serverUrl) {
client.passwordLogin(
serverUrl,
userId.toStdString(),
password.toStdString(),
clientName,
/* startSyncingOnSuccess = */ true
);
};
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
loginFunc(m_clientOnSecondaryRoot, validated);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
.then([
this,
client=m_clientOnSecondaryRoot.toEventLoop(),
userId,
password,
loginFunc
](auto res) {
if (!res.success()) {
// FIXME use real error codes and msgs when available in libkazv
Q_EMIT this->discoverFailed(u""_s, u""_s);
return res;
}
auto serverUrl = res.dataStr("homeserverUrl");
loginFunc(client, serverUrl);
return res;
});
}
}
void MatrixSession::discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl)
{
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
getLoginFlows(homeserverUrl);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString())
.then([
this
](const EffectStatus &res) {
if (!res.success()) {
Q_EMIT discoverFailed(
QString::fromStdString(res.dataStr("errorCode")),
QString::fromStdString(res.dataStr("error"))
);
return;
}
auto serverUrl = QString::fromStdString(res.dataStr("homeserverUrl"));
Q_EMIT discoverSuccessful(serverUrl);
getLoginFlows(serverUrl);
});
}
}
void MatrixSession::getLoginFlows(const QString &serverUrl)
{
lager::get<JobInterface &>(m_context).submit(
Api::GetLoginFlowsJob(serverUrl.toStdString()),
[this](Api::GetLoginFlowsResponse r) {
if (!r.success()) {
Q_EMIT getLoginFlowsFailed(
QString::fromStdString(r.errorCode()),
QString::fromStdString(r.errorMessage())
);
return;
}
auto v = std::move(r).jsonBody().get().at("flows").template get<QJsonValue>();
Q_EMIT getLoginFlowsSuccessful(v);
}
);
}
QUrl MatrixSession::ssoLoginStart(const QString &homeserverUrl)
{
if (m_ssoLoginProcess) {
m_ssoLoginProcess.reset();
}
m_ssoLoginProcess.reset(new SsoLoginProcess());
if (!m_ssoLoginProcess->startServer()) {
return QUrl();
}
auto link = m_ssoLoginProcess->getSsoLink(homeserverUrl);
connect(m_ssoLoginProcess.get(), &SsoLoginProcess::loginTokenAvailable,
this, [this, homeserverUrl](const QString &loginToken) {
Q_EMIT ssoLoginTokenAvailable();
qCInfo(kazvLog) << "Got SSO login token";
m_ssoLoginProcess.reset();
m_clientOnSecondaryRoot.mLoginTokenLogin(
homeserverUrl.toStdString(),
loginToken.toStdString(),
clientName,
/* startSyncingOnSuccess = */ true
);
});
return link;
}
void MatrixSession::logout()
{
m_clientOnSecondaryRoot.logout()
.then([&] (EffectStatus stat) {
if (stat.success()) {
Q_EMIT this->logoutSuccessful();
} else {
Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
}
});
}
MatrixRoomList *MatrixSession::roomList() const
{
return new MatrixRoomList(m_clientOnSecondaryRoot);
}
static std::optional<std::string> optMaybe(QString s)
{
if (s.isEmpty()) {
return std::nullopt;
} else {
return s.toStdString();
}
}
MatrixPromise *MatrixSession::createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
Constants::CreateRoomPreset preset,
bool encrypted
)
{
immer::array<Event> initialState;
if (encrypted) {
initialState = {Event{json{
{"type", "m.room.encryption"},
{"state_key", ""},
{"content", {
{"algorithm", "m.megolm.v1.aes-sha2"},
}},
}}};
}
return new MatrixPromise(m_clientOnSecondaryRoot.createRoom(
isPrivate ? Kazv::RoomVisibility::Private : Kazv::RoomVisibility::Public,
optMaybe(name),
optMaybe(alias),
qStringListToStdF(invite),
isDirect,
allowFederate,
optMaybe(topic),
nlohmann::json(powerLevelContentOverride),
static_cast<Kazv::CreateRoomPreset>(preset),
initialState
));
}
MatrixPromise *MatrixSession::joinRoom(const QString &idOrAlias, const QStringList &servers)
{
return new MatrixPromise(m_clientOnSecondaryRoot.joinRoom(
idOrAlias.toStdString(),
qStringListToStdF(servers)
));
}
MatrixPromise *MatrixSession::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDeviceTrustLevel(
userId.toStdString(),
deviceId.toStdString(),
qStringToTrustLevelFunc(trustLevel)
)
);
}
MatrixPromise *MatrixSession::getSelfProfile()
{
return new MatrixPromise(
m_clientOnSecondaryRoot.getProfile(userId().toStdString())
);
}
MatrixPromise *MatrixSession::setDisplayName(QString displayName)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDisplayName(
displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
)
);
}
MatrixPromise *MatrixSession::setAvatarUrl(QString avatarUrl)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setAvatarUrl(
avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
)
);
}
bool MatrixSession::shouldNotify(MatrixEvent *event) const
{
// Do not notify own event
if (event->sender() == userId()) {
return false;
}
return m_notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
}
bool MatrixSession::shouldPlaySound(MatrixEvent *event) const
{
return m_notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
}
MatrixStickerPackList *MatrixSession::stickerPackList() const
{
return new MatrixStickerPackList(m_clientOnSecondaryRoot);
}
MatrixEvent *MatrixSession::stickerRoomsEvent() const
{
return new MatrixEvent(m_clientOnSecondaryRoot.accountData().map(getCanonicalImagePackRoomsEvent));
}
MatrixPromise *MatrixSession::updateStickerRooms(const QJsonObject &content)
{
auto promise = m_context.createResolvedPromise({});
for (const auto &type : imagePackRoomsEventTypes) {
auto event = Event(json{
{"type", type},
{"content", content},
});
promise = promise.then([client=m_clientOnSecondaryRoot.toEventLoop(), event, ctx=m_context](const auto &stat) {
if (stat.success()) {
return client.setAccountData(event);
}
return ctx.createResolvedPromise(stat);
});
}
return new MatrixPromise(promise);
}
MatrixPromise *MatrixSession::updateStickerPack(MatrixStickerPackSource source)
{
if (source.source == MatrixStickerPackSource::AccountData) {
auto eventJson = std::move(source.event).raw().get();
eventJson["type"] = source.eventType;
return sendAccountDataImpl(Event(std::move(eventJson)));
} else if (source.source == MatrixStickerPackSource::RoomState) {
auto room = m_clientOnSecondaryRoot
.room(source.roomId);
auto promise = m_context.createResolvedPromise({});
for (const auto &type : roomStateEventTypes) {
auto eventJson = std::move(source.event).raw().get();
eventJson["type"] = type;
eventJson["state_key"] = source.stateKey;
promise = promise.then([room=room.toEventLoop(), eventJson, ctx=m_context](const auto &stat) {
if (stat.success()) {
return room.sendStateEvent(Event(eventJson));
}
return ctx.createResolvedPromise(stat);
});
}
return new MatrixPromise(promise);
} else {
return 0;
}
}
MatrixPromise *MatrixSession::addRoomStickerPack(const QString &roomId, const QString &stateKey, const QString &displayName)
{
auto stateKeyStd = stateKey.toStdString();
auto roomIdStd = roomId.toStdString();
auto room = m_clientOnSecondaryRoot.room(roomIdStd);
auto eventJson = getCanonicalImagePackForRoom(room.stateEvents().make().get(), stateKeyStd).originalJson().get();
if (!eventJson.contains("content")) {
eventJson["content"] = json::object();
}
if (!eventJson["content"].contains("pack")) {
eventJson["content"]["pack"] = json::object();
}
eventJson["content"]["pack"]["display_name"] = displayName.toStdString();
return updateStickerPack(MatrixStickerPackSource{
MatrixStickerPackSource::RoomState,
roomStateEventTypes[0],
Event(eventJson),
roomIdStd,
stateKeyStd,
});
}
MatrixUserGivenAttrsMap *MatrixSession::userGivenNicknameMap() const
{
return new MatrixUserGivenAttrsMap(
userGivenNicknameMapFor(m_clientOnSecondaryRoot),
[client=m_clientOnSecondaryRoot](json content) {
return client.setAccountData(json{
{"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
{"content", std::move(content)},
});
}
);
}
MatrixPromise *MatrixSession::sendAccountData(const QString &type, const QJsonObject &content)
{
Event e = json{
{"type", type.toStdString()},
{"content", content},
};
return sendAccountDataImpl(std::move(e));
}
MatrixPromise *MatrixSession::sendAccountDataImpl(Event event)
{
return new MatrixPromise(m_clientOnSecondaryRoot.setAccountData(event));
}
MatrixPromise *MatrixSession::getSpecVersions()
{
return new MatrixPromise(m_clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
}
MatrixPromise *MatrixSession::addDirectRoom(const QString &userId, const QString &roomId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.addDirectRoom(userId.toStdString(), roomId.toStdString()));
}
MatrixPromise *MatrixSession::getRoomIdByAlias(const QString &roomAlias)
{
auto job = m_clientOnSecondaryRoot
.getRoomIdByAliasJob(roomAlias.toStdString());
return new MatrixPromise(
m_context.createPromise([ctx=m_context, job](auto resolve) {
lager::get<JobInterface &>(ctx).submit(job, [resolve](GetRoomIdByAliasResponse r) {
resolve(parseGetRoomIdByAliasResponse(r));
});
}));
}
inline constexpr std::size_t purgeEventsKeepNumber = 5;
MatrixPromise *MatrixSession::purgeEventsExceptRooms(const QStringList &roomIds)
{
return new MatrixPromise(
m_context.createResolvedPromise({})
.then([client=m_clientOnSecondaryRoot.toEventLoop(), roomIds=qStringListToStdF(roomIds)
]([[maybe_unused]] auto &&stat) {
auto map = intoImmer(immer::map<std::string, std::size_t>{},
zug::filter([&roomIds](const std::string &roomId) {
return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
})
| zug::map([](const std::string &roomId) {
return std::make_pair(roomId, purgeEventsKeepNumber);
}),
client.roomIds().make().get()
);
return client.purgeRoomEvents(map);
}).then([](const auto &stat) {
qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
return stat;
})
);
}
MatrixPromise *MatrixSession::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
{
return new MatrixPromise(
m_context.createPromise([
dbStoreGetter=m_dbStoreGetter,
client=m_clientOnSecondaryRoot.toEventLoop(),
roomId,
eventId
](auto resolve) {
dbStoreGetter().getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
auto [timelineEvents, relatedEvents] = std::move(res);
auto loadedCount = timelineEvents[roomId.toStdString()].size();
client.loadEventsFromStorage(timelineEvents, relatedEvents)
.then([resolve, loadedCount](auto &&) {
resolve(EffectStatus(
!!loadedCount,
json{{"loadedCount", loadedCount}}
));
});
});
})
);
}
MatrixPromise *MatrixSession::loadEvent(const QString &roomId, const QString &eventId)
{
return new MatrixPromise(
m_context.createPromise([
dbStoreGetter=m_dbStoreGetter,
client=m_clientOnSecondaryRoot.toEventLoop(),
roomId,
eventId
](auto resolve) {
dbStoreGetter().getEventById(roomId, eventId).then([client, resolve, roomId](auto &&res) {
if (!res.has_value()) {
resolve(EffectStatus(/* succ = */ false));
return;
}
auto [event, _isInTimeline] = std::move(res).value();
// Disregard isInTimeline because when we load one single event,
// we do not want to add it to the timeline. If we add it,
// we lose info about the which events are before/after it,
// causing the remaining timeline to be gapped without a way
// to refill it.
immer::map<std::string, Kazv::EventList> relatedEvents = {
{roomId.toStdString(), {event}},
};
client.loadEventsFromStorage({}, relatedEvents)
.then([resolve](auto &&) {
resolve(EffectStatus(/* succ = */ true));
});
});
})
);
}
MatrixPromise *MatrixSession::importFromKeyBackupFile(QUrl fileUrl, QString password)
{
if (!fileUrl.isLocalFile()) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "Not a local file"}, {"errorCode", "NOT_LOCAL_FILE"}},
}));
}
auto filename = fileUrl.toLocalFile();
auto f = QFile(filename);
if (!f.open(QFile::ReadOnly)) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "File open failed"}, {"errorCode", "FILE_OPEN_FAILED"}},
}));
}
auto ba = f.readAll();
auto content = std::string(ba.begin(), ba.end());
auto passwordStd = std::move(password).toStdString();
return new MatrixPromise(m_clientOnSecondaryRoot.importFromKeyBackupFile(
std::move(content), std::move(passwordStd)
));
}
MatrixPromise *MatrixSession::requestVerifyDevice(QString userId, QString deviceId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.requestOutgoingToDeviceVerification(
std::move(userId).toStdString(),
std::move(deviceId).toStdString()
));
}
+
+MatrixPromise *MatrixSession::requestShareRoomSessionKey(MatrixEvent *event)
+{
+ auto e = event->underlyingEvent();
+ if (!(e.originalJson().get().contains("room_id")
+ && e.originalJson().get().at("room_id").is_string())) {
+ return nullptr;
+ }
+ auto roomId = e.originalJson().get().at("room_id").template get<std::string>();
+
+ return new MatrixPromise(m_clientOnSecondaryRoot.requestShareRoomSessionKey(
+ roomId,
+ e
+ ));
+}
diff --git a/src/matrix-session.hpp b/src/matrix-session.hpp
index fe28523..dd64747 100644
--- a/src/matrix-session.hpp
+++ b/src/matrix-session.hpp
@@ -1,331 +1,341 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "helper.hpp"
#include "meta-types.hpp"
#include "matrix-session-types.hpp"
#include "constants.hpp"
#include <client.hpp>
#include <notification-handler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <lager/extra/qt.hpp>
#include <QObject>
#include <QQmlEngine>
Q_MOC_INCLUDE("matrix-room-list.hpp")
Q_MOC_INCLUDE("matrix-device-list.hpp")
Q_MOC_INCLUDE("matrix-promise.hpp")
Q_MOC_INCLUDE("matrix-event.hpp")
Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
Q_MOC_INCLUDE("matrix-verification-list.hpp")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
class MatrixVerificationList;
class SsoLoginProcess;
class DbStore;
/**
* Represent all operations that can be taken within a Matrix session.
*/
class MatrixSession : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
MatrixSessionContextT m_context;
Kazv::Client m_clientOnSecondaryRoot;
Kazv::LagerStoreEventEmitter::Watchable m_watchable;
Kazv::NotificationHandler m_notificationHandler;
lager::reader<Kazv::VerificationTrackerModel> m_verificationTrackerState;
UniquePtrDL<SsoLoginProcess> m_ssoLoginProcess;
std::function<DbStore &()> m_dbStoreGetter;
public:
MatrixSession(
MatrixSessionContextT context,
Kazv::Client client,
Kazv::LagerStoreEventEmitter::Watchable watchable,
lager::reader<Kazv::VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent = nullptr
);
~MatrixSession() override;
static std::string validateHomeserverUrl(const QString &url);
LAGER_QT_READER(QString, serverUrl);
LAGER_QT_READER(QString, userId);
LAGER_QT_READER(QString, token);
LAGER_QT_READER(QString, deviceId);
LAGER_QT_READER(immer::array<std::string>, specVersions); // The versions of the Matrix Spec supported by the server.
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
Q_INVOKABLE QString mxcUriToHttpAuthenticatedV1(QString mxcUri) const;
Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
// Return true if version is at least as new as the spec version of server
Q_INVOKABLE bool checkSpecVersion(QString version) const;
// Return true if the spec version of server in the range [minVer, maxVer]
Q_INVOKABLE bool checkSpecVersionRange(QString minVer, QString maxVer) const;
Q_INVOKABLE QStringList directRoomIds(QString userId) const;
Q_INVOKABLE MatrixVerificationList *verificationList() const;
Q_SIGNALS:
void trigger(Kazv::KazvTrigger e);
void loginSuccessful(Kazv::KazvTrigger e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void discoverSuccessful(QString serverUrl);
void getLoginFlowsFailed(QString errorCode, QString errorMsg);
void getLoginFlowsSuccessful(QJsonValue flows);
void ssoLoginTokenAvailable();
void logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
public Q_SLOTS:
void login(const QString &userId, const QString &password, const QString &homeserverUrl);
/**
* Auto-discover the server url and then get the login flows from the server.
*
* If homeserverUrl is not provided, try to get it from auto-discovery.
* If discovery is successful, or it is already provided, get the login flows
* by calling getLoginFlows.
*/
void discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl);
void getLoginFlows(const QString &serverUrl);
/**
* Start SSO login flow.
*
* It will start an http server on a local port and pass it as
* the redirect url to the SSO login link.
*
* When the user completes the SSO login flow, we get the login token
* for us to login via the token flow.
*
* @return The SSO login link for the user to open.
*/
QUrl ssoLoginStart(const QString &homeserverUrl);
void logout();
/**
* 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,
Constants::CreateRoomPreset preset,
bool encrypted
);
/**
* Join a room.
* @param idOrAlias The id or alias of the room to join.
* @param servers The servers to use when joining the room.
*/
MatrixPromise *joinRoom(
const QString &idOrAlias,
const QStringList &servers
);
/**
* Change the trust level of a device.
*
* @param userId The user id that owns the device.
* @param deviceId The device id to set the trust level.
* @param trustLevel The trust level.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel);
/**
* Get the profile of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *getSelfProfile();
/**
* Set the display name of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDisplayName(QString displayName);
/**
* Set the avatar url of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setAvatarUrl(QString avatarUrl);
/**
* Check if an event should be notified.
*
* @param event The event to check.
* @return Whether `event` should be notified.
*/
bool shouldNotify(MatrixEvent *event) const;
/**
* Check if an event should be notified with sound.
*
* You should only call this method when `shouldNotify(event)`
* returns true.
*
* @param event The event to check.
* @return Whether `event` should be notified with sound.
*/
bool shouldPlaySound(MatrixEvent *event) const;
/**
* Get the sticker pack list for the current account.
*
* @return A list of sticker packs associated with the current account.
*/
MatrixStickerPackList *stickerPackList() const;
/**
* Get the sticker rooms account data event for the current account.
*
* @return A MatrixEvent representing the sticker rooms account data event.
*/
MatrixEvent *stickerRoomsEvent() const;
/**
* Update the sticker rooms for this account.
*
* @param content The content object for the sticker rooms account data event.
* @return A promise that resolves when the sticker rooms account data is updated,
* or when there is an error.
*/
MatrixPromise *updateStickerRooms(const QJsonObject &content);
/**
* 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);
/**
* Create a new sticker pack in a room.
*
* @param roomId The id of the room.
* @param stateKey The state key of the pack.
* @param displayName Thi display name of the pack.
* @return A promise that resolves when the sticker pack is added,
* or when there is an error.
*/
MatrixPromise *addRoomStickerPack(const QString &roomId, const QString &stateKey, const QString &displayName);
MatrixUserGivenAttrsMap *userGivenNicknameMap() const;
MatrixPromise *sendAccountData(const QString &type, const QJsonObject &content);
/**
* Get all Matrix Spec versions supported by the server.
* Use MatrixSdk::supportSpecVersion() to check if a version is supported.
*/
MatrixPromise *getSpecVersions();
MatrixPromise *addDirectRoom(const QString &userId, const QString &roomId);
MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
/**
* Purge events in all rooms except those specified in roomIds
*
* @param roomIds The ids for rooms to NOT be purged. This allows
* keeping the rooms that are already open intact.
* @return A MatrixPromise that resolves when the events are purged.
*/
MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
/**
* Backfill from the event storage from an existing event in a room.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to backfill from.
* @return A MatrixPromise that resolves when the events are backfilled.
* The Promise is considered successful if and only if at least one event
* is loaded.
*/
MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
/**
* Load one event from the store.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to load.
* @return A MatrixPromise that resolves when loading is finished.
* The Promise is considered successful if and only if the event is loaded.
*/
MatrixPromise *loadEvent(const QString &roomId, const QString &eventId);
/**
* Import from key backup file.
*
* @param fileUrl The url of the key backup file. Will be passed to
* QUrl::toLocalFile(), and must be a local file.
* @param password The password to decrypt the key backup file.
* @return A MatrixPromise that resolves when the keys are imported.
* If successful, `data["imported"]` contains the number of imported keys.
* If unsuccessful, `data` contains a standard error structure.
*/
MatrixPromise *importFromKeyBackupFile(QUrl fileUrl, QString password);
/**
* Send an outbound device verification request.
*
* @param userId The user id of the device to verify.
* @param deviceId The device id of the device to verify.
*/
MatrixPromise *requestVerifyDevice(QString userId, QString deviceId);
+ /**
+ * Request other sessions to send this a shared room key that can
+ * decrypt `event`.
+ *
+ * @param event An undecryptable event.
+ * @return A Promise that resolves when the request is sent, or when
+ * there is an error.
+ */
+ MatrixPromise *requestShareRoomSessionKey(MatrixEvent *event);
+
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
};

File Metadata

Mime Type
text/x-diff
Expires
Fri, Aug 28, 8:19 AM (2 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1735721
Default Alt Text
(94 KB)

Event Timeline