Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F116392
D12.1732996701.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
95 KB
Referenced Files
None
Subscribers
None
D12.1732996701.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D12: Get rid of spin-wait Promises
Attached
Detach File
Event Timeline
Log In to Comment