Page MenuHomePhorge

No OneTemporary

Size
70 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index d664e8e..cb12c93 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,188 +1,215 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
ColumnLayout {
id: sendMessageBox
property var room
property var draftReplyTo: ''
property var timeline: room.timeline()
onRoomChanged: {
textArea.changeText(room.localDraft, true);
}
ColumnLayout {
visible: !!sendMessageBox.draftReplyTo
RowLayout {
Label {
Layout.fillWidth: true
text: l10n.get('send-message-box-reply-to')
}
ToolButton {
action: removeReplyToAction
display: AbstractButton.IconOnly
}
}
Item {
Layout.minimumHeight: Math.min(replyToEventView.implicitHeight, Kirigami.Units.gridUnit * 5)
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
Layout.fillWidth: true
clip: true
Kazv.EventViewWrapper {
id: replyToEventView
anchors {
top: parent.top
left: parent.left
right: parent.right
}
event: room.messageById(sendMessageBox.draftReplyTo)
compactMode: true
}
}
}
RowLayout {
TextArea {
id: textArea
objectName: 'draftMessage'
property var shortcutList: ["Ctrl+Return", "Ctrl+Enter"]
property var inhibitTyping: false
placeholderText: l10n.get('send-message-box-input-placeholder')
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
onTextChanged: {
room.setLocalDraft(text);
if (!inhibitTyping) {
room.setTyping(true);
}
inhibitTyping = false;
}
function changeText(newText, inhibitTyping) {
textArea.inhibitTyping = inhibitTyping;
textArea.text = newText;
}
onVisibleChanged: {
if (!visible) {
room.updateLocalDraftNow();
}
}
// Shortcut keys for sending messages (TODO: Shortcut keys customized by the user)
Shortcut {
sequences: textArea.shortcutList
onActivated: {
sendAction.trigger()
}
}
DropArea {
anchors.fill: parent
onDropped: (drag) => {
if (drag.hasUrls) {
confirmUploadPopup.call(drag.urls);
}
}
}
}
ToolButton {
icon.name: "document-send-symbolic"
onClicked: sendMediaFileAction.trigger()
}
+ ToolButton {
+ action: stickersAction
+ display: AbstractButton.IconOnly
+ }
+
ToolButton {
icon.name: "document-send"
onClicked: sendAction.trigger()
}
}
function mentionUser(userId) {
const prefix = textArea.text.slice(0, textArea.cursorPosition);
const suffix = textArea.text.slice(textArea.cursorPosition);
let insertion = userId;
// If we are not at the beginning and the prev char is not a space,
// insert a space there.
if (prefix && prefix.slice(prefix.length - 1).trim() !== '') {
insertion = ' ' + insertion;
}
// If we are at the end or the next char is not a space,
// insert a space there.
if (!suffix || suffix.slice(0, 1).trim() !== '') {
insertion = insertion + ' ';
}
textArea.text = prefix + insertion + suffix;
textArea.cursorPosition = prefix.length + insertion.length;
}
function focusInput() {
textArea.forceActiveFocus();
}
Kirigami.Action {
id: removeReplyToAction
iconName: 'window-close-symbolic'
text: l10n.get('send-message-box-remove-reply-to-action')
onTriggered: {
sendMessageBox.draftReplyTo = '';
}
}
Kirigami.Action {
id: sendAction
iconName: "document-send"
text: l10n.get("send-message-box-send")
onTriggered: {
room.setTyping(false);
room.sendTextMessage(textArea.text, draftReplyTo);
textArea.changeText("", true);
draftReplyTo = '';
}
}
Kirigami.Action {
id:sendMediaFileAction
iconName: "mail-attachment-symbolic"
text: l10n.get("send-message-box-send-file")
onTriggered: {
fileDialog.open()
}
}
property var confirmUploadPopup: ConfirmUploadPopup {
id: confirmUploadPopup
onUploadRequested: (url) => {
sendMessageBox.uploadFile(url);
}
}
property var fileDialog: FileDialog {
onAccepted: {
sendMessageBox.uploadFile(fileUrl);
}
}
function uploadFile(fileUrl) {
kazvIOManager.startNewUploadJob(
matrixSdk.serverUrl, fileUrl, matrixSdk.token,
room.roomId, sdkVars.roomList, room.encrypted
);
}
+
+ property var stickerPopup: Kirigami.OverlaySheet {
+ id: stickerPopup
+ title: l10n.get('send-message-box-stickers-popup-title')
+
+ Kazv.StickerPicker {
+ stickerPackList: matrixSdk.stickerPackList()
+ onSendMessageRequested: eventJson => {
+ console.log(JSON.stringify(eventJson));
+ room.sendMessage(eventJson, draftReplyTo);
+ draftReplyTo = '';
+ stickerPopup.close();
+ }
+ }
+ }
+
+ Kirigami.Action {
+ id: stickersAction
+ iconName: 'smiley'
+ text: l10n.get('send-message-box-stickers')
+ onTriggered: stickerPopup.open()
+ }
}
diff --git a/src/contents/ui/StickerPicker.qml b/src/contents/ui/StickerPicker.qml
new file mode 100644
index 0000000..9be4798
--- /dev/null
+++ b/src/contents/ui/StickerPicker.qml
@@ -0,0 +1,94 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import org.kde.kirigami 2.13 as Kirigami
+import moe.kazv.mxc.kazv 0.0 as MK
+
+import '.' as Kazv
+
+ColumnLayout {
+ id: stickerPicker
+ property var stickerPackList
+ spacing: 0
+
+ signal sendMessageRequested(var eventJson)
+
+ ScrollView {
+ id: packListScrollView
+ Layout.fillWidth: true
+ Layout.minimumHeight: Kirigami.Units.gridUnit * 2
+ Layout.preferredHeight: packListView.height
+
+ ListView {
+ id: packListView
+ orientation: ListView.Horizontal
+ height: contentHeight
+ anchors.fill: parent
+ currentIndex: 0
+
+ model: stickerPackList
+ delegate: ToolButton {
+ objectName: `stickerPack${index}`
+ property var pack: stickerPackList.at(index)
+ icon.name: 'smiley'
+ text: l10n.get('sticker-picker-user-stickers')
+ checked: ListView.currentIndex === index
+ onClicked: ListView.currentIndex = index
+ }
+ }
+ }
+
+ property var currentPack: packListView.currentItem && packListView.currentItem.pack
+
+ property var stickerSize: Kirigami.Units.iconSizes.enormous
+ property var stickerMargin: Kirigami.Units.smallSpacing
+
+ GridView {
+ id: stickersGridView
+ Layout.fillWidth: true
+ Layout.minimumHeight: stickerPicker.stickerSize * 5
+ Layout.preferredHeight: stickersGridView.height
+ model: currentPack
+ cellWidth: stickerPicker.stickerSize + stickerPicker.stickerMargin
+ cellHeight: stickerPicker.stickerSize + stickerPicker.stickerMargin
+ delegate: Item {
+ objectName: `sticker${index}`
+ property var sticker: currentPack.at(index)
+ height: stickerPicker.stickerSize + stickerPicker.stickerMargin
+ width: stickerPicker.stickerSize + stickerPicker.stickerMargin
+
+ Kirigami.Icon {
+ anchors.centerIn: parent
+ height: stickerPicker.stickerSize
+ width: stickerPicker.stickerSize
+ source: matrixSdk.mxcUriToHttp(sticker.mxcUri)
+ }
+
+ Rectangle {
+ visible: hoverHandler.hovered
+ z: -1
+ anchors.fill: parent
+ color: Kirigami.Theme.activeBackgroundColor
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+
+ TapHandler {
+ onTapped: stickerPicker.sendMessageRequested(sticker.makeEventJson())
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ console.log('stickerpicker finished', stickerPackList.count);
+ }
+}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
index c5f8721..66f26c3 100644
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -1,261 +1,265 @@
### 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 }
user-name-with-id = { $name } ({ $userId })
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 = 加入房间
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 }。
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 = 房间
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-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-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 = 留下
send-message-box-input-placeholder = 在此输入您的讯息...
send-message-box-send = 发送
send-message-box-send-file = 发送文件
send-message-box-reply-to = 回复给
send-message-box-remove-reply-to-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 } 邀请到这个房间了。
## 状态事件
## 通用参数:
## 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-file-sent = 发送了文件「{ $body }」。
event-message-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-source-popup-title = 事件源码
event-source-decrypted = 解密的事件源码
event-source-original = 原始的事件源码
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-encrypted = 这条消息已加密
event-msgtype-fallback-text = 未知的消息类型:{ $msgtype }
event-fallback-text = 未知事件:{ $type }
event-cannot-decrypt-text = (加密内容)
media-file-menu-option-view = 查看
media-file-menu-option-save-as = 保存为
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-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 = 取消
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 }。
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 }。
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> 给你发了一条讯息。
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 }。
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 = 取消
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
index 5402036..66d0ce2 100644
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -1,279 +1,283 @@
### 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 }
user-name-with-id = { $name } ({ $userId })
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
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 }.
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
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-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-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
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-remove-reply-to-action = Remove reply-to relationship
+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 }.
## 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-file-sent = sent a file "{ $body }".
event-message-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-source-popup-title = Event source
event-source-decrypted = Decrypted event source
event-source-original = Original event source
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-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)
media-file-menu-option-view = View
media-file-menu-option-save-as = Save as
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-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
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
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 }.
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.
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 }.
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
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
index a5c9b35..1ce5eb1 100644
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -1,444 +1,451 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#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-member.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "kazv-markdown.hpp"
#include "qfunctionutils.hpp"
#include "helper.hpp"
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, QObject *parent)
: QObject(parent)
, m_room(room)
, 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(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
, 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); }))
, 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);
}
static void maybeAddRelations(nlohmann::json &msg, const QString &replyTo)
{
if (!replyTo.isEmpty()) {
msg["content"]["m.relates_to"] = nlohmann::json{
{"m.in_reply_to", {
{"event_id", replyTo},
}},
};
}
}
static const std::string HTML_FORMAT = "org.matrix.custom.html";
nlohmann::json makeTextMessageJson(const QString &text, const QString &replyTo, Event replyToEvent)
{
auto msg = nlohmann::json{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text},
}},
};
std::string replyToBody;
if (!replyTo.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},
};
}
maybeAddRelations(msg, replyTo);
return msg;
}
+void MatrixRoom::sendMessage(const QJsonObject &eventJson, const QString &replyTo) const
+{
+ auto msg = nlohmann::json(eventJson);
+ maybeAddRelations(msg, replyTo);
+ m_room.sendMessage(Event(msg));
+}
+
void MatrixRoom::sendTextMessage(QString text, QString replyTo) const
{
Event replyToEvent = replyTo.isEmpty()
? Event()
: m_room.message(lager::make_constant(replyTo.toStdString())).make().get();
auto j = makeTextMessageJson(text, replyTo, replyToEvent);
m_room.sendMessage(Event(j));
}
void MatrixRoom::sendMediaFileMessage(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) const
{
auto j = makeMediaFileMessageJson(fileName, mimeType, fileSize, mxcUri);
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
{
auto j = makeEncryptedFileMessageJson(fileName, mimeType, fileSize, mxcUri, key, iv, hash);
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());
}
nlohmann::json MatrixRoom::makeMediaFileMessageJson(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) 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(),
QStringLiteral("m.").append(mimeType.split(QChar('/'))[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
return nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"url", mxcUri.toStdString()},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
}
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
{
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(),
QStringLiteral("m.").append(mimeType.split(QChar('/'))[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
return 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()}
}}
}}
};
}
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());
}
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::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; })
);
}
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()
);
}
MatrixRoomMemberListModel *MatrixRoom::bannedMembers() const
{
return new MatrixRoomMemberListModel(
m_room.bannedMemberEvents()
);
}
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::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()));
}
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
index 3716a65..23c1366 100644
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -1,143 +1,145 @@
/*
* 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 <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
#include <QTimer>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
class MatrixRoomTimeline;
class MatrixRoomMember;
class MatrixPromise;
class MatrixRoomMemberListModel;
class MatrixEvent;
nlohmann::json makeTextMessageJson(const QString &text, const QString &replyTo, Kazv::Event replyToEvent);
class MatrixRoom : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Room m_room;
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, QObject *parent = 0);
~MatrixRoom() override;
LAGER_QT_READER(QString, roomId);
LAGER_QT_READER(QString, name);
LAGER_QT_READER(QStringList, heroNames);
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);
Q_INVOKABLE MatrixRoomMember *memberAt(int index) const;
Q_INVOKABLE MatrixRoomMember *member(QString userId) const;
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
Q_INVOKABLE MatrixEvent *messageById(QString eventId) const;
+ Q_INVOKABLE void sendMessage(const QJsonObject &eventJson, const QString &replyTo) const;
+
Q_INVOKABLE void sendTextMessage(QString text, QString replyTo) const;
Q_INVOKABLE void sendMediaFileMessage(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri) 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;
Q_INVOKABLE void sendReaction(QString text, QString relatedTo) const;
Q_INVOKABLE void resendMessage(QString txnId) 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 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 *kickUser(const QString &userId, const QString &reason) const;
Q_INVOKABLE MatrixPromise *banUser(const QString &userId, const QString &reason = "") const;
Q_INVOKABLE MatrixPromise *unbanUser(const QString &userId) const;
Q_INVOKABLE MatrixPromise *inviteUser(const QString &userId) const;
Q_SIGNALS:
void powerLevelsChanged();
protected:
nlohmann::json makeMediaFileMessageJson(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri) 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;
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/matrix-sticker.cpp b/src/matrix-sticker.cpp
index 1d0d2ec..989fceb 100644
--- a/src/matrix-sticker.cpp
+++ b/src/matrix-sticker.cpp
@@ -1,49 +1,61 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp>
#include <base/cursorutil.hpp>
#include "helper.hpp"
#include "qt-json.hpp"
#include "matrix-sticker.hpp"
using namespace Kazv;
MatrixSticker::MatrixSticker(lager::reader<std::string> shortCode, lager::reader<Kazv::json> sticker, QObject *parent)
: QObject(parent)
, m_sticker(sticker)
, LAGER_QT(shortCode)(shortCode.xform(strToQt))
, LAGER_QT(body)(lager::with(shortCode, m_sticker).xform(
zug::map([](const std::string &shortCode, const json &j) {
if (j.contains("body") && j["body"].is_string()) {
return j["body"].template get<std::string>();
} else {
return shortCode;
}
}) | strToQt))
, LAGER_QT(mxcUri)(m_sticker.xform(zug::map([](const json &j) {
if (j.contains("url") && j["url"].is_string()) {
return j["url"].template get<std::string>();
} else {
return std::string();
}
}) | strToQt))
, LAGER_QT(info)(m_sticker.xform(zug::map([](const json &j) {
if (j.contains("info") && j["info"].is_object()) {
return j["info"];
} else {
return json::object();
}
}) | zug::map([](const json &j) -> QJsonObject {
return j.template get<QJsonObject>();
})))
{
}
MatrixSticker::~MatrixSticker() = default;
+
+QJsonObject MatrixSticker::makeEventJson() const
+{
+ return QJsonObject{
+ {"type", "m.sticker"},
+ {"content", QJsonObject{
+ {"url", mxcUri()},
+ {"body", body()},
+ {"info", info()},
+ }},
+ };
+}
diff --git a/src/matrix-sticker.hpp b/src/matrix-sticker.hpp
index 3fd7b0b..0de004d 100644
--- a/src/matrix-sticker.hpp
+++ b/src/matrix-sticker.hpp
@@ -1,36 +1,38 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <immer/config.hpp>
#include <QObject>
#include <QQmlEngine>
#include <QJsonObject>
#include <lager/reader.hpp>
#include <lager/extra/qt.hpp>
#include <base/types.hpp>
class MatrixSticker : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
lager::reader<Kazv::json> m_sticker;
public:
explicit MatrixSticker(lager::reader<std::string> shortCode, lager::reader<Kazv::json> sticker, QObject *parent = 0);
~MatrixSticker() override;
LAGER_QT_READER(QString, shortCode);
LAGER_QT_READER(QString, body);
LAGER_QT_READER(QString, mxcUri);
LAGER_QT_READER(QJsonObject, info);
+
+ Q_INVOKABLE QJsonObject makeEventJson() const;
};
diff --git a/src/resources.qrc b/src/resources.qrc
index dd0bc88..daa30af 100644
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -1,79 +1,80 @@
<RCC>
<qresource prefix="/">
<file alias="main.qml">contents/ui/main.qml</file>
<file alias="LoginPage.qml">contents/ui/LoginPage.qml</file>
<file alias="MainPage.qml">contents/ui/MainPage.qml</file>
<file alias="TabView.qml">contents/ui/TabView.qml</file>
<file alias="Tab.qml">contents/ui/Tab.qml</file>
<file alias="RoomListView.qml">contents/ui/RoomListView.qml</file>
<file alias="RoomListViewItemDelegate.qml">contents/ui/RoomListViewItemDelegate.qml</file>
<file alias="RoomPage.qml">contents/ui/RoomPage.qml</file>
<file alias="RoomTimelineView.qml">contents/ui/RoomTimelineView.qml</file>
<file alias="SendMessageBox.qml">contents/ui/SendMessageBox.qml</file>
<file alias="EventView.qml">contents/ui/EventView.qml</file>
<file alias="EventViewWrapper.qml">contents/ui/EventViewWrapper.qml</file>
<file alias="EventViewCompact.qml">contents/ui/EventViewCompact.qml</file>
<file alias="EventSourceView.qml">contents/ui/EventSourceView.qml</file>
<file alias="Bubble.qml">contents/ui/Bubble.qml</file>
<file alias="MediaFileMenu.qml">contents/ui/MediaFileMenu.qml</file>
<file alias="KazvIOMenu.qml">contents/ui/KazvIOMenu.qml</file>
<file alias="ConfirmUploadPopup.qml">contents/ui/ConfirmUploadPopup.qml</file>
+ <file alias="StickerPicker.qml">contents/ui/StickerPicker.qml</file>
<file alias="ConfirmationOverlay.qml">contents/ui/ConfirmationOverlay.qml</file>
<file alias="event-types/Simple.qml">contents/ui/event-types/Simple.qml</file>
<file alias="event-types/Text.qml">contents/ui/event-types/Text.qml</file>
<file alias="event-types/Emote.qml">contents/ui/event-types/Emote.qml</file>
<file alias="event-types/Notice.qml">contents/ui/event-types/Notice.qml</file>
<file alias="event-types/State.qml">contents/ui/event-types/State.qml</file>
<file alias="event-types/TextTemplate.qml">contents/ui/event-types/TextTemplate.qml</file>
<file alias="event-types/Image.qml">contents/ui/event-types/Image.qml</file>
<file alias="event-types/File.qml">contents/ui/event-types/File.qml</file>
<file alias="event-types/Video.qml">contents/ui/event-types/Video.qml</file>
<file alias="event-types/Audio.qml">contents/ui/event-types/Audio.qml</file>
<file alias="event-types/MediaBubble.qml">contents/ui/event-types/MediaBubble.qml</file>
<file alias="event-types/Redacted.qml">contents/ui/event-types/Redacted.qml</file>
<file alias="event-types/Reaction.qml">contents/ui/event-types/Reaction.qml</file>
<file alias="event-types/Fallback.qml">contents/ui/event-types/Fallback.qml</file>
<file alias="TypingIndicator.qml">contents/ui/TypingIndicator.qml</file>
<file alias="SelectableText.qml">contents/ui/SelectableText.qml</file>
<file alias="ReactToEventPopup.qml">contents/ui/ReactToEventPopup.qml</file>
<file alias="FileHandler.qml">contents/ui/FileHandler.qml</file>
<file alias="ActionSettingsPage.qml">contents/ui/ActionSettingsPage.qml</file>
<file alias="CreateRoomPage.qml">contents/ui/CreateRoomPage.qml</file>
<file alias="JoinRoomPage.qml">contents/ui/JoinRoomPage.qml</file>
<file alias="RoomMemberListView.qml">contents/ui/RoomMemberListView.qml</file>
<file alias="RoomMemberListViewItemDelegate.qml">contents/ui/RoomMemberListViewItemDelegate.qml</file>
<file alias="AsyncHandler.qml">contents/ui/AsyncHandler.qml</file>
<file alias="UploadFileHelper.qml">contents/ui/UploadFileHelper.qml</file>
<file alias="SettingsPage.qml">contents/ui/SettingsPage.qml</file>
<file alias="settings/ProfileSettings.qml">contents/ui/settings/ProfileSettings.qml</file>
<file alias="settings/CacheSettings.qml">contents/ui/settings/CacheSettings.qml</file>
<file alias="room-settings/RoomSettingsPage.qml">contents/ui/room-settings/RoomSettingsPage.qml</file>
<file alias="room-settings/RoomTagHandler.qml">contents/ui/room-settings/RoomTagHandler.qml</file>
<file alias="room-settings/RoomMemberListPage.qml">contents/ui/room-settings/RoomMemberListPage.qml</file>
<file alias="room-settings/RoomInvitePage.qml">contents/ui/room-settings/RoomInvitePage.qml</file>
<file alias="device-mgmt/Device.qml">contents/ui/device-mgmt/Device.qml</file>
<file alias="device-mgmt/DeviceList.qml">contents/ui/device-mgmt/DeviceList.qml</file>
<file alias="UserPage.qml">contents/ui/UserPage.qml</file>
<file alias="shortcuts/ActionCollection.qml">contents/ui/shortcuts/ActionCollection.qml</file>
<file alias="shortcuts/ActionItem.qml">contents/ui/shortcuts/ActionItem.qml</file>
<file alias="shortcuts/ActionSettings.qml">contents/ui/shortcuts/ActionSettings.qml</file>
<file alias="shortcuts/ShortcutInput.qml">contents/ui/shortcuts/ShortcutInput.qml</file>
<file alias="MessageNotification.qml">contents/ui/MessageNotification.qml</file>
<file alias="Notifier.qml">contents/ui/Notifier.qml</file>
<file alias="l10n.js">js/l10n.js</file>
<file alias="fluent-bundle.js">js/transformed-libs/fluent-bundle.js</file>
<file alias="fluent-sequence.js">js/transformed-libs/fluent-sequence.js</file>
<file alias="fluent-langneg.js">js/transformed-libs/fluent-langneg.js</file>
<file alias="bundled-deps.js">js/transformed-libs/bundled-deps.js</file>
<file alias="global-this.js">js/global-this.js</file>
<file alias="matrix-helpers.js">js/matrix-helpers.js</file>
</qresource>
</RCC>
diff --git a/src/tests/matrix-sticker-pack-test.cpp b/src/tests/matrix-sticker-pack-test.cpp
index 6eb951d..6d78743 100644
--- a/src/tests/matrix-sticker-pack-test.cpp
+++ b/src/tests/matrix-sticker-pack-test.cpp
@@ -1,103 +1,120 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <memory>
#include <QtTest>
#include <lager/state.hpp>
#include <base/event.hpp>
#include <testfixtures/factory.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack.hpp"
#include "matrix-sticker.hpp"
#include "matrix-sdk.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixStickerPackTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testStickerPack();
void testStickerPackList();
};
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
static auto stickerPackEvent = Event{R"({
"content": {
"images": {
"myemote": {
"url": "mxc://example.org/blah"
},
"mysticker": {
"body": "my sticker",
"url": "mxc://example.org/sticker",
"usage": ["sticker"],
"info": {
+ "mimetype": "image/png"
}
}
},
"pack": {
"display_name": "Awesome Pack",
"usage": ["emoticon"]
}
},
"type": "im.ponies.user_emotes"
})"_json};
void MatrixStickerPackTest::testStickerPack()
{
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
QCOMPARE(stickerPack->rowCount(QModelIndex()), 2);
QCOMPARE(stickerPack->count(), 2);
auto sticker0 = toUniquePtr(stickerPack->at(0));
QCOMPARE(sticker0->shortCode(), QStringLiteral("myemote"));
QCOMPARE(sticker0->body(), QStringLiteral("myemote"));
QCOMPARE(sticker0->mxcUri(), QStringLiteral("mxc://example.org/blah"));
QCOMPARE(sticker0->info(), QJsonObject());
+ QCOMPARE(sticker0->makeEventJson(), (QJsonObject{
+ {"type", "m.sticker"},
+ {"content", QJsonObject{
+ {"body", "myemote"},
+ {"url", "mxc://example.org/blah"},
+ {"info", QJsonObject()},
+ }},
+ }));
auto sticker1 = toUniquePtr(stickerPack->at(1));
QCOMPARE(sticker1->shortCode(), QStringLiteral("mysticker"));
QCOMPARE(sticker1->body(), QStringLiteral("my sticker"));
QCOMPARE(sticker1->mxcUri(), QStringLiteral("mxc://example.org/sticker"));
- QCOMPARE(sticker1->info(), QJsonObject());
+ QCOMPARE(sticker1->info(), (QJsonObject{{"mimetype", "image/png"}}));
+ QCOMPARE(sticker1->makeEventJson(), (QJsonObject{
+ {"type", "m.sticker"},
+ {"content", QJsonObject{
+ {"body", "my sticker"},
+ {"url", "mxc://example.org/sticker"},
+ {"info", QJsonObject{{"mimetype", "image/png"}}},
+ }},
+ }));
}
void MatrixStickerPackTest::testStickerPackList()
{
auto model = makeClient(withAccountData({stickerPackEvent}));
std::unique_ptr<MatrixSdk> sdk{makeTestSdk(SdkModel{model})};
auto stickerPackList = toUniquePtr(sdk->stickerPackList());
QCOMPARE(stickerPackList->rowCount(QModelIndex()), 1);
QCOMPARE(stickerPackList->count(), 1);
auto stickerPack = toUniquePtr(stickerPackList->at(0));
QCOMPARE(stickerPack->count(), 2);
}
QTEST_MAIN(MatrixStickerPackTest)
#include "matrix-sticker-pack-test.moc"
diff --git a/src/tests/quick-tests/tst_StickerPicker.qml b/src/tests/quick-tests/tst_StickerPicker.qml
new file mode 100644
index 0000000..dbf08eb
--- /dev/null
+++ b/src/tests/quick-tests/tst_StickerPicker.qml
@@ -0,0 +1,99 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtTest 1.0
+
+import '../../contents/ui' as Kazv
+import 'test-helpers.js' as Helpers
+import 'test-helpers' as TestHelpers
+
+import moe.kazv.mxc.kazv 0.0 as MK
+
+Item {
+ id: item
+ width: 800
+ height: 600
+
+ property var l10n: Helpers.fluentMock
+ property var matrixSdk: TestHelpers.MatrixSdkMock {}
+ property var sdkVars: ({})
+
+ function makeSticker(sticker) {
+ return {
+ type: 'm.sticker',
+ content: {
+ body: sticker.body,
+ url: sticker.mxcUri,
+ info: sticker.info,
+ },
+ };
+ }
+
+ property list<ListModel> stickerPacks: [
+ ListModel {
+ id: stickerPack0
+ ListElement {
+ shortCode: 'some'
+ body: 'some'
+ mxcUri: 'mxc://example.org/some'
+ makeEventJson: () => makeSticker(stickerPack0.get(0))
+ }
+
+ ListElement {
+ shortCode: 'some1'
+ body: 'some1'
+ mxcUri: 'mxc://example.org/some1'
+ makeEventJson: () => makeSticker(stickerPack0.get(1))
+ }
+
+ function at(index) {
+ return stickerPack0.get(index);
+ }
+ }
+ ]
+
+ Kazv.StickerPicker {
+ id: stickerPicker
+ property var _sendMessageRequested: 0
+ property var _lastEventJson: undefined
+ stickerPackList: ListModel {
+ id: stickerPackListModel
+ ListElement {
+ }
+ function at(index) {
+ return stickerPacks[index];
+ }
+ }
+
+ onSendMessageRequested: (eventJson) => {
+ ++stickerPicker._sendMessageRequested;
+ stickerPicker._lastEventJson = eventJson;
+ }
+ }
+
+ TestCase {
+ id: stickerPickerTest
+ name: 'StickerPickerTest'
+ when: windowShown
+
+ function cleanup() {
+ stickerPicker._sendMessageRequested = 0;
+ stickerPicker._lastEventJson = undefined;
+ }
+
+ function test_stickerPicker() {
+ verify(findChild(stickerPicker, 'stickerPack0'));
+ verify(findChild(stickerPicker, 'sticker0'));
+ verify(findChild(stickerPicker, 'sticker1'));
+ const stickerButton = findChild(stickerPicker, 'sticker1');
+ mouseClick(stickerButton);
+ tryVerify(() => stickerPicker._sendMessageRequested, 1000);
+ verify(Helpers.deepEqual(stickerPicker._lastEventJson, makeSticker(stickerPack0.get(1))));
+ }
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 4:27 PM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55324
Default Alt Text
(70 KB)

Event Timeline