Page MenuHomePhorge

No OneTemporary

Size
94 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/room-settings/RoomSettingsPage.qml b/src/contents/ui/room-settings/RoomSettingsPage.qml
index 8c38ef4..713a532 100644
--- a/src/contents/ui/room-settings/RoomSettingsPage.qml
+++ b/src/contents/ui/room-settings/RoomSettingsPage.qml
@@ -1,369 +1,426 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.20 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '../matrix-helpers.js' as Helpers
import '..' as Kazv
import '.' as RoomSettings
Kazv.ClosableScrollablePage {
id: roomSettingsPage
property var room
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomSettingsPage.room
}
property var roomDisplayName: roomNameProvider.name
property var customTagIds: room.tagIds.filter(k => k.startsWith('u.'))
+ property var editingName: false
+ property var submittingName: false
+ property var submitName: Kazv.AsyncHandler {
+ trigger: () => {
+ roomSettingsPage.submittingName = true;
+ return roomSettingsPage.room.setName(roomNameEdit.text);
+ }
+ onResolved: (success, data) => {
+ roomSettingsPage.submittingName = false;
+ if (success) {
+ roomSettingsPage.editingName = false;
+ } else {
+ showPassiveNotification(l10n.get('room-settings-set-name-failed-prompt', {
+ room: room.roomId,
+ errorCode: data.errorCode,
+ errorMsg: data.error,
+ }));
+ }
+ }
+ }
+
property var editingTopic: false
property var submittingTopic: false
property var submitTopic: Kazv.AsyncHandler {
trigger: () => {
roomSettingsPage.submittingTopic = true;
return roomSettingsPage.room.setTopic(roomTopicEdit.text);
}
onResolved: (success, data) => {
roomSettingsPage.submittingTopic = false;
if (success) {
roomSettingsPage.editingTopic = false;
} else {
showPassiveNotification(l10n.get('room-settings-set-topic-failed-prompt', {
room: room.roomId,
errorCode: data.errorCode,
errorMsg: data.error,
}));
}
}
}
title: l10n.get('room-settings-page-title', { room: roomDisplayName })
function tagIdToName(tagId) {
return tagId.slice(2);
}
property var encryptionPopup: Kirigami.PromptDialog {
parent: roomSettingsPage.overlay
objectName: 'encryptionPopup'
title: l10n.get('room-settings-enable-encryption-prompt-dialog-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('room-settings-enable-encryption-prompt-dialog-prompt')
wrapMode: Text.Wrap
}
}
onAccepted: enableEncryption.call()
}
property var enablingEncryption: false
property var enableEncryption: Kazv.AsyncHandler {
trigger: () => {
roomSettingsPage.enablingEncryption = true;
return room.sendStateEvent({
type: 'm.room.encryption',
state_key: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
});
}
onResolved: (success, data) => {
roomSettingsPage.enablingEncryption = false;
if (success) {
showPassiveNotification(l10n.get('room-settings-encryption-enabled-notification'));
} else {
showPassiveNotification(l10n.get('room-settings-encryption-failed-to-enable-notification', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var tagHandler: RoomSettings.RoomTagHandler {
room: roomSettingsPage.room
}
property var members: room.members()
property var avatarCount: 4
ColumnLayout {
RowLayout {
Layout.alignment: Qt.AlignHCenter
Repeater {
objectName: 'roomMembersAvatarsRepeater'
model: Math.min(members.count, roomSettingsPage.avatarCount)
Kazv.AvatarAdapter {
property var member: members.at(index)
property var nameProvider: Kazv.UserNameProvider {
user: member
}
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
source: member.avatarMxcUri ?
matrixSdk.mxcUriToHttp(member.avatarMxcUri) : ''
name: nameProvider.name
}
}
Kirigami.Icon {
objectName: 'roomMembersAvatarsMore'
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
source: 'go-next-skip'
visible: members.count > roomSettingsPage.avatarCount
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
- Label {
- objectName: 'roomNameLabel'
- Layout.alignment: Qt.AlignHCenter
- text: !!room.name ? room.name : room.roomId
- }
Label {
objectName: 'roomIdLabel'
Layout.alignment: Qt.AlignHCenter
font: Kirigami.Theme.smallFont
text: room.roomId
- visible: !!room.name
+ }
+ }
+ RowLayout {
+ visible: !roomSettingsPage.editingName
+ Kazv.SelectableText {
+ objectName: 'roomNameLabel'
+ text: room.name || l10n.get('room-settings-name-missing')
+ wrapMode: Text.Wrap
+ Kirigami.Theme.colorGroup: !!room.name ? Kirigami.Theme.Normal : Kirigami.Theme.Inactive
+ Layout.fillWidth: true
+ }
+ Button {
+ Layout.alignment: Qt.AlignTop
+ objectName: 'editNameButton'
+ text: l10n.get('room-settings-edit-name-action')
+ onClicked: {
+ roomNameEdit.text = room.name;
+ roomSettingsPage.editingName = true;
+ }
+ }
+ }
+ RowLayout {
+ visible: roomSettingsPage.editingName
+ TextArea {
+ id: roomNameEdit
+ objectName: 'roomNameEdit'
+ Layout.fillWidth: true
+ wrapMode: Text.Wrap
+ }
+ Button {
+ Layout.alignment: Qt.AlignTop
+ objectName: 'saveNameButton'
+ enabled: !roomSettingsPage.submittingName
+ text: l10n.get('room-settings-save-name-action')
+ onClicked: roomSettingsPage.submitName.call()
+ }
+ Button {
+ Layout.alignment: Qt.AlignTop
+ objectName: 'discardNameButton'
+ enabled: !roomSettingsPage.submittingName
+ text: l10n.get('room-settings-discard-name-action')
+ onClicked: roomSettingsPage.editingName = false
}
}
RowLayout {
visible: !roomSettingsPage.editingTopic
Kazv.SelectableText {
objectName: 'roomTopicLabel'
text: room.topic || l10n.get('room-settings-topic-missing')
font.italic: !room.topic
wrapMode: Text.Wrap
Kirigami.Theme.colorGroup: !!room.topic ? Kirigami.Theme.Normal : Kirigami.Theme.Inactive
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignTop
objectName: 'editTopicButton'
text: l10n.get('room-settings-edit-topic-action')
onClicked: {
roomTopicEdit.text = room.topic;
roomSettingsPage.editingTopic = true;
}
}
}
RowLayout {
visible: roomSettingsPage.editingTopic
TextArea {
id: roomTopicEdit
objectName: 'roomTopicEdit'
Layout.fillWidth: true
wrapMode: Text.Wrap
}
Button {
Layout.alignment: Qt.AlignTop
objectName: 'saveTopicButton'
enabled: !roomSettingsPage.submittingTopic
text: l10n.get('room-settings-save-topic-action')
onClicked: roomSettingsPage.submitTopic.call()
}
Button {
Layout.alignment: Qt.AlignTop
objectName: 'discardTopicButton'
enabled: !roomSettingsPage.submittingTopic
text: l10n.get('room-settings-discard-topic-action')
onClicked: roomSettingsPage.editingTopic = false
}
}
Button {
text: l10n.get('room-settings-members-action')
icon.name: 'im-user'
Layout.fillWidth: true
onClicked: pageStack.push(Qt.resolvedUrl('RoomMemberListPage.qml'), { room: room, members: room.members() })
}
Button {
text: l10n.get('room-settings-banned-members-action')
icon.name: 'im-kick-user'
icon.color: Kirigami.Theme.negativeTextColor
Layout.fillWidth: true
onClicked: pageStack.push(Qt.resolvedUrl('RoomMemberListPage.qml'), { room: room, members: room.bannedMembers() })
}
Label {
objectName: 'encryptionIndicator'
Layout.fillWidth: true
text: !!room.encrypted ? l10n.get('room-settings-encrypted') : l10n.get('room-settings-not-encrypted')
}
Button {
objectName: 'enableEncryptionButton'
Layout.fillWidth: true
visible: !room.encrypted
enabled: !roomSettingsPage.enablingEncryption
text: l10n.get('room-settings-enable-encryption-action')
onClicked: encryptionPopup.open()
}
RowLayout {
Layout.fillWidth: true
Label {
text: l10n.get('room-settings-tags')
Layout.fillWidth: true
}
CheckBox {
Layout.alignment: Qt.AlignRight
text: l10n.get('room-settings-favourited')
checkable: true
checked: tagHandler.hasTag('m.favourite')
enabled: tagHandler.available
onToggled: tagHandler.toggleTag('m.favourite')
}
}
ListView {
model: customTagIds.length
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: childrenRect.height
delegate: Kirigami.SwipeListItem {
required property var index
contentItem: RowLayout {
Label {
Layout.fillWidth: true
text: tagIdToName(customTagIds[index])
}
}
actions: [
Kirigami.Action {
enabled: tagHandler.available
icon.name: 'list-remove-symbolic'
text: l10n.get('room-settings-remove-tag')
onTriggered: tagHandler.toggleTag(customTagIds[index])
}
]
}
}
RowLayout {
TextField {
id: newTagName
enabled: tagHandler.available
text: ''
Layout.fillWidth: true
}
Button {
enabled: tagHandler.available
text: l10n.get('room-settings-add-tag')
icon.name: 'list-add'
onClicked: {
tagHandler.toggleTag('u.' + newTagName.text)
}
}
Connections {
target: tagHandler
function onTagAdded(tagId) {
if (tagId === 'u.' + newTagName.text) {
newTagName.text = '';
}
}
}
}
Button {
objectName: 'stickerPacksButton'
text: l10n.get('room-sticker-packs-action')
icon.name: 'smiley'
Layout.fillWidth: true
onClicked: {
activateRoomStickerPacksPage(roomSettingsPage.room);
}
}
Button {
text: l10n.get('room-invite-action')
icon.name: 'list-add-user'
Layout.fillWidth: true
onClicked: {
activateRoomInvitePage(roomSettingsPage.room);
}
}
Button {
objectName: 'leaveRoomButton'
text: l10n.get('room-leave-action')
icon.name: 'im-ban-kick-user'
icon.color: Kirigami.Theme.negativeTextColor
Layout.fillWidth: true
onClicked: {
confirmLeaveOverlay.open();
}
}
Button {
objectName: 'forgetRoomButton'
text: l10n.get('room-forget-action')
Layout.fillWidth: true
visible: room.membership === MK.MatrixRoom.Leave
onClicked: {
confirmForgetOverlay.open();
}
}
}
property var confirmLeaveOverlay: Kazv.ConfirmationOverlay {
objectName: 'confirmLeaveRoomPopup'
parent: roomSettingsPage.overlay
title: l10n.get('room-leave-confirm-popup-title')
message: l10n.get('room-leave-confirm-popup-message')
confirmActionText: l10n.get('room-leave-confirm-popup-confirm-action')
cancelActionText: l10n.get('room-leave-confirm-popup-cancel-action')
onAccepted: leaveRoomHandler.call()
}
property var leaveRoomHandler: Kazv.AsyncHandler {
trigger: () => room.leaveRoom()
onResolved: {
if (success) {
showPassiveNotification(l10n.get('leave-room-success-prompt', { room: room.roomId }));
inviteOverlay.close();
} else {
showPassiveNotification(l10n.get('leave-room-failed-prompt', { room: room.roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var confirmForgetOverlay: Kazv.ConfirmationOverlay {
objectName: 'confirmForgetRoomPopup'
parent: roomSettingsPage.overlay
title: l10n.get('room-forget-confirm-popup-title')
message: l10n.get('room-forget-confirm-popup-message')
confirmActionText: l10n.get('room-forget-confirm-popup-confirm-action')
cancelActionText: l10n.get('room-forget-confirm-popup-cancel-action')
onAccepted: forgetRoomHandler.call()
}
property var forgetRoomHandler: Kazv.AsyncHandler {
trigger: () => room.forgetRoom()
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('forget-room-success-prompt', { room: room.roomId }));
} else {
showPassiveNotification(l10n.get('forget-room-failed-prompt', { room: room.roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
index b2d3d86..b1a12e4 100644
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -1,399 +1,404 @@
### 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-prompt = 不能检测此用户所在的服务器,或者服务器不可用。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
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-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 = 我的贴纸
room-timeline-load-more-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-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-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-visibility-prompt = 房间可见性:
create-room-page-visibility-public = 公开
create-room-page-visibility-private = 私有
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-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-titile = 封禁用户
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 = 邀请到房间...
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 = 取消
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
index b196594..01f7147 100644
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -1,427 +1,432 @@
### 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-prompt = Unable to detect the server this user is on, or the server is unavailable. Error code: { $errorCode }. Error message: { $errorMsg }.
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-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
room-timeline-load-more-action = Load more
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-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-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...
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
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
index 408ff25..df71e24 100644
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -1,575 +1,580 @@
/*
* 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 <lager/setter.hpp>
#include <nlohmann/json.hpp>
#include <event.hpp>
#include <QFile>
#include <QMimeDatabase>
#include "kazv-log.hpp"
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
#include "matrix-room-pinned-events-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "kazv-markdown.hpp"
#include "qt-json.hpp"
#include "qfunctionutils.hpp"
#include "helper.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const int typingDebounceMs = 500;
static const int saveDraftDebounceMs = 5000;
static const int typingTimeoutMs = 1000;
MatrixRoom::MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, lager::reader<Kazv::Event> userGivenNicknameEvent, QObject *parent)
: QObject(parent)
, m_room(room)
, m_userGivenNicknameEvent(userGivenNicknameEvent)
, m_selfUserId(selfUserId)
, m_memberNames(m_room.members())
, m_powerLevels(m_room.powerLevels())
, LAGER_QT(roomId)(m_room.roomId().xform(strToQt))
, LAGER_QT(name)(m_room.nameOpt()[lager::lenses::or_default].xform(strToQt))
, LAGER_QT(topic)(m_room.topic().xform(strToQt))
, LAGER_QT(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
, LAGER_QT(heroEvents)(m_room.heroMemberEvents()
.map([](const auto &events) {
auto res = QList<QVariant>{};
res.reserve(events.size());
std::transform(events.begin(), events.end(),
std::back_inserter(res),
[](const auto &event) -> QVariant {
return event.originalJson().get().template get<QJsonObject>();
});
return QVariant(res);
}))
, LAGER_QT(avatarMxcUri)(m_room.avatarMxcUri().xform(strToQt))
, LAGER_QT(roomOrHeroAvatarMxcUri)(lager::with(m_room.heroMemberEvents(), m_room.avatarMxcUri())
.map([](const auto &heroes, const auto &roomAvatar) {
if (!roomAvatar.empty()) {
return roomAvatar;
}
if (heroes.size() == 1) {
return zug::reduce(roomMemberToAvatar(zug::last), std::string(), heroes);
}
return std::string();
})
.xform(strToQt))
, LAGER_QT(localDraft)(m_room.localDraft().xform(strToQt))
, LAGER_QT(encrypted)(m_room.encrypted())
, LAGER_QT(memberNames)(m_memberNames.xform(strListToQt))
, LAGER_QT(tagIds)(m_room.tags().map([](const auto &tagsMap) {
return zug::into(
QStringList(),
zug::map([](const auto pair) {
return QString::fromStdString(pair.first);
}),
tagsMap);
}))
, LAGER_QT(membership)(m_room.membership().map([](const auto &membership) { return static_cast<Membership>(membership); }))
, LAGER_QT(unreadNotificationCount)(m_room.unreadNotificationEventIds().map([](const auto &cont) -> int { return cont.size(); }))
, m_setTypingThrottled(QFunctionUtils::Throttle([self=QPointer<MatrixRoom>(this)]() {
if (self) {
self->setTypingImpl();
}
}, typingDebounceMs))
, m_updateLocalDraftDebounced(QFunctionUtils::Debounce([self=QPointer<MatrixRoom>(this)]() {
if (self) {
self->updateLocalDraftNow();
}
}, saveDraftDebounceMs))
, m_internalLocalDraft(std::nullopt)
{
lager::watch(m_powerLevels, [this](auto &&) {
powerLevelsChanged();
});
}
MatrixRoom::~MatrixRoom() {
updateLocalDraftNow();
}
MatrixRoomTimeline *MatrixRoom::timeline() const
{
return new MatrixRoomTimeline(m_room);
}
MatrixRoomPinnedEventsTimeline *MatrixRoom::pinnedEventsTimeline() const
{
return new MatrixRoomPinnedEventsTimeline(m_room);
}
MatrixPromise *MatrixRoom::pinEvents(const QStringList &eventIds) const
{
return new MatrixPromise(m_room.pinEvents(intoImmer(
immer::flex_vector<std::string>(),
qStringToStd,
eventIds)));
}
MatrixPromise *MatrixRoom::unpinEvents(const QStringList &eventIds) const
{
return new MatrixPromise(m_room.unpinEvents(intoImmer(
immer::flex_vector<std::string>(),
qStringToStd,
eventIds)));
}
static void maybeAddRelations(nlohmann::json &msg, const QString &relType, const QString &relatedTo)
{
if (relatedTo.isEmpty()) {
return;
}
if (relType == u"m.in_reply_to"_s) {
msg["content"]["m.relates_to"] = nlohmann::json{
{"m.in_reply_to", {
{"event_id", relatedTo},
}},
};
} else {
msg["content"]["m.relates_to"] = nlohmann::json{
{"rel_type", relType},
{"event_id", relatedTo},
};
}
}
static const std::string HTML_FORMAT = "org.matrix.custom.html";
nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Event replyToEvent)
{
auto msg = nlohmann::json{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text},
}},
};
std::string replyToBody;
if (relType == u"m.in_reply_to"_s && !relatedTo.isEmpty()) {
auto replyToContent = replyToEvent.content().get();
if (replyToContent.contains("format")
&& replyToContent["format"] == HTML_FORMAT
&& replyToContent.contains("formatted_body")
&& replyToContent["formatted_body"].is_string()) {
replyToBody = replyToContent["formatted_body"].template get<std::string>();
auto replyPos = replyToBody.find("</mx-reply>");
if (replyPos != std::string::npos) {
replyToBody.erase(0, replyPos + std::string("</mx-reply>").size());
}
} else if (replyToContent.contains("body") && replyToContent["body"].is_string()) {
replyToBody = markdownToHtml(replyToContent["body"].template get<std::string>()).html;
}
if (!replyToBody.empty()) {
replyToBody = "<mx-reply><blockquote>" + replyToBody + "</blockquote></mx-reply>";
}
}
auto richText = markdownToHtml(text.toStdString());
msg["content"]["format"] = HTML_FORMAT;
msg["content"]["formatted_body"] = replyToBody + richText.html;
auto mentions = richText.mentions;
if (!replyToEvent.sender().empty()
&& std::find(mentions.begin(), mentions.end(), replyToEvent.sender()) == mentions.end()) {
mentions = std::move(mentions).push_back(replyToEvent.sender());
}
if (!mentions.empty()) {
msg["content"]["m.mentions"] = {
{"user_ids", mentions},
};
}
if (relType == u"m.replace"_s && !relatedTo.isEmpty()) {
msg["content"]["m.new_content"] = msg["content"];
}
maybeAddRelations(msg, relType, relatedTo);
return msg;
}
void MatrixRoom::sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const
{
auto msg = nlohmann::json(eventJson);
maybeAddRelations(msg, relType, relatedTo);
m_room.sendMessage(Event(msg));
}
void MatrixRoom::sendTextMessage(QString text, const QString &relType, QString relatedTo) const
{
Event replyToEvent = (relType == u"m.in_reply_to"_s && !relatedTo.isEmpty())
? m_room.message(lager::make_constant(relatedTo.toStdString())).make().get()
: Event();
auto j = makeTextMessageJson(text, relType, relatedTo, replyToEvent);
m_room.sendMessage(Event(j));
}
void MatrixRoom::sendMediaFileMessage(
QString fileName, QString mimeType, qint64 fileSize, QString mxcUri,
const QString &relType, const QString &relatedTo) const
{
auto j = makeMediaFileMessageJson(
fileName, mimeType, fileSize, mxcUri, relType, relatedTo);
Kazv::Event e{j};
m_room.sendMessage(e);
}
void MatrixRoom::sendEncryptedFileMessage(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
const QString &key, const QString &iv, const QByteArray &hash,
const QString &relType, const QString &relatedTo) const
{
auto j = makeEncryptedFileMessageJson(
fileName, mimeType, fileSize, mxcUri,
key, iv, hash, relType, relatedTo);
Kazv::Event e{j};
m_room.sendMessage(e);
}
static nlohmann::json makeReactionJson(QString text, QString relatedTo)
{
auto msg = nlohmann::json{
{"type", "m.reaction"},
{"content", {
{"m.relates_to", {
{"rel_type", "m.annotation"},
{"event_id", relatedTo.toStdString()},
{"key", text.toStdString()},
}},
}},
};
return msg;
}
void MatrixRoom::sendReaction(QString text, QString relatedTo) const
{
auto j = makeReactionJson(text, relatedTo);
m_room.sendMessage(Event(j));
}
void MatrixRoom::resendMessage(QString txnId) const
{
m_room.resendMessage(txnId.toStdString());
}
MatrixPromise *MatrixRoom::sendStateEvent(const QJsonObject &eventJson) const
{
return new MatrixPromise(m_room.sendStateEvent(Event(json(eventJson))));
}
MatrixPromise *MatrixRoom::setSelfName(const QString &name) const
{
auto eventJson = m_room.state({"m.room.member", m_selfUserId.get()}).make().get().originalJson().get();
eventJson.merge_patch(json{
{"content", {
{"displayname", name.toStdString()},
}},
});
return new MatrixPromise(m_room.sendStateEvent(Event(json(eventJson))));
}
nlohmann::json MatrixRoom::makeMediaFileMessageJson(
QString fileName, QString mimeType, qint64 fileSize, QString mxcUri,
const QString &relType, const QString &relatedTo) const
{
static auto available_msgtype = std::array<std::string, 3>{"m.audio", "m.video", "m.image"};
auto try_msgtype = std::find(available_msgtype.begin(), available_msgtype.end(),
u"m."_s.append(mimeType.split(u'/')[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
auto msg = nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"url", mxcUri.toStdString()},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
maybeAddRelations(msg, relType, relatedTo);
return msg;
}
nlohmann::json MatrixRoom::makeEncryptedFileMessageJson(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
const QString &key, const QString &iv, const QByteArray &hash,
const QString &relType, const QString &relatedTo) const
{
static auto available_msgtype = std::array<std::string, 3>{"m.audio", "m.video", "m.image"};
auto try_msgtype = std::find(available_msgtype.begin(), available_msgtype.end(),
u"m."_s.append(mimeType.split(u'/')[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
auto msg = nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"file", {
{"url", mxcUri.toStdString()},
{"key", {
{"kty", "oct"},
{"key_ops", nlohmann::json::array({"encrypt", "decrypt"})},
{"alg", "A256CTR"},
{"k", key.toStdString()},
{"ext", true}
}},
{"iv", iv.toStdString()},
{"hashes", {
{"sha256", hash.toStdString()}
}},
{"v", "v2"}
}},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
maybeAddRelations(msg, relType, relatedTo);
return msg;
}
MatrixPromise *MatrixRoom::redactEvent(QString eventId, QString reason) const
{
qCInfo(kazvLog) << "redactEvent(" << eventId << ", " << reason << ")";
return new MatrixPromise(m_room.redactEvent(
eventId.toStdString(),
reason.isEmpty() ? std::nullopt : std::optional<std::string>(reason.toStdString())
));
}
MatrixPromise *MatrixRoom::removeLocalEcho(QString txnId) const
{
return new MatrixPromise(m_room.removeLocalEcho(txnId.toStdString()));
}
MatrixPromise *MatrixRoom::addOrSetTag(QString tagId) const
{
return new MatrixPromise(m_room.addOrSetTag(tagId.toStdString()));
}
MatrixPromise *MatrixRoom::removeTag(QString tagId) const
{
return new MatrixPromise(m_room.removeTag(tagId.toStdString()));
}
MatrixPromise *MatrixRoom::paginateBackFrom(QString eventId) const
{
return new MatrixPromise(m_room.paginateBackFromEvent(eventId.toStdString()));
}
MatrixPromise *MatrixRoom::leaveRoom() const
{
return new MatrixPromise(m_room.leave());
}
MatrixPromise *MatrixRoom::forgetRoom() const
{
return new MatrixPromise(m_room.forget());
}
MatrixRoomMember *MatrixRoom::memberAt(int index) const
{
return new MatrixRoomMember(m_room.memberEventByCursor(
m_memberNames[index][lager::lenses::or_default]));
}
MatrixRoomMember *MatrixRoom::member(QString userId) const
{
return new MatrixRoomMember(m_room.memberEventFor(userId.toStdString()));
}
MatrixEvent *MatrixRoom::state(const QString &type, const QString &stateKey) const
{
return new MatrixEvent(m_room.state({type.toStdString(), stateKey.toStdString()}));
}
MatrixEvent *MatrixRoom::messageById(QString eventId) const
{
return new MatrixEvent(
m_room.message(lager::make_constant(eventId.toStdString()))
.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
m_room
);
}
MatrixEvent *MatrixRoom::localEchoById(QString txnId) const
{
return new MatrixEvent(
m_room.localEcho(lager::make_constant(txnId.toStdString()))
.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
m_room
);
}
MatrixRoomMemberListModel *MatrixRoom::typingUsers() const
{
return new MatrixRoomMemberListModel(
m_room.typingMemberEvents()
.xform(containerMap(EventList{}, zug::filter([self=m_selfUserId.make().get()](const auto &e) {
return e.stateKey() != self;
})))
);
}
void MatrixRoom::setTyping(bool typing)
{
if (typing) {
m_setTypingThrottled();
} else {
m_room.setTyping(false, std::nullopt);
}
}
void MatrixRoom::setTypingImpl()
{
m_room.setTyping(true, typingTimeoutMs);
}
void MatrixRoom::setLocalDraft(QString localDraft)
{
// To avoid heavy computations when updating the local draft again
// and again, we only set our internal state. **Assume only
// one MatrixRoom can deal with the local draft** at the same time,
// it is safe to update it debounced and when we are destructed.
// See also the destructor.
m_internalLocalDraft = localDraft;
m_updateLocalDraftDebounced();
}
void MatrixRoom::updateLocalDraftNow()
{
if (m_internalLocalDraft.has_value()) {
m_room.setLocalDraft(m_internalLocalDraft.value().toStdString());
m_internalLocalDraft = std::nullopt;
}
}
MatrixRoomMemberListModel *MatrixRoom::members() const
{
return new MatrixRoomMemberListModel(
m_room.joinedMemberEvents(),
QString(),
m_userGivenNicknameEvent
);
}
MatrixRoomMemberListModel *MatrixRoom::bannedMembers() const
{
return new MatrixRoomMemberListModel(
m_room.bannedMemberEvents(),
QString(),
m_userGivenNicknameEvent
);
}
qint64 MatrixRoom::userPowerLevel(const QString &userId) const
{
auto e = m_powerLevels.get().normalizedEvent().content().get();
auto userIdStd = userId.toStdString();
return e.at("users").contains(userIdStd)
? e.at("users")[userIdStd].template get<Kazv::PowerLevel>()
: e.at("users_default").template get<Kazv::PowerLevel>();
}
MatrixPromise *MatrixRoom::setUserPowerLevel(const QString &userId, qint64 powerLevel) const
{
auto next = m_powerLevels.get().setUser(userId.toStdString(), powerLevel);
return new MatrixPromise(m_room.sendStateEvent(next.originalEvent()));
}
MatrixPromise *MatrixRoom::unsetUserPowerLevel(const QString &userId) const
{
auto next = m_powerLevels.get().setUser(userId.toStdString(), std::nullopt);
return new MatrixPromise(m_room.sendStateEvent(next.originalEvent()));
}
MatrixPromise *MatrixRoom::getStateEvent(const QString &type, const QString &stateKey) const
{
return new MatrixPromise(m_room.getStateEvent(
type.toStdString(), stateKey.toStdString()
));
}
MatrixPromise *MatrixRoom::ensureStateEvent(const QString &type, const QString &stateKey) const
{
if (m_room
.stateOpt({type.toStdString(), stateKey.toStdString()})
.make()
.get()
.has_value()) {
return MatrixPromise::createResolved(true, QJsonObject());
} else {
return getStateEvent(type, stateKey);
}
}
MatrixPromise *MatrixRoom::refreshState() const
{
return new MatrixPromise(m_room.refreshRoomState());
}
MatrixPromise *MatrixRoom::kickUser(const QString &userId, const QString &reason) const
{
return new MatrixPromise(m_room.kick(userId.toStdString(), reason.toStdString()));
}
MatrixPromise *MatrixRoom::banUser(const QString &userId, const QString &reason) const
{
return new MatrixPromise(m_room.ban(userId.toStdString(), reason.toStdString()));
}
MatrixPromise *MatrixRoom::unbanUser(const QString &userId) const
{
return new MatrixPromise(m_room.unban(userId.toStdString()));
}
MatrixPromise *MatrixRoom::inviteUser(const QString &userId) const
{
return new MatrixPromise(m_room.invite(userId.toStdString()));
}
MatrixPromise *MatrixRoom::postReadReceipt(const QString &eventId) const
{
return new MatrixPromise(m_room.postReceipt(eventId.toStdString()));
}
+MatrixPromise *MatrixRoom::setName(const QString &newName) const
+{
+ return new MatrixPromise(m_room.setName(newName.toStdString()));
+}
+
MatrixPromise *MatrixRoom::setTopic(const QString &newTopic) const
{
return new MatrixPromise(m_room.setTopic(newTopic.toStdString()));
}
MatrixStickerPackList *MatrixRoom::stickerPackList() const
{
return new MatrixStickerPackList(m_room);
}
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
index b8dad4e..8cca82c 100644
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -1,185 +1,187 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
#include <QTimer>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
Q_MOC_INCLUDE("matrix-room-timeline.hpp")
Q_MOC_INCLUDE("matrix-room-pinned-events-timeline.hpp")
Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
class MatrixRoomTimeline;
class MatrixRoomPinnedEventsTimeline;
class MatrixRoomMember;
class MatrixPromise;
class MatrixRoomMemberListModel;
class MatrixEvent;
class MatrixStickerPackList;
nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Kazv::Event replyToEvent);
class MatrixRoom : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Room m_room;
lager::reader<Kazv::Event> m_userGivenNicknameEvent;
lager::reader<std::string> m_selfUserId;
lager::reader<immer::flex_vector<std::string>> m_memberNames;
lager::reader<Kazv::PowerLevelsDesc> m_powerLevels;
public:
enum Membership {
Invite = Kazv::RoomMembership::Invite,
Join = Kazv::RoomMembership::Join,
Leave = Kazv::RoomMembership::Leave,
};
Q_ENUM(Membership);
explicit MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, lager::reader<Kazv::Event> userGivenNicknameEvent = lager::make_constant(Kazv::Event()), QObject *parent = 0);
~MatrixRoom() override;
LAGER_QT_READER(QString, roomId);
LAGER_QT_READER(QString, name);
LAGER_QT_READER(QString, topic);
LAGER_QT_READER(QStringList, heroNames);
LAGER_QT_READER(QVariant, heroEvents);
LAGER_QT_READER(QString, avatarMxcUri);
LAGER_QT_READER(QString, roomOrHeroAvatarMxcUri);
LAGER_QT_READER(QString, localDraft);
LAGER_QT_READER(bool, encrypted);
LAGER_QT_READER(QStringList, memberNames);
LAGER_QT_READER(QStringList, tagIds);
LAGER_QT_READER(Membership, membership);
LAGER_QT_READER(int, unreadNotificationCount);
Q_INVOKABLE MatrixRoomMember *memberAt(int index) const;
Q_INVOKABLE MatrixRoomMember *member(QString userId) const;
Q_INVOKABLE MatrixEvent *state(const QString &type, const QString &stateKey) const;
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
Q_INVOKABLE MatrixRoomPinnedEventsTimeline *pinnedEventsTimeline() const;
Q_INVOKABLE MatrixPromise *pinEvents(const QStringList &eventIds) const;
Q_INVOKABLE MatrixPromise *unpinEvents(const QStringList &eventIds) const;
Q_INVOKABLE MatrixEvent *messageById(QString eventId) const;
Q_INVOKABLE MatrixEvent *localEchoById(QString txnId) const;
Q_INVOKABLE void sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendTextMessage(QString text, const QString &relType, QString relatedTo) const;
Q_INVOKABLE void sendMediaFileMessage(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri,
const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendEncryptedFileMessage(const QString &fileName, const QString& mimeType,
const qint64 fileSize, const QString& mxcUri,
const QString &key, const QString &iv, const QByteArray &hash,
const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendReaction(QString text, QString relatedTo) const;
Q_INVOKABLE void resendMessage(QString txnId) const;
Q_INVOKABLE MatrixPromise *sendStateEvent(const QJsonObject &eventJson) const;
Q_INVOKABLE MatrixPromise *setSelfName(const QString &name) const;
Q_INVOKABLE MatrixPromise *redactEvent(QString eventId, QString reason) const;
Q_INVOKABLE MatrixPromise *removeLocalEcho(QString txnId) const;
Q_INVOKABLE MatrixRoomMemberListModel *typingUsers() const;
Q_INVOKABLE void setTyping(bool typing);
Q_INVOKABLE void setLocalDraft(QString localDraft);
Q_INVOKABLE void updateLocalDraftNow();
Q_INVOKABLE MatrixPromise *addOrSetTag(QString tagId) const;
Q_INVOKABLE MatrixPromise *removeTag(QString tagId) const;
Q_INVOKABLE MatrixPromise *paginateBackFrom(QString eventId) const;
Q_INVOKABLE MatrixPromise *leaveRoom() const;
Q_INVOKABLE MatrixPromise *forgetRoom() const;
Q_INVOKABLE MatrixRoomMemberListModel *members() const;
Q_INVOKABLE MatrixRoomMemberListModel *bannedMembers() const;
Q_INVOKABLE qint64 userPowerLevel(const QString &userId) const;
Q_INVOKABLE MatrixPromise *setUserPowerLevel(const QString &userId, qint64 powerLevel) const;
Q_INVOKABLE MatrixPromise *unsetUserPowerLevel(const QString &userId) const;
Q_INVOKABLE MatrixPromise *getStateEvent(const QString &type, const QString &stateKey) const;
Q_INVOKABLE MatrixPromise *ensureStateEvent(const QString &type, const QString &stateKey) const;
Q_INVOKABLE MatrixPromise *refreshState() const;
Q_INVOKABLE MatrixPromise *kickUser(const QString &userId, const QString &reason) const;
Q_INVOKABLE MatrixPromise *banUser(const QString &userId, const QString &reason = QStringLiteral()) const;
Q_INVOKABLE MatrixPromise *unbanUser(const QString &userId) const;
Q_INVOKABLE MatrixPromise *inviteUser(const QString &userId) const;
Q_INVOKABLE MatrixPromise *postReadReceipt(const QString &eventId) const;
+ Q_INVOKABLE MatrixPromise *setName(const QString &newName) const;
+
Q_INVOKABLE MatrixPromise *setTopic(const QString &newTopic) const;
/**
* Get the sticker pack list defined in this room.
*
* @return A list of sticker packs defined in this room.
*/
Q_INVOKABLE MatrixStickerPackList *stickerPackList() const;
Q_SIGNALS:
void powerLevelsChanged();
protected:
nlohmann::json makeMediaFileMessageJson(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri,
const QString &relType, const QString &relatedTo) const;
nlohmann::json makeEncryptedFileMessageJson(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
const QString &key, const QString &iv, const QByteArray &hash,
const QString &relType, const QString &relatedTo) const;
private Q_SLOTS:
void setTypingImpl();
private:
std::function<void()> m_setTypingThrottled;
std::function<void()> m_updateLocalDraftDebounced;
std::optional<QString> m_internalLocalDraft;
};
diff --git a/src/tests/quick-tests/tst_RoomSettingsPage.qml b/src/tests/quick-tests/tst_RoomSettingsPage.qml
index 1a87df6..f416b04 100644
--- a/src/tests/quick-tests/tst_RoomSettingsPage.qml
+++ b/src/tests/quick-tests/tst_RoomSettingsPage.qml
@@ -1,288 +1,346 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import '../../contents/ui/room-settings' as KazvRS
import moe.kazv.mxc.kazv 0.0 as MK
import 'test-helpers.js' as Helpers
import 'test-helpers' as TestHelpers
Item {
id: item
width: 800
height: 600
property var mockHelper: TestHelpers.MockHelper {}
property var roomUnencrypted: Helpers.factory.room({
membership: MK.MatrixRoom.Join,
encrypted: false,
sendStateEvent: mockHelper.promise(),
})
property var roomEncrypted: Helpers.factory.room({
membership: MK.MatrixRoom.Join,
encrypted: true,
})
property var roomLeft: Helpers.factory.room({
membership: MK.MatrixRoom.Leave,
forgetRoom: mockHelper.promise(),
})
property var roomJoined: Helpers.factory.room({
membership: MK.MatrixRoom.Join,
leaveRoom: mockHelper.promise(),
topic: '',
})
property var roomWithMembers: Helpers.factory.room({
name: 'some name',
roomId: '!someid:example.com',
members: () => ({
at(index) {
return {
userId: `@u${index}:example.com`,
name: `user ${index}`,
};
},
count: 5
}),
+ setName: mockHelper.promise(),
})
property var roomWithoutName: Helpers.factory.room({
name: '',
roomId: '!someid:example.com',
})
property var roomWithTopic: Helpers.factory.room({
membership: MK.MatrixRoom.Join,
topic: 'some topic',
setTopic: mockHelper.promise(),
})
property var showPassiveNotification: mockHelper.noop()
property var l10n: Helpers.fluentMock
property var matrixSdk: TestHelpers.MatrixSdkMock {
property var userId: '@foo:example.org'
}
property var sdkVars: ({})
ColumnLayout {
KazvRS.RoomSettingsPage {
id: pageUnencrypted
room: item.roomUnencrypted
}
KazvRS.RoomSettingsPage {
id: pageEncrypted
room: item.roomEncrypted
}
KazvRS.RoomSettingsPage {
id: pageLeft
room: item.roomLeft
}
KazvRS.RoomSettingsPage {
id: pageJoined
room: item.roomJoined
}
KazvRS.RoomSettingsPage {
id: pageWithMembers
room: item.roomWithMembers
}
KazvRS.RoomSettingsPage {
id: pageWithoutName
room: item.roomWithoutName
}
KazvRS.RoomSettingsPage {
id: pageWithTopic
room: item.roomWithTopic
}
}
TestCase {
id: roomSettingsPageTest
name: 'RoomSettingsPageTest'
when: windowShown
function initTestCase() {
pageJoined.contentItem.clip = false;
pageWithTopic.contentItem.clip = false;
+ pageWithMembers.contentItem.clip = false;
}
function init() {
pageJoined.submittingTopic = false;
pageJoined.editingTopic = false;
pageWithTopic.submittingTopic = false;
pageWithTopic.editingTopic = false;
+ pageWithMembers.submittingName = false;
+ pageWithMembers.editingName = false;
mockHelper.clearAll();
}
function test_encryptionIndicator() {
compare(findChild(pageUnencrypted, 'encryptionIndicator').text, l10n.get('room-settings-not-encrypted'));
verify(findChild(pageUnencrypted, 'enableEncryptionButton').visible);
compare(findChild(pageEncrypted, 'encryptionIndicator').text, l10n.get('room-settings-encrypted'));
verify(!findChild(pageEncrypted, 'enableEncryptionButton').visible);
}
function test_enableEncryption() {
pageUnencrypted.encryptionPopup.accepted();
tryVerify(() => roomUnencrypted.sendStateEvent.lastArgs()[0].type === 'm.room.encryption');
tryVerify(() => roomUnencrypted.sendStateEvent.lastArgs()[0].state_key === '');
verify(!findChild(pageUnencrypted, 'enableEncryptionButton').enabled);
roomUnencrypted.sendStateEvent.lastRetVal().resolve(true, {});
tryVerify(() => findChild(pageUnencrypted, 'enableEncryptionButton').enabled);
}
function test_forgetRoom() {
const forgetRoomButton = findChild(pageLeft, 'forgetRoomButton');
const confirmPopup = findChild(pageLeft, 'confirmForgetRoomPopup');
verify(forgetRoomButton.visible);
verify(!findChild(pageJoined, 'forgetRoomButton').visible);
confirmPopup.accepted();
tryVerify(() => roomLeft.forgetRoom.calledTimes() === 1);
roomLeft.forgetRoom.lastRetVal().resolve(true, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_forgetRoomFailure() {
const confirmPopup = findChild(pageLeft, 'confirmForgetRoomPopup');
confirmPopup.accepted();
roomLeft.forgetRoom.lastRetVal().resolve(false, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_leaveRoom() {
const leaveRoomButton = findChild(pageJoined, 'leaveRoomButton');
const confirmPopup = findChild(pageJoined, 'confirmLeaveRoomPopup');
verify(leaveRoomButton.visible);
confirmPopup.accepted();
tryVerify(() => roomJoined.leaveRoom.calledTimes() === 1);
roomJoined.leaveRoom.lastRetVal().resolve(true, {});
console.log(roomJoined.membership);
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_leaveRoomFailure() {
const confirmPopup = findChild(pageJoined, 'confirmLeaveRoomPopup');
confirmPopup.accepted();
roomJoined.leaveRoom.lastRetVal().resolve(false, {});
tryVerify(() => item.showPassiveNotification.calledTimes() === 1);
}
function test_roomMembersAvatars() {
const repeater = findChild(pageWithMembers, 'roomMembersAvatarsRepeater');
tryVerify(() => repeater.count === 4);
const more = findChild(pageWithMembers, 'roomMembersAvatarsMore');
verify(more.visible);
}
function test_roomWithName() {
const nameLabel = findChild(pageWithMembers, 'roomNameLabel');
const idLabel = findChild(pageWithMembers, 'roomIdLabel');
verify(nameLabel.text === 'some name');
verify(idLabel.text === '!someid:example.com');
- verify(idLabel.visible);
}
function test_roomWithoutName() {
const nameLabel = findChild(pageWithoutName, 'roomNameLabel');
- const idLabel = findChild(pageWithoutName, 'roomIdLabel');
- verify(nameLabel.text === '!someid:example.com');
- verify(!idLabel.visible)
+ const idLabel = findChild(pageWithMembers, 'roomIdLabel');
+ compare(nameLabel.text, l10n.get('room-settings-name-missing'));
+ compare(idLabel.text, '!someid:example.com');
}
function test_roomWithoutTopic() {
const topicLabel = findChild(pageJoined, 'roomTopicLabel');
verify(topicLabel.font.italic);
compare(topicLabel.text, l10n.get('room-settings-topic-missing'));
}
function test_roomWithTopic() {
const topicLabel = findChild(pageWithTopic, 'roomTopicLabel');
verify(!topicLabel.font.italic);
compare(topicLabel.text, 'some topic');
}
function test_editRoomTopic() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
verify(saveTopicButton.visible);
verify(saveTopicButton.enabled);
const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
verify(discardTopicButton.visible);
verify(discardTopicButton.enabled);
mouseClick(saveTopicButton);
tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
compare(roomWithTopic.setTopic.lastArgs()[0], 'other topic');
verify(!saveTopicButton.enabled);
verify(!discardTopicButton.enabled);
roomWithTopic.setTopic.lastRetVal().resolve(true, {});
tryVerify(() => editButton.visible);
verify(!saveTopicButton.visible);
}
function test_editRoomTopicFailed() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
mouseClick(saveTopicButton);
tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
roomWithTopic.setTopic.lastRetVal().resolve(false, {});
verify(saveTopicButton.enabled);
verify(textArea.visible);
}
function test_editRoomTopicDiscard() {
const editButton = findChild(pageWithTopic, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageWithTopic, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
textArea.text = 'other topic';
const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
mouseClick(discardTopicButton);
compare(roomWithTopic.setTopic.calledTimes(), 0);
verify(!textArea.visible);
mouseClick(editButton);
tryVerify(() => textArea.visible);
verify(textArea.text === 'some topic');
}
function test_addRoomTopic() {
const editButton = findChild(pageJoined, 'editTopicButton');
mouseClick(editButton);
const textArea = findChild(pageJoined, 'roomTopicEdit');
tryVerify(() => textArea.visible);
verify(textArea.text === '');
}
+
+ function test_editRoomName() {
+ const editButton = findChild(pageWithMembers, 'editNameButton');
+ mouseClick(editButton);
+ const textArea = findChild(pageWithMembers, 'roomNameEdit');
+ tryVerify(() => textArea.visible);
+ verify(textArea.text === 'some name');
+ textArea.text = 'other name';
+ const saveNameButton = findChild(pageWithMembers, 'saveNameButton');
+ verify(saveNameButton.visible);
+ verify(saveNameButton.enabled);
+ const discardNameButton = findChild(pageWithMembers, 'discardNameButton');
+ verify(discardNameButton.visible);
+ verify(discardNameButton.enabled);
+ mouseClick(saveNameButton);
+ tryVerify(() => roomWithMembers.setName.calledTimes() === 1);
+ compare(roomWithMembers.setName.lastArgs()[0], 'other name');
+ verify(!saveNameButton.enabled);
+ verify(!discardNameButton.enabled);
+
+ roomWithMembers.setName.lastRetVal().resolve(true, {});
+ tryVerify(() => editButton.visible);
+ verify(!saveNameButton.visible);
+ }
+
+ function test_editRoomNameFailed() {
+ const editButton = findChild(pageWithMembers, 'editNameButton');
+ mouseClick(editButton);
+ const textArea = findChild(pageWithMembers, 'roomNameEdit');
+ tryVerify(() => textArea.visible);
+ verify(textArea.text === 'some name');
+ textArea.text = 'other name';
+ const saveNameButton = findChild(pageWithMembers, 'saveNameButton');
+ mouseClick(saveNameButton);
+ tryVerify(() => roomWithMembers.setName.calledTimes() === 1);
+ roomWithMembers.setName.lastRetVal().resolve(false, {});
+ verify(saveNameButton.enabled);
+ verify(textArea.visible);
+ }
+
+ function test_editRoomNameDiscard() {
+ const editButton = findChild(pageWithMembers, 'editNameButton');
+ mouseClick(editButton);
+ const textArea = findChild(pageWithMembers, 'roomNameEdit');
+ tryVerify(() => textArea.visible);
+ verify(textArea.text === 'some name');
+ textArea.text = 'other name';
+ const discardNameButton = findChild(pageWithMembers, 'discardNameButton');
+ mouseClick(discardNameButton);
+ compare(roomWithMembers.setName.calledTimes(), 0);
+ verify(!textArea.visible);
+ mouseClick(editButton);
+ tryVerify(() => textArea.visible);
+ verify(textArea.text === 'some name');
+ }
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 3:19 PM (20 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55263
Default Alt Text
(94 KB)

Event Timeline