Page MenuHomePhorge

D12.1732996701.diff
No OneTemporary

Size
95 KB
Referenced Files
None
Subscribers
None

D12.1732996701.diff

diff --git a/.arcconfig b/.arcconfig
new file mode 100644
--- /dev/null
+++ b/.arcconfig
@@ -0,0 +1,4 @@
+{
+ "phabricator.uri": "https://iron.lily-is.land",
+ "repository.callsign": "K"
+}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,26 +4,33 @@
workflow:
rules:
- - if: $CI_PIPELINE_SOURCE == "merge_request_event"
- - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
+ - if: '$CI_PIPELINE_SOURCE == "trigger"'
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: $CI_COMMIT_BRANCH
+ - if: '$CI_COMMIT_TAG !~ /^phabricator\//'
stages:
- - check-changelog
+ - prepare
- lint
- build
+ - report
-'check-changelog':
- stage: check-changelog
- image: alpine
- dependencies: []
- script:
- - sh ./tools/check-changelog
+.report:
+ image:
+ name: 'reg.lily.kazv.moe/infra/phorge-ci-tools:servant'
rules:
- - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release\//
- when: never
- - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "servant"
+ - if: $TARGET_PHID
+ when: always
+ - when: never
+ before_script:
+ - pipelineUrl="$CI_PROJECT_URL"/-/pipelines/"$CI_PIPELINE_ID"
+
+report-start:
+ extends: .report
+ stage: prepare
+ script:
+ - 'echo "{\"receiver\": \"$TARGET_PHID\", \"type\": \"work\", \"unit\": [{\"name\": \"GitLab CI (information only)\", \"result\": \"skip\", \"details\": \"$pipelineUrl\", \"format\": \"remarkup\"}]}" | /tools/arcanist/bin/arc call-conduit --conduit-uri https://iron.lily-is.land/ --conduit-token "$CONDUIT_TOKEN" -- harbormaster.sendmessage'
'lint:no-tabs':
stage: lint
@@ -66,3 +73,23 @@
- kazv-Release.AppImage
expire_in: 1 week
rules: *build-rules
+
+report-success:
+ extends: .report
+ rules:
+ - if: $TARGET_PHID
+ when: on_success
+ - when: never
+ stage: report
+ script:
+ - 'echo "{\"receiver\": \"$TARGET_PHID\", \"type\": \"pass\"}" | /tools/arcanist/bin/arc call-conduit --conduit-uri https://iron.lily-is.land/ --conduit-token "$CONDUIT_TOKEN" -- harbormaster.sendmessage'
+
+report-failure:
+ extends: .report
+ rules:
+ - if: $TARGET_PHID
+ when: on_failure
+ - when: never
+ stage: report
+ script:
+ - 'echo "{\"receiver\": \"$TARGET_PHID\", \"type\": \"fail\"}" | /tools/arcanist/bin/arc call-conduit --conduit-uri https://iron.lily-is.land/ --conduit-token "$CONDUIT_TOKEN" -- harbormaster.sendmessage'
diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
--- a/.gitlab/merge_request_templates/Default.md
+++ b/.gitlab/merge_request_templates/Default.md
@@ -1,30 +1,27 @@
+Summary | 概述:
+<!--
+ (Please remove the comments when submitting the merge request.)
+ (在提交合并请求的时候请删掉这些注释。)
+ Describe in detail what the changes are.
+ 详细描述改变了什么。
-# Checklist | 检查清单
+ Any added UI text must be translated via `l10n.get()`.
+ The messages must be translated into every *core language*: en, cmn.
-Before marking this merge request as Ready, the following must be done:
+ 任何添加的 UI 文字都要用 `l10n.get()` 翻译。
+ 讯息必须翻译成所有*核心语言*:en,cmn。
-在把这个合并请求标记成就绪之前,这些东西必须完成:
-
-- [ ]
- The code must compile. Preferably, each commit should compile.
-
- 代码必须能编译。最好每个提交都能编译。
-
-- [ ]
- Any added UI text must be translated via `l10n.get()`.
- The messages must be translated into every *core language*: en, cmn.
-
- 任何添加的 UI 文字都要用 `l10n.get()` 翻译。
- 讯息必须翻译成所有*核心语言*:en,cmn。
-
- - [ ]
The merge request creator must translate each message into one of the *core languages*.
The others can be handled by others.
开合并请求的人必须把每条讯息翻译成一种*核心语言*。
别的可以给别人做。
+-->
-- [ ]
- If the merge request contains a user-visible change, it must have a changelog.
+Type | 类型: <!-- add|remove|skip|security|fix -->
- 如果这个合并请求包括对用户可见的更改,必须写个更改记录。
+Test Plan | 测试计划:
+<!--
+ Tell reviewers how to verify the intended behaviours.
+ 告诉审核者怎么验证想要的表现。
+-->
diff --git a/changelogs/71.add b/changelogs/71.add
new file mode 100644
--- /dev/null
+++ b/changelogs/71.add
@@ -0,0 +1 @@
+Support sending stickers
diff --git a/changelogs/84.add b/changelogs/84.add
new file mode 100644
--- /dev/null
+++ b/changelogs/84.add
@@ -0,0 +1 @@
+Display edited version of events
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,6 +19,8 @@
matrix-room-timeline.cpp
matrix-room-member.cpp
matrix-room-member-list-model.cpp
+ matrix-event-reader.cpp
+ matrix-event-reader-list-model.cpp
matrix-event.cpp
meta-types.cpp
l10n-provider.cpp
@@ -26,6 +28,11 @@
helper.cpp
matrix-promise.cpp
kazv-util.cpp
+ matrix-sticker-pack.cpp
+ matrix-sticker.cpp
+ matrix-sticker-pack-list.cpp
+ matrix-sticker-pack-source.cpp
+ qt-promise-handler.cpp
device-mgmt/matrix-device.cpp
device-mgmt/matrix-device-list.cpp
diff --git a/src/contents/ui/AddStickerPopup.qml b/src/contents/ui/AddStickerPopup.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/AddStickerPopup.qml
@@ -0,0 +1,88 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import org.kde.kirigami 2.20 as Kirigami
+import moe.kazv.mxc.kazv 0.0 as MK
+
+import '.' as Kazv
+
+Kirigami.OverlaySheet {
+ id: addStickerPopup
+
+ property var stickerPackList
+ property var event
+
+ property var currentPack: stickerPackList.at(0)
+ property var stickerSize: Kirigami.Units.iconSizes.enormous
+ property var addingSticker: false
+
+ title: l10n.get('add-sticker-popup-title')
+
+ ColumnLayout {
+ Kirigami.FormLayout {
+ Layout.fillWidth: true
+
+ Kirigami.Icon {
+ Layout.preferredHeight: addStickerPopup.stickerSize
+ Layout.preferredWidth: addStickerPopup.stickerSize
+ height: addStickerPopup.stickerSize
+ width: addStickerPopup.stickerSize
+ source: matrixSdk.mxcUriToHttp(event.content.url)
+ }
+
+ TextField {
+ Layout.fillWidth: true
+ id: shortCodeInput
+ objectName: 'shortCodeInput'
+ readOnly: addStickerPopup.addingSticker
+ Kirigami.FormData.label: l10n.get('add-sticker-popup-short-code-prompt')
+ }
+
+ Label {
+ objectName: 'shortCodeExistsWarning'
+ visible: currentPack.hasShortCode(shortCodeInput.text)
+ text: l10n.get('add-sticker-popup-short-code-exists-warning')
+ }
+ }
+
+ RowLayout {
+ Layout.alignment: Qt.AlignRight
+ Button {
+ objectName: 'addStickerButton'
+ text: l10n.get('add-sticker-popup-add-sticker-button')
+ onClicked: addStickerPopup.updateStickerPack.call()
+ enabled: !addStickerPopup.addingSticker
+ }
+
+ Button {
+ objectName: 'cancelButton'
+ text: l10n.get('add-sticker-popup-cancel-button')
+ onClicked: addStickerPopup.close()
+ }
+ }
+ }
+
+ property var updateStickerPack: Kazv.AsyncHandler {
+ trigger: () => {
+ addStickerPopup.addingSticker = true;
+ return matrixSdk.updateStickerPack(
+ currentPack.addSticker(shortCodeInput.text, addStickerPopup.event)
+ );
+ }
+ onResolved: (success, data) => {
+ addStickerPopup.addingSticker = false;
+ if (!success) {
+ showPassiveNotification(l10n.get('add-sticker-popup-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
+ } else {
+ addStickerPopup.close();
+ }
+ }
+ }
+}
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -191,6 +191,19 @@
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
}
+ Label {
+ objectName: 'editedIndicator'
+ visible: !compactMode && event.isEdited
+ text: l10n.get('event-edited-indicator')
+ font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
+ }
+
+ Kazv.EventReadIndicator {
+ objectName: 'eventReadIndicator'
+ shouldShow: !compactMode && !event.isLocalEcho
+ model: event.readers()
+ }
+
ToolButton {
id: moreButton
objectName: 'moreButton'
diff --git a/src/contents/ui/EventReadIndicator.qml b/src/contents/ui/EventReadIndicator.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/EventReadIndicator.qml
@@ -0,0 +1,96 @@
+/*
+ * 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 '.' as Kazv
+
+ItemDelegate {
+ id: readIndicator
+ property var shouldShow: true
+ property var model
+ property var maxItems: 4
+ property var avatarSize: Kirigami.Units.iconSizes.sizeForLabels || Kirigami.Units.iconSizes.small / 2
+ property var actualItems: Math.min(model.count, maxItems)
+
+ visible: shouldShow && readIndicator.model.count > 0
+
+ implicitHeight: layout.implicitHeight
+ implicitWidth: layout.implicitWidth
+
+ RowLayout {
+ id: layout
+ objectName: 'readIndicatorLayout'
+ anchors.fill: parent
+ Repeater {
+ model: readIndicator.actualItems
+
+ Kirigami.Avatar {
+ objectName: `readIndicatorAvatar${index}`
+ property var member: readIndicator.model.at(index)
+ Layout.preferredWidth: readIndicator.avatarSize
+ Layout.preferredHeight: readIndicator.avatarSize
+ source: member.avatarMxcUri ? matrixSdk.mxcUriToHttp(member.avatarMxcUri) : ''
+ name: member.name || member.userId
+ }
+ }
+
+ Label {
+ objectName: 'moreUsersIndicator'
+ visible: readIndicator.model.count > readIndicator.maxItems
+ text: l10n.get('event-read-indicator-more', { rest: readIndicator.model.count - readIndicator.maxItems })
+ }
+ }
+
+ Kirigami.OverlayDrawer {
+ id: readIndicatorDrawer
+ edge: Qt.BottomEdge
+ modal: true
+ contentItem: ColumnLayout {
+ Label {
+ text: l10n.get('event-read-indicator-list-title', { numUsers: readIndicator.model.count })
+ }
+
+ Repeater {
+ model: readIndicator.model
+
+ ItemDelegate {
+ Layout.fillWidth: true
+ implicitWidth: itemLayout.implicitWidth
+ implicitHeight: itemLayout.implicitHeight
+ property var member: readIndicator.model.at(index)
+ RowLayout {
+ id: itemLayout
+ anchors.fill: parent
+ Kirigami.Avatar {
+ objectName: `readIndicatorAvatar${index}`
+ Layout.preferredWidth: readIndicator.avatarSize
+ Layout.preferredHeight: readIndicator.avatarSize
+ source: member.avatarMxcUri ? matrixSdk.mxcUriToHttp(member.avatarMxcUri) : ''
+ name: member.name || member.userId
+ }
+
+ Label {
+ text: member.name || member.userId
+ Layout.fillWidth: true
+ }
+
+ Label {
+ Layout.alignment: Qt.AlignRight
+ text: member.formattedTime
+ }
+ }
+ }
+ }
+ }
+ }
+
+ onClicked: readIndicatorDrawer.open()
+}
diff --git a/src/contents/ui/EventView.qml b/src/contents/ui/EventView.qml
--- a/src/contents/ui/EventView.qml
+++ b/src/contents/ui/EventView.qml
@@ -200,6 +200,10 @@
if (e.isState) {
return 'state';
}
+ // ignore edits of other events
+ if (e.relationType === 'm.replace') {
+ return 'ignore';
+ }
switch (e.type) {
case 'm.room.message':
switch (e.content.msgtype) {
@@ -231,6 +235,9 @@
return 'unknown';
}
+ case 'm.sticker':
+ return 'image';
+
case 'm.room.redaction':
return 'ignore';
diff --git a/src/contents/ui/MainPage.qml b/src/contents/ui/MainPage.qml
--- a/src/contents/ui/MainPage.qml
+++ b/src/contents/ui/MainPage.qml
@@ -20,25 +20,34 @@
onCurrentTagIdChanged: sdkVars.roomList.setTagId(currentTagId)
- header: RowLayout {
- ToolButton {
- icon.name: 'clock'
+ header: ColumnLayout {
+ TextField {
+ text: sdkVars.roomList.filter
+ onTextChanged: sdkVars.roomList.filter = text
+ placeholderText: l10n.get('main-page-room-filter-prompt')
Layout.fillWidth: true
- text: l10n.get('main-page-recent-tab-title')
- onClicked: currentTagId = ''
- display: AbstractButton.TextUnderIcon
- checkable: true
- checked: currentTagId === ''
}
- ToolButton {
- icon.name: 'non-starred-symbolic'
- Layout.fillWidth: true
- text: l10n.get('main-page-favourites-tab-title')
- onClicked: currentTagId = 'm.favourite'
- display: AbstractButton.TextUnderIcon
- checkable: true
- checked: currentTagId === 'm.favourite'
+ RowLayout {
+ ToolButton {
+ icon.name: 'clock'
+ Layout.fillWidth: true
+ text: l10n.get('main-page-recent-tab-title')
+ onClicked: currentTagId = ''
+ display: AbstractButton.TextUnderIcon
+ checkable: true
+ checked: currentTagId === ''
+ }
+
+ ToolButton {
+ icon.name: 'non-starred-symbolic'
+ Layout.fillWidth: true
+ text: l10n.get('main-page-favourites-tab-title')
+ onClicked: currentTagId = 'm.favourite'
+ display: AbstractButton.TextUnderIcon
+ checkable: true
+ checked: currentTagId === 'm.favourite'
+ }
}
}
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -109,6 +109,11 @@
onClicked: sendMediaFileAction.trigger()
}
+ ToolButton {
+ action: stickersAction
+ display: AbstractButton.IconOnly
+ }
+
ToolButton {
icon.name: "document-send"
onClicked: sendAction.trigger()
@@ -185,4 +190,26 @@
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
--- /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/contents/ui/event-types/Image.qml b/src/contents/ui/event-types/Image.qml
--- a/src/contents/ui/event-types/Image.qml
+++ b/src/contents/ui/event-types/Image.qml
@@ -19,6 +19,7 @@
property var gender: 'neutral'
property var body: event.content.body
+ property var curEvent: event
property var thumbnailFile: imageInfo.thumbnail_file || {}
property var thumbnailInfo: imageInfo.thumbnail_info || {}
@@ -35,6 +36,7 @@
property var jobManager: kazvIOManager
property var mtxSdk: matrixSdk
+ property var isSticker: event.type === 'm.sticker'
Kazv.FileHandler {
id: fileHandler
@@ -53,7 +55,7 @@
Layout.fillWidth: true
wrapMode: Text.Wrap
- text: l10n.get('event-message-image-sent', { gender, body })
+ text: isSticker ? '' : l10n.get('event-message-image-sent', { gender, body })
}
property var image: Image {
@@ -65,6 +67,20 @@
horizontalAlignment: Image.AlignLeft
fillMode: Image.PreserveAspectFit
}
+
+ property var canAddSticker: !!event.content.url
+ property var addStickerPopup: Kazv.AddStickerPopup {
+ stickerPackList: matrixSdk.stickerPackList()
+ event: upper.curEvent
+ }
+
+ property var addStickerAction: Kirigami.Action {
+ text: l10n.get('media-file-menu-add-sticker-action')
+ onTriggered: bubble.addStickerPopup.open()
+ }
+
+ additionalMenuContent: canAddSticker ? [addStickerAction] : []
+
ColumnLayout {
id: layout
Layout.fillWidth: true
diff --git a/src/contents/ui/event-types/MediaBubble.qml b/src/contents/ui/event-types/MediaBubble.qml
--- a/src/contents/ui/event-types/MediaBubble.qml
+++ b/src/contents/ui/event-types/MediaBubble.qml
@@ -21,7 +21,9 @@
eventContent: bubble.eventContent
}
- menuContent: mediaFileMenu.optionMenu
+ menuContent: mediaFileMenu.optionMenu.concat(additionalMenuContent)
+
+ property var additionalMenuContent: []
property var progressBar: Kazv.KazvIOMenu {
id: progressBar
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -45,6 +45,7 @@
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 ->
@@ -88,6 +89,10 @@
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 = 加载更多
@@ -149,9 +154,20 @@
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 = (编辑过了)
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 }
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -45,6 +45,7 @@
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 ->
@@ -88,6 +89,10 @@
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
@@ -161,9 +166,23 @@
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)
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 }
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -49,9 +49,9 @@
}
registerKazvQmlTypes();
-#if KAZV_IS_WINDOWS
- qRegisterMetaType<Kazv::KazvEvent>();
-#endif
+
+ KazvMetaTypeRegistration registration;
+ Q_UNUSED(registration);
#if KAZV_IS_WINDOWS
// On Windows the default search path is only in qrc
diff --git a/src/matrix-event-reader-list-model.hpp b/src/matrix-event-reader-list-model.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-event-reader-list-model.hpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "matrix-room-member-list-model.hpp"
+
+class MatrixEventReader;
+
+class MatrixEventReaderListModel : public MatrixRoomMemberListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ lager::reader<immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>> m_readers;
+
+public:
+ explicit MatrixEventReaderListModel(lager::reader<immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>> readers, QObject *parent = 0);
+ ~MatrixEventReaderListModel() override;
+
+ Q_INVOKABLE MatrixEventReader *at(int index) const;
+};
diff --git a/src/matrix-event-reader-list-model.cpp b/src/matrix-event-reader-list-model.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-event-reader-list-model.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "matrix-event-reader-list-model.hpp"
+#include "matrix-event-reader.hpp"
+
+using namespace Kazv;
+
+MatrixEventReaderListModel::MatrixEventReaderListModel(lager::reader<immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>> readers, QObject *parent)
+ : MatrixRoomMemberListModel(
+ readers.xform(containerMap(EventList{}, zug::map([](const auto &item) {
+ return std::get<0>(item);
+ }))),
+ parent
+ )
+ , m_readers(readers)
+{
+}
+
+MatrixEventReaderListModel::~MatrixEventReaderListModel() = default;
+
+MatrixEventReader *MatrixEventReaderListModel::at(int index) const
+{
+ auto reader = m_readers[index][lager::lenses::or_default].make();
+ return new MatrixEventReader(
+ reader.map([](const auto &r) {
+ return std::get<0>(r);
+ }),
+ reader.map([](const auto &r) {
+ return std::get<1>(r);
+ }));
+}
diff --git a/src/matrix-event-reader.hpp b/src/matrix-event-reader.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-event-reader.hpp
@@ -0,0 +1,26 @@
+/*
+ * 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 "matrix-room-member.hpp"
+
+class MatrixEventReader : public MatrixRoomMember
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit MatrixEventReader(lager::reader<Kazv::Event> memberEvent, lager::reader<Kazv::Timestamp> timestamp, QObject *parent = 0);
+ ~MatrixEventReader() override;
+
+ LAGER_QT_READER(qint64, timestamp);
+ LAGER_QT_READER(QString, formattedTime);
+};
diff --git a/src/matrix-event-reader.cpp b/src/matrix-event-reader.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-event-reader.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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 <QDateTime>
+
+#include "matrix-event-reader.hpp"
+
+using namespace Kazv;
+
+MatrixEventReader::MatrixEventReader(lager::reader<Kazv::Event> memberEvent, lager::reader<Kazv::Timestamp> timestamp, QObject *parent)
+ : MatrixRoomMember(memberEvent, parent)
+ , LAGER_QT(timestamp)(timestamp.map([](auto ts) {
+ return static_cast<qint64>(ts);
+ }))
+ , LAGER_QT(formattedTime)(timestamp
+ .map([](Timestamp ts) {
+ auto locale = QLocale::system();
+ return locale.toString(
+ QDateTime::fromMSecsSinceEpoch(ts).time(),
+ QLocale::ShortFormat
+ );
+ }))
+{
+}
+
+MatrixEventReader::~MatrixEventReader() = default;
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -18,6 +18,8 @@
#include "qt-json.hpp"
+class MatrixEventReaderListModel;
+
class MatrixEvent : public QObject
{
Q_OBJECT
@@ -26,10 +28,16 @@
lager::reader<std::optional<Kazv::LocalEchoDesc>> m_localEcho;
lager::reader<Kazv::Event> m_event;
+ std::optional<Kazv::Room> m_room;
+ lager::reader<std::string> m_eventIdStd;
+ lager::reader<std::string> m_senderStd;
+ /// the unedited content of this event
+ lager::reader<QJsonObject> m_originalContent;
+ lager::reader<Kazv::EventList> m_edits;
public:
- explicit MatrixEvent(lager::reader<std::variant<Kazv::Event, Kazv::LocalEchoDesc>> event, QObject *parent = 0);
- explicit MatrixEvent(lager::reader<Kazv::Event> event, QObject *parent = 0);
+ explicit MatrixEvent(lager::reader<std::variant<Kazv::Event, Kazv::LocalEchoDesc>> event, std::optional<Kazv::Room> room = std::nullopt, QObject *parent = 0);
+ explicit MatrixEvent(lager::reader<Kazv::Event> event, std::optional<Kazv::Room> room = std::nullopt, QObject *parent = 0);
~MatrixEvent() override;
LAGER_QT_READER(QString, eventId);
@@ -52,6 +60,9 @@
LAGER_QT_READER(QString, relationType);
LAGER_QT_READER(QString, relatedEventId);
LAGER_QT_READER(QString, formattedTime);
+ LAGER_QT_READER(bool, isEdited);
+
+ Q_INVOKABLE MatrixEventReaderListModel *readers() const;
Kazv::Event underlyingEvent() const;
};
diff --git a/src/matrix-event.cpp b/src/matrix-event.cpp
--- a/src/matrix-event.cpp
+++ b/src/matrix-event.cpp
@@ -8,8 +8,10 @@
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include "matrix-event.hpp"
+#include "matrix-event-reader-list-model.hpp"
#include "helper.hpp"
+#include "kazv-log.hpp"
using namespace Kazv;
@@ -31,15 +33,58 @@
}
}
-MatrixEvent::MatrixEvent(lager::reader<std::variant<Event, LocalEchoDesc>> event, QObject *parent)
+MatrixEvent::MatrixEvent(lager::reader<std::variant<Event, LocalEchoDesc>> event, std::optional<Room> room, QObject *parent)
: QObject(parent)
, m_localEcho(event.map(getLocalEcho))
, m_event(event.map(getEvent))
- , LAGER_QT(eventId)(m_event.xform(zug::map([](Event e) { return e.id(); }) | strToQt))
- , LAGER_QT(sender)(m_event.xform(zug::map([](Event e) { return e.sender(); }) | strToQt))
+ , m_room(room)
+ , m_eventIdStd(m_event.map(&Event::id))
+ , m_senderStd(m_event.map(&Event::sender))
+ , m_originalContent(m_event.map([](Event e) { return e.content().get().get<QJsonObject>(); }))
+ , m_edits(m_room.has_value() ? lager::reader<EventList>(lager::with(
+ m_event,
+ m_room->relatedEvents(m_eventIdStd, "m.replace")
+ ).map([](const Event &origEvent, const EventList &edits) -> EventList {
+ // https://spec.matrix.org/v1.10/client-server-api/#event-replacements
+ if (!(
+ // (3) no state key
+ !origEvent.isState()
+ // (4) original event is not an edit
+ && origEvent.relationship().first != "m.replace"
+ )) {
+ return EventList{};
+ }
+ auto origType = origEvent.type();
+ auto origSender = origEvent.sender();
+ return intoImmer(
+ EventList{},
+ zug::filter([&origType, &origSender](const Event &ev) {
+ return
+ // (0) same room id requirement is implicit.
+ // (1) same sender
+ origSender == ev.sender()
+ // (2) same type
+ && origType == ev.type()
+ // (3) no state key
+ && !ev.isState()
+ // (4) original event is not an edit is checked above
+ // (5) replaced event content has `m.new_content`
+ && ev.content().get().contains("m.new_content");
+ }),
+ edits
+ );
+ })) : lager::make_constant(EventList{}))
+ , LAGER_QT(eventId)(m_eventIdStd.xform(strToQt))
+ , LAGER_QT(sender)(m_senderStd.xform(strToQt))
, LAGER_QT(type)(m_event.xform(zug::map([](Event e) { return e.type(); }) | strToQt))
, LAGER_QT(stateKey)(m_event.xform(zug::map([](Event e) { return e.stateKey(); }) | strToQt))
- , LAGER_QT(content)(m_event.xform(zug::map([](Event e) { return e.content().get().get<QJsonObject>(); })))
+ , LAGER_QT(content)(lager::with(m_originalContent, m_edits).map([](const QJsonObject &origContent, const EventList &edits) {
+ if (edits.empty()) {
+ return origContent;
+ } else {
+ return edits.at(edits.size() - 1).content().get().at("m.new_content").get<QJsonObject>();
+ }
+ }))
, LAGER_QT(encrypted)(m_event.xform(zug::map([](Event e) { return e.encrypted(); })))
, LAGER_QT(decrypted)(m_event.map([](Event e) { return e.decrypted(); }))
, LAGER_QT(isState)(m_event.map([](Event e) { return e.isState(); }))
@@ -88,16 +133,49 @@
QLocale::ShortFormat
);
}))
+ , LAGER_QT(isEdited)(m_edits.map([](const auto &edits) { return !edits.empty(); }))
{
}
-MatrixEvent::MatrixEvent(lager::reader<Event> event, QObject *parent)
- : MatrixEvent(event.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }), parent)
+MatrixEvent::MatrixEvent(lager::reader<Event> event, std::optional<Kazv::Room> room, QObject *parent)
+ : MatrixEvent(event.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }), room, parent)
{
}
MatrixEvent::~MatrixEvent() = default;
+MatrixEventReaderListModel *MatrixEvent::readers() const
+{
+ if (!m_room.has_value()) {
+ qCDebug(kazvLog) << "readers(): no room!";
+ return new MatrixEventReaderListModel(lager::make_constant(immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>{}));
+ }
+
+ auto eventReaders = lager::with(
+ m_room.value().eventReaders(m_event.map(&Event::id)),
+ m_room.value().stateEvents()
+ ).map([](const auto &eventReaders, const auto &states) {
+ return intoImmer(
+ immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>{},
+ zug::map([states](const auto &item) -> std::tuple<Kazv::Event, Kazv::Timestamp> {
+ auto [userId, ts] = item;
+ qCDebug(kazvLog) << "readers(): read by" << QString::fromStdString(userId);
+ auto memberEvent = states.count({"m.room.member", userId})
+ ? states[{"m.room.member", userId}]
+ : Event{json{
+ {"content", json::object()},
+ {"state_key", userId},
+ {"type", "m.room.member"},
+ }};
+ return {memberEvent, ts};
+ }),
+ eventReaders
+ );
+ }).make();
+
+ return new MatrixEventReaderListModel(eventReaders);
+}
+
Kazv::Event MatrixEvent::underlyingEvent() const
{
return m_event.get();
diff --git a/src/matrix-room-list.hpp b/src/matrix-room-list.hpp
--- a/src/matrix-room-list.hpp
+++ b/src/matrix-room-list.hpp
@@ -14,6 +14,7 @@
#include <QAbstractListModel>
#include <lager/sensor.hpp>
+#include <lager/state.hpp>
#include <lager/extra/qt.hpp>
#include <client/client.hpp>
@@ -30,14 +31,16 @@
QString m_tagId;
lager::sensor<std::string> m_tagIdCursor;
int m_internalCount;
+ lager::state<std::string, lager::automatic_tag> m_filter;
lager::reader<immer::flex_vector<std::string>> m_roomIds;
public:
- explicit MatrixRoomList(Kazv::Client client, QString tagId = QString(), QObject *parent = 0);
+ explicit MatrixRoomList(Kazv::Client client, QString tagId = QString(), QString filter = QString(), QObject *parent = 0);
~MatrixRoomList() override;
Q_INVOKABLE void setTagId(QString tagId);
+ LAGER_QT_CURSOR(QString, filter);
LAGER_QT_READER(int, count);
LAGER_QT_READER(QStringList, roomIds);
diff --git a/src/matrix-room-list.cpp b/src/matrix-room-list.cpp
--- a/src/matrix-room-list.cpp
+++ b/src/matrix-room-list.cpp
@@ -29,18 +29,72 @@
return latestEvent.originServerTs();
}
-MatrixRoomList::MatrixRoomList(Kazv::Client client, QString tagId, QObject *parent)
+MatrixRoomList::MatrixRoomList(Kazv::Client client, QString tagId, QString filter, QObject *parent)
: QAbstractListModel(parent)
, m_client(client)
, m_tagId(tagId)
, m_tagIdCursor(lager::make_sensor([this] { return m_tagId.toStdString(); }))
, m_internalCount(0)
- , m_roomIds(lager::with(m_tagIdCursor, m_client.rooms(), m_client.roomIdsByTagId())
- .map([](const auto &tagIdStdStr, const auto &allRooms, const auto &roomsByTagMap) {
+ , m_filter(lager::make_state(filter.toStdString(), lager::automatic_tag{}))
+ , m_roomIds(lager::with(m_tagIdCursor, m_client.rooms(), m_filter, m_client.roomIdsByTagId())
+ .map([](const auto &tagIdStdStr, const auto &allRooms, const auto &filter, const auto &roomsByTagMap) {
auto toId = zug::map([](const auto &pair) {
return pair.first;
});
+ auto roomName = [](const auto &room) {
+ auto content = room.stateEvents[{"m.room.name", ""}].content().get();
+ if (content.contains("name") && content["name"].is_string()) {
+ return content["name"].template get<std::string>();
+ }
+ return std::string();
+ };
+
+ auto roomHeroNames = [](const auto &room) {
+ auto heroEvents = room.heroMemberEvents();
+ return intoImmer(
+ immer::flex_vector<std::string>(),
+ zug::map([](const Event &ev) {
+ auto content = ev.content().get();
+ if (content.contains("displayname") && content["displayname"].is_string()) {
+ return content["displayname"].template get<std::string>();
+ }
+ return std::string();
+ }),
+ heroEvents
+ );
+ };
+
+ auto applyFilter = zug::filter([&filter, &allRooms, &roomName, &roomHeroNames](const auto &id) {
+ if (filter.empty()) {
+ return true;
+ }
+
+ const auto &room = allRooms[id];
+ // Use exact match for room id
+ if (room.roomId == filter) {
+ return true;
+ }
+
+ auto name = roomName(room);
+ if (!name.empty()) {
+ // Use substring match for name search
+ return name.find(filter) != std::string::npos;
+ }
+
+ // The room has no name, use hero names for the search
+ auto heroes = roomHeroNames(room);
+ // If any of the room hero matches the filter, consider it a match
+ return std::any_of(heroes.begin(), heroes.end(),
+ [&filter](const auto &name) {
+ return name.find(filter) != std::string::npos;
+ })
+ || std::any_of(room.heroIds.begin(), room.heroIds.end(),
+ [&filter](const auto &id) {
+ return id.find(filter) != std::string::npos;
+ });
+ });
+
auto sortByTimestampDesc = [allRooms](std::vector<std::string> container) {
std::sort(
container.begin(),
@@ -55,16 +109,17 @@
if (tagIdStdStr.empty()) {
return sortByTimestampDesc(zug::into_vector(
- toId,
+ toId | applyFilter,
allRooms
));
} else {
return sortByTimestampDesc(zug::into_vector(
- toId,
+ toId | applyFilter,
roomsByTagMap[tagIdStdStr]
));
}
}))
+ , LAGER_QT(filter)(m_filter.xform(strToQt, qStringToStd))
, LAGER_QT(count)(m_roomIds.xform(containerSize))
, LAGER_QT(roomIds)(m_roomIds.xform(zug::map(
[](auto container) {
diff --git a/src/matrix-room-timeline.cpp b/src/matrix-room-timeline.cpp
--- a/src/matrix-room-timeline.cpp
+++ b/src/matrix-room-timeline.cpp
@@ -69,7 +69,7 @@
return Event();
}
}
- }));
+ }), m_room);
}
int MatrixRoomTimeline::rowCount(const QModelIndex &index) const
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -68,6 +68,8 @@
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,
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -154,6 +154,13 @@
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()
@@ -321,7 +328,8 @@
{
return new MatrixEvent(
m_room.message(lager::make_constant(eventId.toStdString()))
- .map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; })
+ .map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
+ m_room
);
}
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -27,6 +27,7 @@
class MatrixPromise;
class MatrixSdkTest;
class MatrixEvent;
+class MatrixStickerPackList;
struct MatrixSdkPrivate;
@@ -196,6 +197,25 @@
*/
bool shouldNotify(MatrixEvent *event) const;
+ /**
+ * Get the sticker pack list for the current account.
+ *
+ * @return A list of sticker packs associated with the current account.
+ */
+ MatrixStickerPackList *stickerPackList() const;
+
+ /**
+ * Update the sticker pack from source.
+ *
+ * @param source The source of the sticker pack to update.
+ * @return A promise that resolves when the sticker pack is updated,
+ * or when there is an error.
+ */
+ MatrixPromise *updateStickerPack(MatrixStickerPackSource source);
+
+private:
+ MatrixPromise *sendAccountDataImpl(Kazv::Event event);
+
private: // Testing
friend MatrixSdkTest;
friend MatrixSdk *makeTestSdk(Kazv::SdkModel model);
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -40,6 +40,8 @@
#include "qt-promise-handler.hpp"
#include "qt-job-handler.hpp"
#include "device-mgmt/matrix-device-list.hpp"
+#include "matrix-sticker-pack-list.hpp"
+#include "kazv-log.hpp"
using namespace Kazv;
@@ -612,4 +614,25 @@
return m_d->notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
}
+MatrixStickerPackList *MatrixSdk::stickerPackList() const
+{
+ return new MatrixStickerPackList(m_d->clientOnSecondaryRoot);
+}
+
+MatrixPromise *MatrixSdk::updateStickerPack(MatrixStickerPackSource source)
+{
+ if (source.source == MatrixStickerPackSource::AccountData) {
+ auto eventJson = std::move(source.event).raw().get();
+ eventJson["type"] = source.eventType;
+ return sendAccountDataImpl(Event(std::move(eventJson)));
+ } else {
+ return 0;
+ }
+}
+
+MatrixPromise *MatrixSdk::sendAccountDataImpl(Event event)
+{
+ return new MatrixPromise(m_d->clientOnSecondaryRoot.setAccountData(event));
+}
+
#include "matrix-sdk.moc"
diff --git a/src/matrix-sticker-pack-list.hpp b/src/matrix-sticker-pack-list.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack-list.hpp
@@ -0,0 +1,50 @@
+/*
+ * 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 <QAbstractListModel>
+
+#include <immer/flex_vector.hpp>
+
+#include <lager/reader.hpp>
+#include <lager/extra/qt.hpp>
+
+#include <client/client.hpp>
+#include <base/event.hpp>
+
+#include "matrix-sticker-pack-source.hpp"
+
+class MatrixStickerPack;
+
+class MatrixStickerPackList : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ Kazv::Client m_client;
+ lager::reader<immer::flex_vector<MatrixStickerPackSource>> m_events;
+ int m_internalCount;
+
+public:
+ explicit MatrixStickerPackList(Kazv::Client client, QObject *parent = 0);
+ ~MatrixStickerPackList() override;
+
+ LAGER_QT_READER(int, count);
+
+ Q_INVOKABLE MatrixStickerPack *at(int index) const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ int rowCount(const QModelIndex &parent) const override;
+
+private Q_SLOTS:
+ void updateInternalCount();
+};
diff --git a/src/matrix-sticker-pack-list.cpp b/src/matrix-sticker-pack-list.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack-list.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 <lager/lenses/optional.hpp>
+#include <lager/lenses/at.hpp>
+
+#include "matrix-sticker-pack.hpp"
+#include "matrix-sticker-pack-list.hpp"
+
+using namespace Kazv;
+
+static const std::string accountDataEventType = "im.ponies.user_emotes";
+
+lager::reader<immer::flex_vector<MatrixStickerPackSource>> getEventsFromClient(Client client)
+{
+ lager::reader<Event> userEmotes = client.accountData()[accountDataEventType][lager::lenses::or_default];
+ return userEmotes.map([](Event e) {
+ return immer::flex_vector<MatrixStickerPackSource>{
+ {MatrixStickerPackSource::AccountData, accountDataEventType, e},
+ };
+ });
+}
+
+MatrixStickerPackList::MatrixStickerPackList(Client client, QObject *parent)
+ : QAbstractListModel(parent)
+ , m_client(client)
+ , m_events(getEventsFromClient(m_client))
+ , m_internalCount(0)
+ , LAGER_QT(count)(m_events.map([](const auto &events) {
+ return static_cast<int>(events.size());
+ }))
+{
+ m_internalCount = count();
+
+ connect(this, &MatrixStickerPackList::countChanged, this, &MatrixStickerPackList::updateInternalCount);
+}
+
+MatrixStickerPackList::~MatrixStickerPackList() = default;
+
+MatrixStickerPack *MatrixStickerPackList::at(int index) const
+{
+ return new MatrixStickerPack(m_events.map([index](const auto &events) {
+ if (events.size() > std::size_t(index)) {
+ return events[index];
+ } else {
+ return MatrixStickerPackSource{
+ MatrixStickerPackSource::AccountData,
+ accountDataEventType,
+ Event(),
+ };
+ }
+ }));
+}
+
+QVariant MatrixStickerPackList::data(const QModelIndex &/* index */, int /* role */) const
+{
+ return QVariant();
+}
+
+int MatrixStickerPackList::rowCount(const QModelIndex &parent = QModelIndex()) const
+{
+ if (parent.isValid()) {
+ return 0;
+ } else {
+ return count();
+ }
+}
+
+void MatrixStickerPackList::updateInternalCount()
+{
+ auto curCount = count();
+ auto oldCount = m_internalCount;
+ auto diff = std::abs(curCount - oldCount);
+
+ if (curCount > oldCount) {
+ beginInsertRows(QModelIndex(), 0, diff - 1);
+ m_internalCount = curCount;
+ endInsertRows();
+ } else if (curCount < oldCount) {
+ beginRemoveRows(QModelIndex(), 0, diff - 1);
+ m_internalCount = curCount;
+ endRemoveRows();
+ }
+}
diff --git a/src/matrix-sticker-pack-source.hpp b/src/matrix-sticker-pack-source.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack-source.hpp
@@ -0,0 +1,28 @@
+/*
+ * 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 <base/event.hpp>
+
+struct MatrixStickerPackSource
+{
+ enum Source
+ {
+ AccountData,
+ RoomState,
+ };
+
+ Source source;
+ std::string eventType;
+ Kazv::Event event;
+};
+
+bool operator==(const MatrixStickerPackSource &a, const MatrixStickerPackSource &b);
+
+bool operator!=(const MatrixStickerPackSource &a, const MatrixStickerPackSource &b);
diff --git a/src/matrix-sticker-pack-source.cpp b/src/matrix-sticker-pack-source.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack-source.cpp
@@ -0,0 +1,22 @@
+/*
+ * 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 "matrix-sticker-pack-source.hpp"
+
+bool operator==(const MatrixStickerPackSource &a, const MatrixStickerPackSource &b)
+{
+ return a.source == b.source
+ && b.eventType == b.eventType
+ && a.event == b.event;
+}
+
+bool operator!=(const MatrixStickerPackSource &a, const MatrixStickerPackSource &b)
+{
+ return !(a == b);
+}
diff --git a/src/matrix-sticker-pack.hpp b/src/matrix-sticker-pack.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack.hpp
@@ -0,0 +1,62 @@
+/*
+ * 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 <QAbstractListModel>
+
+#include <lager/reader.hpp>
+#include <lager/extra/qt.hpp>
+
+#include <base/event.hpp>
+
+#include "matrix-sticker-pack-source.hpp"
+#include "meta-types.hpp"
+
+class MatrixSticker;
+class MatrixEvent;
+
+class MatrixStickerPack : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ lager::reader<MatrixStickerPackSource> m_source;
+ lager::reader<Kazv::Event> m_event;
+ lager::reader<Kazv::json> m_images;
+ int m_internalCount;
+
+public:
+ explicit MatrixStickerPack(lager::reader<MatrixStickerPackSource> source, QObject *parent = 0);
+ ~MatrixStickerPack() override;
+
+ LAGER_QT_READER(int, count);
+
+ Q_INVOKABLE MatrixSticker *at(int index) const;
+
+ Q_INVOKABLE bool hasShortCode(const QString &shortCode) const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ int rowCount(const QModelIndex &parent) const override;
+
+ /**
+ * Add a sticker to the pack and return the source of the
+ * modified pack.
+ *
+ * @param shortCode The short code of the sticker.
+ * @param event The event of the sticker.
+ * @return The MatrixStickerPackSource of the modified pack.
+ */
+ Q_INVOKABLE QVariant addSticker(const QString &shortCode, MatrixEvent *event) const;
+
+private Q_SLOTS:
+ void updateInternalCount();
+};
diff --git a/src/matrix-sticker-pack.cpp b/src/matrix-sticker-pack.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker-pack.cpp
@@ -0,0 +1,149 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include <immer/config.hpp>
+
+#include <cursorutil.hpp>
+
+#include "matrix-sticker.hpp"
+#include "matrix-event.hpp"
+#include "matrix-sticker-pack.hpp"
+
+using namespace Kazv;
+
+MatrixStickerPack::MatrixStickerPack(lager::reader<MatrixStickerPackSource> source, QObject *parent)
+ : QAbstractListModel(parent)
+ , m_source(source)
+ , m_event(m_source[&MatrixStickerPackSource::event])
+ , m_images(m_event.xform(
+ eventContent
+ | zug::map([](const JsonWrap &content) {
+ if (content.get().contains("images")
+ && content.get()["images"].is_object()) {
+ auto size = content.get()["images"].size();
+ auto items = content.get()["images"].items();
+ auto array = json(size, json::object());
+ std::transform(
+ items.begin(), items.end(),
+ array.begin(), [](const auto &it) {
+ return json::array({it.key(), it.value()});
+ }
+ );
+ return array;
+ } else {
+ return json::array();
+ }
+ })))
+ , m_internalCount(0)
+ , LAGER_QT(count)(m_images.map([](const JsonWrap &images) -> int {
+ return images.get().size();
+ }))
+{
+ m_internalCount = count();
+
+ connect(this, &MatrixStickerPack::countChanged, this, &MatrixStickerPack::updateInternalCount);
+}
+
+MatrixStickerPack::~MatrixStickerPack() = default;
+
+MatrixSticker *MatrixStickerPack::at(int index) const
+{
+ using namespace nlohmann::literals;
+ auto image = m_images.map([index](const json &j) {
+ if (j.size() > std::size_t(index)) {
+ return j[index];
+ } else {
+ return json::array();
+ }
+ }).make();
+ return new MatrixSticker(
+ image.xform(jsonAtOr("/0"_json_pointer, json(std::string())) | zug::map([](const json &j) {
+ return j.template get<std::string>();
+ })),
+ image.xform(jsonAtOr("/1"_json_pointer, json::object()))
+ );
+}
+
+bool MatrixStickerPack::hasShortCode(const QString &shortCode) const
+{
+ return m_event.map([shortCode=shortCode.toStdString()](const Event &e) {
+ auto content = e.content();
+ return content.get().contains("images")
+ && content.get()["images"].is_object()
+ && content.get()["images"].contains(shortCode);
+ })
+ .make().get();
+}
+
+QVariant MatrixStickerPack::data(const QModelIndex &/* index */, int /* role */) const
+{
+ return QVariant();
+}
+
+int MatrixStickerPack::rowCount(const QModelIndex &parent = QModelIndex()) const
+{
+ if (parent.isValid()) {
+ return 0;
+ } else {
+ return count();
+ }
+}
+
+void MatrixStickerPack::updateInternalCount()
+{
+ auto curCount = count();
+ auto oldCount = m_internalCount;
+ auto diff = std::abs(curCount - oldCount);
+
+ if (curCount > oldCount) {
+ beginInsertRows(QModelIndex(), 0, diff - 1);
+ m_internalCount = curCount;
+ endInsertRows();
+ } else if (curCount < oldCount) {
+ beginRemoveRows(QModelIndex(), 0, diff - 1);
+ m_internalCount = curCount;
+ endRemoveRows();
+ }
+}
+
+QVariant MatrixStickerPack::addSticker(const QString &shortCode, MatrixEvent *event) const
+{
+ if (!event) {
+ return QVariant::fromValue(m_source.get());
+ }
+
+ auto source = m_source.get();
+
+ auto eventJson = source.event.raw().get();
+
+ eventJson.merge_patch(json{
+ {"content", {
+ {"images", {
+ {shortCode.toStdString(), json::object()},
+ }},
+ }},
+ });
+ auto &sticker = eventJson["content"]["images"][shortCode.toStdString()];
+
+ auto content = event->content();
+ auto body = event->content()["body"];
+ if (!body.isUndefined()) {
+ sticker["body"] = body;
+ }
+ auto url = event->content()["url"];
+ if (!url.isUndefined()) {
+ sticker["url"] = url;
+ }
+ auto info = event->content()["info"];
+ if (!info.isUndefined()) {
+ sticker["info"] = info;
+ }
+
+ source.event = Event(eventJson);
+
+ return QVariant::fromValue(source);
+}
diff --git a/src/matrix-sticker.hpp b/src/matrix-sticker.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker.hpp
@@ -0,0 +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/matrix-sticker.cpp b/src/matrix-sticker.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-sticker.cpp
@@ -0,0 +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/meta-types.hpp b/src/meta-types.hpp
--- a/src/meta-types.hpp
+++ b/src/meta-types.hpp
@@ -11,15 +11,17 @@
#include <QMetaType>
#include <base/kazvevents.hpp>
+#include "matrix-sticker-pack-source.hpp"
#include "kazv-platform.hpp"
Q_DECLARE_METATYPE(Kazv::KazvEvent)
+Q_DECLARE_METATYPE(MatrixStickerPackSource)
-#if !KAZV_IS_WINDOWS
class KazvMetaTypeRegistration
{
- static KazvMetaTypeRegistration instance;
public:
+ KazvMetaTypeRegistration();
+
int m_kazvEvent;
+ int m_matrixStickerPackSource;
};
-#endif
diff --git a/src/meta-types.cpp b/src/meta-types.cpp
--- a/src/meta-types.cpp
+++ b/src/meta-types.cpp
@@ -1,6 +1,6 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
@@ -10,8 +10,8 @@
#include "meta-types.hpp"
#include "kazv-platform.hpp"
-#if !KAZV_IS_WINDOWS
-KazvMetaTypeRegistration KazvMetaTypeRegistration::instance{
- qRegisterMetaType<Kazv::KazvEvent>()
- };
-#endif
+KazvMetaTypeRegistration::KazvMetaTypeRegistration()
+ : m_kazvEvent(qRegisterMetaType<Kazv::KazvEvent>())
+ , m_matrixStickerPackSource(qRegisterMetaType<MatrixStickerPackSource>())
+{
+}
diff --git a/src/qt-json.hpp b/src/qt-json.hpp
--- a/src/qt-json.hpp
+++ b/src/qt-json.hpp
@@ -13,6 +13,7 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
+#include <cmath>
inline void to_json(nlohmann::json& j, const QJsonObject& qj);
inline void from_json(const nlohmann::json& j, QJsonObject& qj);
@@ -30,7 +31,19 @@
} else if (qj.isBool()) {
j = qj.toBool();
} else if (qj.isDouble()) {
- j = qj.toDouble();
+ // Qt's json library does not distinguish between the numeric types
+ // and stores them all as doubles.
+ // nlohmann's json, however, differentiate between integers and
+ // floating point numbers, and will serialize them differently.
+ // Prefer using ints here, because canonical json in the Matrix spec
+ // uses ints, not floats.
+ auto num = qj.toDouble();
+ auto floor = std::floor(num);
+ if (floor == num) {
+ j = static_cast<long long>(floor);
+ } else {
+ j = num;
+ }
} else if (qj.isString()) {
j = qj.toString().toStdString();
}
diff --git a/src/qt-promise-handler.hpp b/src/qt-promise-handler.hpp
--- a/src/qt-promise-handler.hpp
+++ b/src/qt-promise-handler.hpp
@@ -1,6 +1,6 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -17,8 +17,6 @@
#include <promise-interface.hpp>
-#include "kazv-log.hpp"
-
template<class T>
class QtPromise;
@@ -45,6 +43,18 @@
}
}
+class QtPromiseSignalTrigger : public QObject
+{
+ Q_OBJECT
+public:
+ QtPromiseSignalTrigger(QObject *parent = 0);
+
+ ~QtPromiseSignalTrigger();
+
+Q_SIGNALS:
+ void finished();
+};
+
template<class T>
class QtPromise : public Kazv::AbstractPromise<QtPromiseDetail::QtPromiseHelper::PromiseType, T>
{
@@ -53,29 +63,32 @@
template<class FuncT, class PromiseT, class ResolveT>
struct WaitHelper
{
- void wait() const {
- if (p.ready()) {
- auto res = func(p.get());
- using ResT = decltype(res);
- if constexpr (Kazv::isPromise<ResT>) {
- QtPromiseDetail::post(
- executor,
- [w=WaitHelper<QtPromiseDetail::IdentityFunc, ResT, ResolveT>{
- executor,
- res,
- QtPromiseDetail::IdentityFunc{},
- resolve,
- }]() mutable {
- w.wait();
- });
- } else {
- resolve(res);
- }
+ void resolveOrWait()
+ {
+ auto res = func(p.get());
+ using ResT = decltype(res);
+ if constexpr (Kazv::isPromise<ResT>) {
+ res.then([resolve=resolve](auto val) {
+ resolve(std::move(val));
+ return std::decay_t<decltype(val)>(); // we don't care about this value
+ });
} else {
+ resolve(res);
+ }
+ }
+
+ void wait()
+ {
+ if (p.ready()) {
QtPromiseDetail::post(
executor,
- [*this]() mutable {
- wait();
+ [*this]() mutable { resolveOrWait(); }
+ );
+ } else {
+ QObject::connect(
+ p.m_signalTrigger, &QtPromiseSignalTrigger::finished,
+ executor, [*this]() mutable {
+ resolveOrWait();
}
);
}
@@ -90,28 +103,50 @@
struct ResolveHelper
{
template<class ValT>
- void operator()(ValT val) const {
+ void operator()(ValT val) const
+ {
using ResT = std::decay_t<decltype(val)>;
if constexpr (Kazv::isPromise<ResT>) {
- auto w = WaitHelper<QtPromiseDetail::IdentityFunc, ResT, ResolveHelper>{
+ QtPromiseDetail::post(
executor,
- val,
- QtPromiseDetail::IdentityFunc{},
- *this
- };
- w.wait();
+ [qtPromise=std::move(val), *this]() mutable {
+ if (qtPromise.ready()) {
+ setValue(qtPromise.get());
+ } else {
+ qtPromise.then([*this](auto val) mutable {
+ setValue(std::move(val));
+ return std::decay_t<decltype(val)>(); // we don't care about this value
+ });
+ }
+ });
} else {
- p->set_value(std::move(val));
+ QtPromiseDetail::post(
+ executor,
+ [*this, val=std::move(val)]() mutable {
+ setValue(std::move(val));
+ });
+ }
+ }
+
+ void setValue(T resolvedValue) const
+ {
+ p->set_value(std::move(resolvedValue));
+ if (signalTrigger) {
+ Q_EMIT signalTrigger->finished();
+ signalTrigger->deleteLater();
}
}
QPointer<QObject> executor;
+ QPointer<QtPromiseSignalTrigger> signalTrigger;
std::shared_ptr<std::promise<T>> p;
};
public:
QtPromise(QObject *executor, T value)
: BaseT(this)
- , m_executor(executor) {
+ , m_executor(executor)
+ , m_signalTrigger()
+ {
std::promise<T> p;
m_val = p.get_future().share();
p.set_value(std::move(value));
@@ -120,10 +155,14 @@
template<class Func>
QtPromise(QObject *executor, Func &&callback)
: BaseT(this)
- , m_executor(executor) {
+ , m_executor(executor)
+ , m_signalTrigger(new QtPromiseSignalTrigger())
+ {
auto p = std::make_shared<std::promise<T>>();
m_val = p->get_future().share();
- auto resolve = ResolveHelper{m_executor, p};
+
+ m_signalTrigger->moveToThread(m_executor->thread());
+ auto resolve = ResolveHelper{m_executor, m_signalTrigger, p};
QtPromiseDetail::post(
m_executor,
@@ -162,6 +201,7 @@
}
private:
QPointer<QObject> m_executor;
+ QPointer<QtPromiseSignalTrigger> m_signalTrigger;
std::shared_future<T> m_val;
};
diff --git a/src/qt-promise-handler.cpp b/src/qt-promise-handler.cpp
new file mode 100644
--- /dev/null
+++ b/src/qt-promise-handler.cpp
@@ -0,0 +1,15 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include "qt-promise-handler.hpp"
+
+QtPromiseSignalTrigger::QtPromiseSignalTrigger(QObject *parent)
+ : QObject(parent)
+{}
+
+QtPromiseSignalTrigger::~QtPromiseSignalTrigger() = default;
diff --git a/src/register-types.cpp b/src/register-types.cpp
--- a/src/register-types.cpp
+++ b/src/register-types.cpp
@@ -1,6 +1,6 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2021-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -14,6 +14,8 @@
#include "matrix-room-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-room-member-list-model.hpp"
+#include "matrix-event-reader.hpp"
+#include "matrix-event-reader-list-model.hpp"
#include "matrix-event.hpp"
#include "l10n-provider.hpp"
#include "kazv-config.hpp"
@@ -24,6 +26,9 @@
#include "device-mgmt/matrix-device.hpp"
#include "matrix-promise.hpp"
#include "kazv-util.hpp"
+#include "matrix-sticker.hpp"
+#include "matrix-sticker-pack.hpp"
+#include "matrix-sticker-pack-list.hpp"
void registerKazvQmlTypes()
{
@@ -39,9 +44,14 @@
qmlRegisterUncreatableType<MatrixRoomMemberListModel>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoomMemberListModel", "");
qmlRegisterUncreatableType<MatrixRoomTimeline>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoomTimeline", "");
qmlRegisterUncreatableType<MatrixEvent>("moe.kazv.mxc.kazv", 0, 0, "MatrixEvent", "");
+ qmlRegisterUncreatableType<MatrixEventReader>("moe.kazv.mxc.kazv", 0, 0, "MatrixEventReader", "");
+ qmlRegisterUncreatableType<MatrixEventReaderListModel>("moe.kazv.mxc.kazv", 0, 0, "MatrixEventReaderListModel", "");
qmlRegisterUncreatableType<MatrixDeviceList>("moe.kazv.mxc.kazv", 0, 0, "MatrixDeviceList", "");
qmlRegisterUncreatableType<MatrixDevice>("moe.kazv.mxc.kazv", 0, 0, "MatrixDevice", "");
qmlRegisterUncreatableType<MatrixPromise>("moe.kazv.mxc.kazv", 0, 0, "MatrixPromise", "");
+ qmlRegisterUncreatableType<MatrixSticker>("moe.kazv.mxc.kazv", 0, 0, "MatrixSticker", "");
+ qmlRegisterUncreatableType<MatrixStickerPack>("moe.kazv.mxc.kazv", 0, 0, "MatrixStickerPack", "");
+ qmlRegisterUncreatableType<MatrixStickerPackList>("moe.kazv.mxc.kazv", 0, 0, "MatrixStickerPackList", "");
qmlRegisterSingletonInstance<KazvUtil>("moe.kazv.mxc.kazv", 0, 0, "KazvUtil", new KazvUtil());
qmlRegisterSingletonInstance<ShortcutUtil>("moe.kazv.mxc.kazvshortcuts", 0, 0, "ShortcutUtil", new ShortcutUtil());
}
diff --git a/src/resources.qrc b/src/resources.qrc
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -18,6 +18,8 @@
<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="AddStickerPopup.qml">contents/ui/AddStickerPopup.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>
@@ -34,6 +36,7 @@
<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="EventReadIndicator.qml">contents/ui/EventReadIndicator.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>
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -9,6 +9,7 @@
ecm_add_tests(
qt-job-handler-test.cpp
qt-promise-handler-test.cpp
+ qt-json-test.cpp
device-mgmt-test.cpp
matrix-sdk-test.cpp
matrix-promise-test.cpp
@@ -16,6 +17,7 @@
matrix-room-member-list-model-test.cpp
matrix-room-list-test.cpp
kazv-markdown-test.cpp
+ matrix-sticker-pack-test.cpp
LINK_LIBRARIES Qt5::Test kazvtestlib
)
diff --git a/src/tests/matrix-room-list-test.cpp b/src/tests/matrix-room-list-test.cpp
--- a/src/tests/matrix-room-list-test.cpp
+++ b/src/tests/matrix-room-list-test.cpp
@@ -32,6 +32,7 @@
void testRoomList();
void testSorted();
void testSortedWithTag();
+ void testFilter();
};
static auto tagEvent = Event{R"({
@@ -134,6 +135,53 @@
QCOMPARE(roomList->roomIdAt(2), QString::fromStdString(room2.roomId));
}
+void MatrixRoomListTest::testFilter()
+{
+ auto model = makeTestModel();
+ model.client = makeClient();
+ auto room1 = makeRoom(
+ withRoomState({makeEvent(withStateKey("") | withEventType("m.room.name") | withEventContent(json{{"name", "some room"}}))})
+ );
+ auto room2 = makeRoom(
+ withRoomState({makeEvent(withStateKey("") | withEventType("m.room.name") | withEventContent(json{{"name", "some other room"}}))})
+ );
+ auto room3 = makeRoom(
+ withRoomId("!some:example.org")
+ | withRoomState({
+ makeMemberEvent(withStateKey("@foo:tusooa.xyz") | withEventKV(json::json_pointer("/content/displayname"), "User aaa")),
+ makeMemberEvent(withStateKey("@bar:tusooa.xyz") | withEventKV(json::json_pointer("/content/displayname"), "User bbb")),
+ })
+ );
+ room3.heroIds = immer::flex_vector<std::string>{"@foo:tusooa.xyz", "@bar:tusooa.xyz"};
+
+ withRoom(room1)(model.client);
+ withRoom(room2)(model.client);
+ withRoom(room3)(model.client);
+
+ std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
+ auto roomList = toUniquePtr(sdk->roomList());
+ roomList->setfilter("some");
+
+ QCOMPARE(roomList->count(), 2);
+ QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room2.roomId));
+ QCOMPARE(roomList->roomIdAt(1), QString::fromStdString(room1.roomId));
+
+ roomList->setfilter("!some:example.org");
+
+ QCOMPARE(roomList->count(), 1);
+ QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
+
+ roomList->setfilter("foo");
+
+ QCOMPARE(roomList->count(), 1);
+ QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
+
+ roomList->setfilter("aaa");
+
+ QCOMPARE(roomList->count(), 1);
+ QCOMPARE(roomList->roomIdAt(0), QString::fromStdString(room3.roomId));
+}
+
QTEST_MAIN(MatrixRoomListTest)
#include "matrix-room-list-test.moc"
diff --git a/src/tests/matrix-room-timeline-test.cpp b/src/tests/matrix-room-timeline-test.cpp
--- a/src/tests/matrix-room-timeline-test.cpp
+++ b/src/tests/matrix-room-timeline-test.cpp
@@ -11,16 +11,21 @@
#include <QtTest>
+#include <factory.hpp>
+
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event.hpp>
+#include <matrix-event-reader-list-model.hpp>
+#include <matrix-event-reader.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Kazv;
+using namespace Kazv::Factory;
class MatrixRoomTimelineTest : public QObject
{
@@ -28,6 +33,8 @@
private Q_SLOTS:
void testLocalEcho();
+ void testReadReceipts();
+ void testEdits();
};
void MatrixRoomTimelineTest::testLocalEcho()
@@ -51,6 +58,76 @@
QCOMPARE(second->txnId(), "some-other-txn-id");
}
+void MatrixRoomTimelineTest::testReadReceipts()
+{
+ auto r = makeRoom(withRoomTimeline({
+ makeEvent(withEventId("$1")),
+ makeEvent(withEventId("$2")),
+ }));
+ r.eventReadUsers = {{"$1", {"@a:example.com", "@b:example.com"}}};
+ r.readReceipts = {{"@a:example.com", {"$1", 1234}}, {"@b:example.com", {"$2", 5678}}};
+ auto model = SdkModel{makeClient(withRoom(r))};
+ std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
+ auto roomList = toUniquePtr(sdk->roomList());
+ auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+ auto timeline = toUniquePtr(room->timeline());
+ {
+ auto event = toUniquePtr(timeline->at(1));
+ QCOMPARE(event->eventId(), "$1");
+ auto readers = toUniquePtr(event->readers());
+ QCOMPARE(readers->count(), 2);
+ auto reader1 = toUniquePtr(readers->at(0));
+ QCOMPARE(reader1->userId(), "@a:example.com");
+ QCOMPARE(reader1->timestamp(), 1234);
+ auto reader2 = toUniquePtr(readers->at(1));
+ QCOMPARE(reader2->userId(), "@b:example.com");
+ QCOMPARE(reader2->timestamp(), 5678);
+ }
+
+ {
+ auto event = toUniquePtr(timeline->at(0));
+ QCOMPARE(event->eventId(), "$2");
+ auto readers = toUniquePtr(event->readers());
+ QCOMPARE(readers->count(), 0);
+ }
+}
+
+void MatrixRoomTimelineTest::testEdits()
+{
+ auto r = makeRoom(withRoomTimeline({
+ makeEvent(withEventId("$0")),
+ makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0")),
+ makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1")),
+ // valid
+ makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1")),
+ // also valid
+ makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1")),
+ // invalid, changed event type
+ makeEvent(withEventId("$5") | withEventContent(json{{"m.new_content", {{"body", "fourth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
+ // invalid, changed event sender
+ makeEvent(withEventId("$6") | withEventContent(json{{"m.new_content", {{"body", "fifth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withEventSenderId("@othersender:example.com")),
+ // invalid, has state key
+ makeEvent(withEventId("$7") | withEventContent(json{{"m.new_content", {{"body", "sixth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withStateKey("")),
+ // invalid, no m.new_content
+ makeEvent(withEventId("$8") | withEventContent(json{{"body", "seventh"}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
+ }));
+ auto model = SdkModel{makeClient(withRoom(r))};
+ std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
+ auto roomList = toUniquePtr(sdk->roomList());
+ auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+ auto timeline = toUniquePtr(room->timeline());
+
+ auto event = toUniquePtr(timeline->at(7));
+ QCOMPARE(event->eventId(), "$1");
+ // the content is taken from the newest valid edit
+ qDebug() << event->content();
+ QCOMPARE(event->content(), (QJsonObject{{"body", "third"}}));
+ // the relation is taken from the original content
+ QCOMPARE(event->relationType(), "moe.kazv.mxc.some-rel");
+ QCOMPARE(event->relatedEventId(), "$0");
+ QVERIFY(event->isEdited());
+}
+
QTEST_MAIN(MatrixRoomTimelineTest)
#include "matrix-room-timeline-test.moc"
diff --git a/src/tests/matrix-sticker-pack-test.cpp b/src/tests/matrix-sticker-pack-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/matrix-sticker-pack-test.cpp
@@ -0,0 +1,154 @@
+/*
+ * 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-event.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();
+ void testAddToPack();
+};
+
+// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
+static auto stickerPackEvent = Event{R"({
+ "content": {
+ "images": {
+ "myemote": {
+ "url": "mxc://example.org/blah"
+ },
+ "mysticker": {
+ "body": "my sticker",
+ "url": "mxc://example.org/sticker",
+ "usage": ["sticker"],
+ "info": {
+ "mimetype": "image/png"
+ }
+ }
+ },
+ "pack": {
+ "display_name": "Awesome Pack",
+ "usage": ["emoticon"]
+ }
+ },
+ "type": "im.ponies.user_emotes"
+})"_json};
+
+void MatrixStickerPackTest::testStickerPack()
+{
+ auto sourceCursor = lager::make_state(MatrixStickerPackSource{
+ MatrixStickerPackSource::AccountData,
+ "im.ponies.user_emotes",
+ stickerPackEvent,
+ }, lager::automatic_tag{});
+
+ auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
+
+ QCOMPARE(stickerPack->rowCount(QModelIndex()), 2);
+ QCOMPARE(stickerPack->count(), 2);
+ QVERIFY(stickerPack->hasShortCode(QStringLiteral("myemote")));
+ QVERIFY(!stickerPack->hasShortCode(QStringLiteral("lolol")));
+
+ auto sticker0 = toUniquePtr(stickerPack->at(0));
+ QCOMPARE(sticker0->shortCode(), QStringLiteral("myemote"));
+ QCOMPARE(sticker0->body(), QStringLiteral("myemote"));
+ QCOMPARE(sticker0->mxcUri(), QStringLiteral("mxc://example.org/blah"));
+ QCOMPARE(sticker0->info(), QJsonObject());
+ QCOMPARE(sticker0->makeEventJson(), (QJsonObject{
+ {"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{{"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);
+}
+
+void MatrixStickerPackTest::testAddToPack()
+{
+ using namespace nlohmann::literals;
+
+ auto sourceCursor = lager::make_state(MatrixStickerPackSource{
+ MatrixStickerPackSource::AccountData,
+ "im.ponies.user_emotes",
+ stickerPackEvent,
+ }, lager::automatic_tag{});
+
+ auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
+
+ auto contentJson = json{
+ {"url", "mxc://example.org/someotheremote"},
+ {"info", {{"mimetype", "image/png"}}},
+ {"body", "some other emote"},
+ };
+ auto eventCursor = lager::make_constant(makeEvent(
+ withEventContent(contentJson)
+ ));
+ auto event = toUniquePtr(new MatrixEvent(eventCursor));
+
+ auto newSource = stickerPack->addSticker("someotheremote", event.get()).template value<MatrixStickerPackSource>();
+
+ auto expected = sourceCursor.get().event.content().get();
+ expected["images"]["someotheremote"] = contentJson;
+
+ QVERIFY(newSource.event.content().get() == expected);
+}
+
+QTEST_MAIN(MatrixStickerPackTest)
+
+#include "matrix-sticker-pack-test.moc"
diff --git a/src/tests/qt-json-test.cpp b/src/tests/qt-json-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/qt-json-test.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 <QtTest>
+
+#include "qt-json.hpp"
+
+class QtJsonTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testNumericConv();
+};
+
+static auto j = nlohmann::json::parse(R"({
+ "foo": 1
+})");
+
+static auto jFloat = nlohmann::json::parse(R"({
+ "foo": 1.0
+})");
+
+static auto jFloat2 = nlohmann::json::parse(R"({
+ "foo": 1.1
+})");
+
+void QtJsonTest::testNumericConv()
+{
+ QJsonObject qj = j;
+ nlohmann::json j2 = qj;
+ QVERIFY(j2 == j);
+
+ qj = jFloat;
+ j2 = qj;
+ QVERIFY(j2 == j);
+ QVERIFY(j2 == jFloat);
+
+ qj = jFloat2;
+ j2 = qj;
+ QVERIFY(j2 == jFloat2);
+}
+
+QTEST_MAIN(QtJsonTest)
+
+#include "qt-json-test.moc"
diff --git a/src/tests/qt-promise-handler-test.hpp b/src/tests/qt-promise-handler-test.hpp
--- a/src/tests/qt-promise-handler-test.hpp
+++ b/src/tests/qt-promise-handler-test.hpp
@@ -16,4 +16,6 @@
void testPromise();
void testTimer();
void testStop();
+ void testSingleTypePromise();
+ void testResolveToPromise();
};
diff --git a/src/tests/qt-promise-handler-test.cpp b/src/tests/qt-promise-handler-test.cpp
--- a/src/tests/qt-promise-handler-test.cpp
+++ b/src/tests/qt-promise-handler-test.cpp
@@ -11,6 +11,8 @@
#include <QTimer>
+#include "promise-interface.hpp"
+
#include "qt-promise-handler-test.hpp"
#include "qt-promise-handler.hpp"
@@ -135,4 +137,76 @@
loop.exec();
}
+void QtPromiseHandlerTest::testSingleTypePromise()
+{
+ QEventLoop loop;
+ QObject *obj = new QObject(&loop);
+ auto ph = Kazv::SingleTypePromiseInterface<int>(QtPromiseHandler(std::ref(*obj)));
+
+ std::vector<int> v;
+
+ auto p1 = ph.create([&v](auto resolve) {
+ qDebug() << "p1";
+ v.push_back(1);
+ resolve(2);
+ });
+
+ auto p2 = p1.then([&v, &ph](int val) {
+ qDebug() << "p2";
+ v.push_back(val);
+ return ph.createResolved(0)
+ .then([&ph](int) {
+ qDebug() << "within p2";
+ return ph.createResolved(3);
+ });
+ });
+
+ auto p3 = p2.then([&v, &ph](int val) {
+ qDebug() << "p3";
+ v.push_back(val);
+ return ph.createResolved(-1);
+ });
+
+ auto p4 = p3.then([](int val) {
+ qDebug() << "p4" << val;
+ [=] { QVERIFY(val == -1); }();
+ return 5;
+ });
+
+ auto p5 = p4.then([obj, &loop](int val) {
+ qDebug() << "p5" << val;
+ [=] { QVERIFY(val == 5); }();
+ obj->deleteLater();
+ loop.quit();
+ return 0;
+ });
+
+ loop.exec();
+
+ QVERIFY((v == std::vector<int>{ 1, 2, 3 }));
+}
+
+void QtPromiseHandlerTest::testResolveToPromise()
+{
+ QEventLoop loop;
+ QObject *obj = new QObject(&loop);
+ auto ph = Kazv::SingleTypePromiseInterface<int>(QtPromiseHandler(std::ref(*obj)));
+
+ auto p1 = ph.createResolveToPromise([&ph](auto resolve) {
+ qDebug() << "p1";
+ resolve(ph.createResolved(1)
+ .then([&ph](auto) {
+ return ph.createResolved(2);
+ }));
+ });
+
+ p1.then([obj, &loop](int val) {
+ [=] { QVERIFY(val == 2); }();
+ obj->deleteLater();
+ loop.quit();
+ return 0;
+ });
+ loop.exec();
+}
+
QTEST_MAIN(QtPromiseHandlerTest)
diff --git a/src/tests/quick-tests/tst_AddStickerPopup.qml b/src/tests/quick-tests/tst_AddStickerPopup.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/tst_AddStickerPopup.qml
@@ -0,0 +1,142 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtTest 1.0
+import org.kde.kirigami 2.20 as Kirigami
+
+import '../../contents/ui' as Kazv
+import 'test-helpers.js' as Helpers
+import 'test-helpers' as TestHelpers
+
+Item {
+ id: item
+ width: 800
+ height: 600
+
+ property var l10n: Helpers.fluentMock
+ property var promiseComp: Component {
+ TestHelpers.MatrixPromiseMock {}
+ }
+ property var matrixSdk: TestHelpers.MatrixSdkMock {
+ id: matrixSdk
+ property var _updateStickerPackCalled: 0
+ property var _updateStickerPackLastArg: undefined
+ property var _updateStickerPackPromise: undefined
+ function updateStickerPack(arg) {
+ ++matrixSdk._updateStickerPackCalled;
+ matrixSdk._updateStickerPackLastArg = arg;
+ matrixSdk._updateStickerPackPromise = promiseComp.createObject(matrixSdk);
+ return matrixSdk._updateStickerPackPromise;
+ }
+ }
+
+ Kazv.AddStickerPopup {
+ id: addStickerPopup
+ property var _closeCalled: 0
+ event: ({
+ content: {
+ url: 'mxc://example.org/something',
+ },
+ })
+ stickerPackList: ({
+ at() {
+ return {
+ addSticker(shortCode, event) {
+ return {
+ [shortCode]: event.content
+ };
+ },
+ hasShortCode(shortCode) {
+ return shortCode === 'foo';
+ },
+ };
+ },
+ })
+
+ function close() {
+ ++addStickerPopup._closeCalled;
+ }
+ }
+
+ TestCase {
+ id: eventViewTest
+ name: 'EventViewTest'
+ when: windowShown
+
+ function init() {
+ addStickerPopup.contentItem.parent = item;
+ }
+
+ function cleanup() {
+ matrixSdk._updateStickerPackCalled = 0;
+ matrixSdk._updateStickerPackLastArg = undefined;
+ matrixSdk._updateStickerPackPromise = undefined;
+ addStickerPopup._closeCalled = 0;
+ }
+
+ function test_addSticker() {
+ findChild(addStickerPopup, 'shortCodeInput').text = 'bar';
+ verify(!findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
+ const button = findChild(addStickerPopup, 'addStickerButton');
+ verify(button.enabled);
+ // tryVerify(() => false, 50000);
+ mouseClick(button);
+ tryVerify(() => matrixSdk._updateStickerPackCalled === 1);
+ verify(Helpers.deepEqual(matrixSdk._updateStickerPackLastArg, {
+ 'bar': {
+ url: 'mxc://example.org/something',
+ }
+ }));
+
+ tryVerify(() => !button.enabled);
+ tryVerify(() => findChild(addStickerPopup, 'shortCodeInput').readOnly);
+ verify(addStickerPopup._closeCalled === 0);
+ matrixSdk._updateStickerPackPromise.resolve(true, {});
+ tryVerify(() => addStickerPopup._closeCalled === 1);
+ }
+
+ function test_addStickerFailed() {
+ findChild(addStickerPopup, 'shortCodeInput').text = 'bar';
+ verify(!findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
+ const button = findChild(addStickerPopup, 'addStickerButton');
+ verify(button.enabled);
+ // tryVerify(() => false, 50000);
+ mouseClick(button);
+ tryVerify(() => matrixSdk._updateStickerPackCalled === 1);
+ verify(Helpers.deepEqual(matrixSdk._updateStickerPackLastArg, {
+ 'bar': {
+ url: 'mxc://example.org/something',
+ }
+ }));
+ matrixSdk._updateStickerPackPromise.resolve(false, {});
+ tryVerify(() => button.enabled);
+ verify(addStickerPopup._closeCalled === 0);
+ }
+
+ function test_addStickerOverride() {
+ findChild(addStickerPopup, 'shortCodeInput').text = 'foo';
+ tryVerify(() => findChild(addStickerPopup, 'shortCodeExistsWarning').visible);
+ const button = findChild(addStickerPopup, 'addStickerButton');
+ verify(button.enabled);
+ // tryVerify(() => false, 50000);
+ mouseClick(button);
+ tryVerify(() => matrixSdk._updateStickerPackCalled === 1);
+ verify(Helpers.deepEqual(matrixSdk._updateStickerPackLastArg, {
+ 'foo': {
+ url: 'mxc://example.org/something',
+ }
+ }));
+
+ tryVerify(() => !button.enabled);
+ tryVerify(() => findChild(addStickerPopup, 'shortCodeInput').readOnly);
+ verify(addStickerPopup._closeCalled === 0);
+ matrixSdk._updateStickerPackPromise.resolve(true, {});
+ tryVerify(() => addStickerPopup._closeCalled === 1);
+ }
+ }
+}
diff --git a/src/tests/quick-tests/tst_EventReadIndicator.qml b/src/tests/quick-tests/tst_EventReadIndicator.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/tst_EventReadIndicator.qml
@@ -0,0 +1,66 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.3
+import QtQuick.Layouts 1.15
+import QtTest 1.0
+
+import '../../contents/ui' as Kazv
+import 'test-helpers.js' as Helpers
+import 'test-helpers' as TestHelpers
+
+Item {
+ id: item
+ width: 800
+ height: 600
+
+ property var l10n: Helpers.fluentMock
+ property var matrixSdk: TestHelpers.MatrixSdkMock {}
+ property var kazvIOManager: TestHelpers.KazvIOManagerMock {}
+
+ function makeModel(size) {
+ return {
+ at(index) {
+ return {
+ userId: `@u${index}:example.com`,
+ name: `user ${index}`,
+ };
+ },
+ count: size,
+ };
+ }
+
+ Kazv.EventReadIndicator {
+ id: indicator
+ }
+
+ TestCase {
+ id: eventReadIndicatorTest
+ name: 'EventReadIndicatorTest'
+ when: windowShown
+
+ function test_noUser() {
+ indicator.model = makeModel(0);
+ verify(!indicator.visible);
+ }
+
+ function test_singleUser() {
+ indicator.model = makeModel(1);
+ tryVerify(() => indicator.visible);
+ verify(findChild(indicator, 'readIndicatorAvatar0').name === 'user 0');
+ verify(!findChild(indicator, 'moreUsersIndicator').visible);
+ }
+
+ function test_tooManyUsers() {
+ indicator.model = makeModel(5);
+ tryVerify(() => indicator.visible);
+ verify(indicator.actualItems === indicator.maxItems);
+ verify(findChild(indicator, 'readIndicatorAvatar3').name === 'user 3');
+ verify(!findChild(indicator, 'readIndicatorAvatar4'));
+ verify(findChild(indicator, 'moreUsersIndicator').visible);
+ }
+ }
+}
diff --git a/src/tests/quick-tests/tst_StickerPicker.qml b/src/tests/quick-tests/tst_StickerPicker.qml
new file mode 100644
--- /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/plain
Expires
Sat, Nov 30, 11:58 AM (14 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41676
Default Alt Text
D12.1732996701.diff (95 KB)

Event Timeline