Page MenuHomePhorge

No OneTemporary

Size
101 KB
Referenced Files
None
Subscribers
None
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8d77fe8..f49eb19 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,292 +1,295 @@
function(kazv_add_qml_files)
set(options "")
set(oneValueArgs BASE_DIRECTORY)
set(multiValueArgs SOURCES)
cmake_parse_arguments(PARSE_ARGV 0 kazv_add_qml_files "${options}" "${oneValueArgs}" "${multiValueArgs}")
set(target "${kazv_add_qml_files_UNPARSED_ARGUMENTS}")
set(allSources)
foreach(source ${kazv_add_qml_files_SOURCES})
message(STATUS "Adding source: ${kazv_add_qml_files_BASE_DIRECTORY} / ${source}")
set_source_files_properties(
"${kazv_add_qml_files_BASE_DIRECTORY}/${source}"
PROPERTIES
QT_RESOURCE_ALIAS "${source}"
)
list(APPEND allSources "${kazv_add_qml_files_BASE_DIRECTORY}/${source}")
endforeach()
qt_target_qml_sources(
"${target}"
QML_FILES ${allSources}
)
endfunction()
if(WIN32)
set(KAZV_IS_WINDOWS 1)
else()
set(KAZV_IS_WINDOWS 0)
endif()
configure_file(kazv-version.cpp.in kazv-version.cpp)
configure_file(kazv-platform.hpp.in kazv-platform.hpp)
configure_file(kazv-defs.hpp.in kazv-defs.hpp)
set(kazvqmlmodule_SRCS
qt-job-handler.cpp
qt-job.cpp
${CMAKE_CURRENT_BINARY_DIR}/kazv-version.cpp
matrix-sdk.cpp
kazv-session-lock-guard.cpp
matrix-room.cpp
matrix-room-list.cpp
matrix-room-timeline.cpp
matrix-room-pinned-events-timeline.cpp
matrix-room-member.cpp
matrix-room-member-list-model.cpp
matrix-event-reader.cpp
matrix-event-reader-list-model.cpp
+ matrix-event-reaction-list-model.cpp
+ matrix-event-reaction.cpp
matrix-event.cpp
matrix-event-list.cpp
matrix-link.cpp
meta-types.cpp
l10n-provider.cpp
qt-rand-adapter.cpp
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
matrix-user-given-attrs-map.cpp
kazv-abstract-list-model.cpp
qt-promise-handler.cpp
device-mgmt/matrix-device.cpp
device-mgmt/matrix-device-list.cpp
kazv-config.cpp
kazv-path-config.cpp
kazv-io-manager.cpp
kazv-io-job.cpp
kazv-file.cpp
upload-job-model.cpp
kazv-markdown.cpp
kazv-clipboard.cpp
)
ecm_qt_declare_logging_category(kazvqmlmodule_SRCS
HEADER kazv-log.hpp
IDENTIFIER kazvLog
CATEGORY_NAME moe.kazv.mxc.kazv
)
qt_add_qml_module(kazvqmlmodule
STATIC
URI moe.kazv.mxc.kazv
VERSION 0.0
SOURCES ${kazvqmlmodule_SRCS}
)
target_link_libraries(kazvqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Network
Threads::Threads
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
Qt${QT_MAJOR_VERSION}::Quick
Qt${QT_MAJOR_VERSION}::QuickControls2
Qt${QT_MAJOR_VERSION}::Svg
Qt${QT_MAJOR_VERSION}::Concurrent
Qt${QT_MAJOR_VERSION}::Widgets
KF${KF_MAJOR_VERSION}::ConfigCore KF${KF_MAJOR_VERSION}::KIOCore
KF${KF_MAJOR_VERSION}::Notifications
KF${KF_MAJOR_VERSION}::CoreAddons
${CMARK_TARGET_NAME}
)
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/device-mgmt)
qt_add_qml_module(kazvshortcutsqmlmodule
STATIC
URI moe.kazv.mxc.kazvshortcuts
VERSION 0.0
SOURCES
shortcuts/shortcut-util.cpp
)
target_link_libraries(kazvshortcutsqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
)
target_include_directories(kazvshortcutsqmlmodule
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/shortcuts
)
add_library(kazvprivlib INTERFACE)
target_link_libraries(kazvprivlib
INTERFACE
kazvqmlmodule
kazvqmlmoduleplugin
kazvshortcutsqmlmodule
kazvshortcutsqmlmoduleplugin
)
qt_add_qml_module(kazvqmlqmlmodule
STATIC
URI moe.kazv.mxc.kazvqml
VERSION 0.0
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY contents/ui
SOURCES
Main.qml
About.qml
PageManager.qml
LoginPage.qml
MainPage.qml
TabView.qml
Tab.qml
ClosableScrollablePage.qml
SelfDestroyableOverlaySheet.qml
RoomListView.qml
RoomListViewItemDelegate.qml
RoomPage.qml
RoomPinnedEventsPage.qml
RoomTimelineView.qml
SendMessageBox.qml
EventView.qml
EventViewWrapper.qml
EventViewCompact.qml
EventSourceView.qml
EventHistoryView.qml
+ EventReactions.qml
Bubble.qml
MediaFileMenu.qml
KazvIOMenu.qml
ConfirmUploadPopup.qml
StickerPicker.qml
AddStickerPopup.qml
StickerPackNameProvider.qml
ConfirmationOverlay.qml
event-types/Simple.qml
event-types/Text.qml
event-types/Emote.qml
event-types/Notice.qml
event-types/State.qml
event-types/TextTemplate.qml
event-types/Image.qml
event-types/File.qml
event-types/Video.qml
event-types/Audio.qml
event-types/MediaBubble.qml
event-types/Redacted.qml
event-types/Reaction.qml
event-types/Fallback.qml
TypingIndicator.qml
EventReadIndicator.qml
SelectableText.qml
ReactToEventPopup.qml
FileHandler.qml
ActionSettingsPage.qml
CreateRoomPage.qml
JoinRoomPage.qml
RoomMemberListView.qml
RoomMemberListViewItemDelegate.qml
UserNameProvider.qml
RoomNameProvider.qml
Completion.qml
AsyncHandler.qml
UploadFileHelper.qml
SettingsPage.qml
settings/ProfileSettings.qml
settings/CacheSettings.qml
room-settings/RoomSettingsPage.qml
room-settings/RoomTagHandler.qml
room-settings/RoomMemberListPage.qml
room-settings/RoomInvitePage.qml
room-settings/RoomStickerPacksPage.qml
room-settings/RoomStickerPackItemDelegate.qml
device-mgmt/Device.qml
device-mgmt/DeviceList.qml
UserPage.qml
shortcuts/ActionCollection.qml
shortcuts/ActionItem.qml
shortcuts/ActionSettings.qml
shortcuts/ShortcutInput.qml
MessageNotification.qml
Notifier.qml
NotificationAction.qml
AvatarAdapter.qml
ImageAdapter.qml
FileDialogAdapter.qml
FolderDialogAdapter.qml
ScrollablePageAdapter.qml
emoji/Activities.qml
emoji/Flags.qml
emoji/Objects.qml
emoji/SmileysAndEmotion.qml
emoji/TravelAndPlaces.qml
emoji/AnimalsAndNature.qml
emoji/FoodAndDrink.qml
emoji/PeopleAndBody.qml
emoji/Symbols.qml
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js
SOURCES
l10n.js
global-this.js
matrix-helpers.js
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js/transformed-libs
SOURCES
fluent-bundle.js
fluent-sequence.js
fluent-langneg.js
bundled-deps.js
)
target_link_libraries(kazvqmlqmlmodule PUBLIC kazvprivlib)
set(kazv_SRCS
main.cpp
l10n/translations.qrc
)
add_executable(kazv ${kazv_SRCS})
target_link_libraries(kazv
PRIVATE
kazvqmlqmlmodule
kazvqmlqmlmoduleplugin
kazvprivlib
)
if(kazv_LINK_BREEZE_ICONS)
target_link_libraries(kazv PRIVATE KF${KF_MAJOR_VERSION}::BreezeIcons)
endif()
set(kazv_INSTALL_ARGS ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
if(NOT kazv_INSTALL_ARGS)
set(kazv_INSTALL_ARGS ${KF${KF_MAJOR_VERSION}_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
install(TARGETS kazv ${kazv_INSTALL_ARGS})
add_subdirectory(tests)
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
index 40f9679..8709a56 100644
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -1,262 +1,270 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
import 'matrix-helpers.js' as Helpers
ItemDelegate {
id: upper
objectName: 'bubble'
default property var children
property var currentEvent: event
property var menuContent: []
property var shouldDisplayTime: displayTime || (!compactMode && !event.isLocalEcho)
property var eventPinned: event.eventId && pinnedEvents.eventIds.includes(event.eventId)
+ readonly property var eventReactions: event.reactions()
topPadding: 0
bottomPadding: 0
readonly property var bubbleSpacing: leftPadding + rightPadding
readonly property double replyMaxHeight: Kirigami.Units.gridUnit * 7
Layout.fillWidth: true
property var backgroundItem: Rectangle {
anchors.fill: parent
color: (hovered || isSelected) ? Kirigami.Theme.activeBackgroundColor : Kirigami.Theme.backgroundColor
}
background: compactMode ? null : backgroundItem
property var eventSourcePopupComp: Component {
Kazv.SelfDestroyableOverlaySheet {
shouldSelfDestroy: true
title: l10n.get('event-source-popup-title')
Kazv.EventSourceView {
Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
event: upper.currentEvent
}
}
}
property var reactionPopupComp: Component {
Kazv.ReactToEventPopup {
shouldSelfDestroy: true
onAccepted: eventView.reactWith(text)
}
}
function getIsEditable(event) {
return event.sender === matrixSdk.userId
&& event.type === 'm.room.message'
&& event.content.msgtype === 'm.text';
}
property var menuComp: Component {
Menu {
objectName: 'bubbleContextMenu'
property list<QtObject> staticItems: [
Kirigami.Action {
objectName: 'replyToMenuItem'
text: l10n.get('event-reply-action')
onTriggered: setDraftRelation('m.in_reply_to', currentEvent.eventId)
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'editMenuItem'
text: l10n.get('event-edit-action')
onTriggered: {
setDraftRelation('m.replace', currentEvent.eventId);
replaceDraftRequested(Helpers.getEventBodyForEditing(event));
}
enabled: event && !event.redacted && !event.isLocalEcho && getIsEditable(event)
},
Kirigami.Action {
objectName: 'deleteMenuItem'
text: l10n.get('event-delete')
onTriggered: deleteEventRequested(event.isLocalEcho ? event.txnId : event.eventId, event.isLocalEcho)
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'reactMenuItem'
text: l10n.get('event-react-action')
onTriggered: reactionPopupComp.createObject(applicationWindow().overlay).open()
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'pinUnpinMenuItem'
enabled: !event.isLocalEcho
text: eventPinned ? l10n.get('event-unpin-action') : l10n.get('event-pin-action')
onTriggered: eventPinned ? unpinEventRequested(event.eventId) : pinEventRequested(event.eventId)
},
MenuSeparator {},
Kirigami.Action {
text: l10n.get('event-view-source')
onTriggered: eventSourcePopupComp.createObject(applicationWindow().overlay).open()
},
Kirigami.Action {
objectName: 'viewHistoryMenuItem'
text: l10n.get('event-view-history')
enabled: event && !!event.isEdited
onTriggered: viewEventHistoryRequested(event.eventId)
}
]
contentData: Array.from(staticItems).concat(upper.menuContent)
onClosed: destroy()
}
}
function shouldPopupMenu() {
return !compactMode
}
function maybePopupMenu() {
if (shouldPopupMenu()) {
menuComp.createObject(parent).popup(parent)
}
}
contentItem: RowLayout {
id: bubbleRootLayout
property var replyToOrAnnotatedEventId: currentEvent.replyingToEventId || (currentEvent.relationType === 'm.annotation' ? currentEvent.relatedEventId : '')
ColumnLayout {
Layout.fillWidth: true
MouseArea {
visible: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
Layout.fillWidth: true
Layout.margins: 0
Layout.preferredHeight: inReplyToLayout.implicitHeight
Layout.maximumHeight: upper.replyMaxHeight
clip: true
onClicked: eventListView.goToEvent(bubbleRootLayout.replyToOrAnnotatedEventId)
RowLayout {
id: inReplyToLayout
anchors {
top: parent.top
left: parent.left
right: parent.right
}
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: Kirigami.Units.smallSpacing
color: Kirigami.Theme.activeTextColor
clip: true
}
Loader {
id: inReplyToLoader
clip: true
source: Qt.resolvedUrl('EventViewCompact.qml')
active: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
asynchronous: true
Layout.fillWidth: true
property var event: room.messageById(bubbleRootLayout.replyToOrAnnotatedEventId)
property var props: ({
event,
sender: room.member(event.sender || matrixSdk.userId),
stateKeyUser: event.stateKey ? room.member(event.stateKey) : {},
isGapped: timeline.gaps.includes(event.eventId),
})
}
}
}
ColumnLayout {
id: children
data: upper.children
}
+
+ Kazv.EventReactions {
+ objectName: 'eventReactions'
+ visible: !compactMode
+ Layout.fillWidth: true
+ reactions: upper.eventReactions
+ }
}
property var popupAction: Kirigami.Action {
id: popupAction
text: l10n.get('event-popup-action')
icon.name: 'view-more-horizontal-symbolic'
onTriggered: maybePopupMenu()
}
RowLayout {
Layout.alignment: Qt.AlignBottom
Kirigami.Icon {
source: 'emblem-encrypted-locked'
visible: !compactMode && event && event.encrypted
Layout.preferredHeight: inlineBadgeSize
Layout.preferredWidth: inlineBadgeSize
Accessible.role: Accessible.StaticText
Accessible.name: l10n.get('event-encrypted')
ToolTip.text: l10n.get('event-encrypted')
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hoverHandlerEncIcon.hovered
HoverHandler { id: hoverHandlerEncIcon }
}
ToolButton {
objectName: 'localEchoIndicator'
visible: !!event.isLocalEcho
hoverEnabled: true
enabled: event.isFailed
icon.name: event.isSending ? 'state-sync' : 'state-warning'
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Accessible.role: event.isSending ? Accessible.StaticText : Accessible.Button
Accessible.name: event.isSending ? l10n.get('event-sending') : l10n.get('event-resend')
ToolTip.text: event.isSending ? l10n.get('event-sending') : l10n.get('event-send-failed')
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hovered
onClicked: {
room.resendMessage(event.txnId);
}
}
Label {
objectName: 'timeIndicator'
visible: shouldDisplayTime
text: event.formattedTime
ToolTip.text: event.formattedDateTime
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
ToolTip.visible: hoverHandlerTime.hovered
HoverHandler { id: hoverHandlerTime }
font: Kirigami.Theme.smallFont
}
Label {
objectName: 'editedIndicator'
visible: !compactMode && event.isEdited
text: l10n.get('event-edited-indicator')
font: Kirigami.Theme.smallFont
}
Kazv.EventReadIndicator {
objectName: 'eventReadIndicator'
shouldShow: !compactMode && !event.isLocalEcho
model: event.readers()
}
ToolButton {
id: moreButton
objectName: 'moreButton'
visible: !compactMode
action: popupAction
display: AbstractButton.IconOnly
}
}
}
}
diff --git a/src/contents/ui/EventReactions.qml b/src/contents/ui/EventReactions.qml
new file mode 100644
index 0000000..6e4a630
--- /dev/null
+++ b/src/contents/ui/EventReactions.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
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+import moe.kazv.mxc.kazv 0.0 as MK
+
+Flow {
+ required property var reactions
+ Repeater {
+ model: reactions
+ delegate: RowLayout {
+ id: reaction
+ objectName: `reaction${index}`
+ spacing: -1
+ Kirigami.Theme.colorSet: Kirigami.Theme.Button
+ required property int index
+ readonly property var item: reactions.at(index)
+ readonly property string key: item.key
+ readonly property double commonHeight: Math.max(content.implicitHeight, count.implicitHeight)
+ readonly property string selfReactEventId: getSelfReactEventId()
+
+ function getSelfReactEventId() {
+ for (let i = 0; i < item.count; ++i) {
+ const e = item.at(i);
+ if (e.sender === matrixSdk.userId) {
+ return e.eventId;
+ }
+ }
+ return '';
+ }
+
+ ItemDelegate {
+ id: content
+ objectName: 'reactionContent'
+ Layout.preferredHeight: reaction.commonHeight
+ rightInset: 0
+ contentItem: Text {
+ objectName: 'reactionContentText'
+ textFormat: Qt.RichText
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ // See the comments in event-types/Reaction.qml
+ text: '<span style="font-family: emoji, sans-serif;">' + MK.KazvUtil.escapeHtml(key) + '</span>'
+ Accessible.description: l10n.get('event-react-button-description', { key })
+ color: content.checked ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor
+ }
+ checked: !!reaction.selfReactEventId
+ onClicked: reaction.selfReactEventId ? deleteEventRequestedNoConfirm(reaction.selfReactEventId, /* isLocalEcho = */ false) : eventView.reactWith(key)
+ background: Rectangle {
+ topLeftRadius: Kirigami.Units.cornerRadius
+ bottomLeftRadius: Kirigami.Units.cornerRadius
+ border.color: Kirigami.Theme.highlightColor
+ color: content.hovered
+ ? Kirigami.Theme.hoverColor
+ : content.checked
+ ? Kirigami.Theme.highlightColor
+ : 'transparent'
+ }
+ }
+
+ ItemDelegate {
+ id: count
+ objectName: 'reactionCount'
+ Layout.preferredHeight: reaction.commonHeight
+ leftInset: 0
+ contentItem: Text {
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ text: `${reaction.item.count}`
+ }
+ background: Rectangle {
+ topRightRadius: Kirigami.Units.cornerRadius
+ bottomRightRadius: Kirigami.Units.cornerRadius
+ border.color: Kirigami.Theme.highlightColor
+ color: count.hovered
+ ? Kirigami.Theme.hoverColor
+ : 'transparent'
+ }
+ }
+ }
+ }
+}
diff --git a/src/contents/ui/EventView.qml b/src/contents/ui/EventView.qml
index bf2a1c7..f0b1989 100644
--- a/src/contents/ui/EventView.qml
+++ b/src/contents/ui/EventView.qml
@@ -1,277 +1,279 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.13 as Kirigami
import 'event-types' as Types
Item {
id: eventView
property var event
property var prevEvent
property var prevEventType: prevEvent ? getMessageType(prevEvent) : 'ignore'
property var sender
property var stateKeyUser
property var isSelected: false
property var displayTime
property var messageType: getMessageType(event)
property var iconSize: Kirigami.Units.iconSizes.large
property var inlineBadgeSize: Kirigami.Units.iconSizes.smallMedium
property var senderNameSize: Kirigami.Units.gridUnit * 1.2
property var isGapped: false
property var compactMode: false
property var calculatedHeight: (loader.item ? loader.item.implicitHeight : 0)
height: calculatedHeight
implicitHeight: calculatedHeight
Layout.minimumHeight: calculatedHeight
Layout.preferredHeight: calculatedHeight
Component {
id: textEV
Types.Text {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: emoteEV
Types.Emote {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: noticeEV
Types.Notice {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: stateEV
Types.State {
event: eventView.event
sender: eventView.sender
stateKeyUser: eventView.stateKeyUser
isSelected: eventView.isSelected
}
}
Component {
id: imageEV
Types.Image {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: fileEV
Types.File {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: videoEV
Types.Video {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: audioEV
Types.Audio {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: unknownEV
Types.Fallback {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: redactedEV
Types.Redacted {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: reactionEV
Types.Reaction {
event: eventView.event
sender: eventView.sender
isSelected: eventView.isSelected
}
}
Component {
id: ignoreEV
Item {
}
}
Loader {
sourceComponent: getSource(messageType)
id: loader
anchors.left: eventView.left
anchors.right: eventView.right
anchors.top: eventView.top
}
function getSource(t) {
switch (t) {
case 'text':
return textEV;
case 'emote':
return emoteEV;
case 'notice':
return noticeEV;
case 'state':
return stateEV;
case 'image':
return imageEV;
case 'file':
return fileEV;
case 'video':
return videoEV;
case 'audio':
return audioEV;
case 'redacted':
return redactedEV;
case 'reaction':
return reactionEV;
case 'ignore':
return ignoreEV;
default:
return unknownEV;
}
}
function getMessageType(e) {
if (e.redacted) {
return 'redacted';
}
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) {
case 'm.text':
return 'text';
case 'm.emote':
return 'emote';
case 'm.notice':
return 'notice';
case 'm.image':
return 'image';
case 'm.file':
return 'file';
case 'm.audio':
return 'audio';
case 'm.video':
return 'video';
case 'm.location':
return 'location';
default:
return 'unknown';
}
case 'm.sticker':
return 'image';
case 'm.room.redaction':
return 'ignore';
case 'm.reaction':
- return 'reaction';
+ // Only display a reaction if it is a failed local echo
+ // (otherwise it will be stuck forever and we have no way to delete it)
+ return e.isFailed ? 'reaction' : 'ignore';
default:
return 'unknown';
}
}
function paginateBack() {
paginateBackRequested(event.eventId);
}
function reactWith(reactionText) {
room.sendReaction(reactionText, event.eventId);
}
onIsGappedChanged: maybePaginateBack()
onEventChanged: maybePaginateBack()
Component.onCompleted: maybePaginateBack()
Connections {
target: eventView.event
function onEventIdChanged() {
eventView.maybePaginateBack();
}
}
function maybePaginateBack() {
if (isGapped) {
paginateBack();
}
}
}
diff --git a/src/contents/ui/RoomTimelineView.qml b/src/contents/ui/RoomTimelineView.qml
index a2af6d5..7805d87 100644
--- a/src/contents/ui/RoomTimelineView.qml
+++ b/src/contents/ui/RoomTimelineView.qml
@@ -1,178 +1,185 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
ListView {
id: roomTimelineView
objectName: 'roomTimelineView'
property var timeline
property var eventIds: timeline.eventIds
property string selectedEventId
signal pinEventRequested(string eventId)
signal unpinEventRequested(string eventId)
signal deleteEventRequested(eventIdOrTxnId: string, isLocalEcho: bool)
+ signal deleteEventRequestedNoConfirm(eventIdOrTxnId: string, isLocalEcho: bool)
property var pinEvent: Kazv.AsyncHandler {
property var eventIds
trigger: () => room.pinEvents(eventIds)
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('event-pin-success-prompt'));
} else {
showPassiveNotification(l10n.get('event-pin-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var unpinEvent: Kazv.AsyncHandler {
property var eventIds
trigger: () => room.unpinEvents(eventIds)
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('event-unpin-success-prompt'));
} else {
showPassiveNotification(l10n.get('event-unpin-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var deleteEvent: Kazv.AsyncHandler {
property var eventId
property var reason
trigger: () => room.redactEvent(eventId, reason)
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('event-delete-failed', { errorMsg: data.error, errorCode: data.errorCode }));
}
}
}
property var deleteLocalEcho: Kazv.AsyncHandler {
property var txnId
trigger: () => room.removeLocalEcho(txnId)
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('event-delete-failed', { errorMsg: data.error, errorCode: data.errorCode }));
}
}
}
onPinEventRequested: (eventId) => {
pinEventPopupComp.createObject(Overlay.overlay, { eventId }).open()
}
onUnpinEventRequested: (eventId) => {
unpinEventPopupComp.createObject(Overlay.overlay, { eventId }).open()
}
onDeleteEventRequested: (eventIdOrTxnId, isLocalEcho) => {
confirmDeletionPopupComp.createObject(Overlay.overlay, { eventIdOrTxnId, isLocalEcho }).open()
}
+ onDeleteEventRequestedNoConfirm: (eventIdOrTxnId, isLocalEcho) => {
+ doDeleteEvent(eventIdOrTxnId, isLocalEcho);
+ }
+
+ function doDeleteEvent(eventIdOrTxnId, isLocalEcho) {
+ if (isLocalEcho) {
+ deleteLocalEcho.txnId = eventIdOrTxnId;
+ deleteLocalEcho.call();
+ } else {
+ deleteEvent.eventId = eventIdOrTxnId;
+ deleteEvent.call();
+ }
+ }
+
property var pinEventPopupComp: Component {
Kazv.ConfirmationOverlay {
objectName: 'pinEventPopup'
property var eventId
shouldSelfDestroy: true
title: l10n.get('event-pin-confirmation-title')
message: l10n.get('event-pin-confirmation')
confirmActionText: l10n.get('event-pin-confirm-action')
cancelActionText: l10n.get('event-pin-cancel-action')
onAccepted: {
pinEvent.eventIds = [eventId];
pinEvent.call();
}
EventViewWrapper {
Layout.fillWidth: true
event: room.messageById(eventId)
compactMode: true
}
}
}
property var unpinEventPopupComp: Component {
Kazv.ConfirmationOverlay {
objectName: 'unpinEventPopup'
property var eventId
shouldSelfDestroy: true
title: l10n.get('event-unpin-confirmation-title')
message: l10n.get('event-unpin-confirmation')
confirmActionText: l10n.get('event-unpin-confirm-action')
cancelActionText: l10n.get('event-unpin-cancel-action')
onAccepted: {
unpinEvent.eventIds = [eventId];
unpinEvent.call();
}
EventViewWrapper {
Layout.fillWidth: true
event: room.messageById(eventId)
compactMode: true
}
}
}
property var confirmDeletionPopupComp: Component {
Kazv.ConfirmationOverlay {
objectName: 'confirmDeletionPopup'
property string eventIdOrTxnId
property bool isLocalEcho
shouldSelfDestroy: true
title: l10n.get('confirm-deletion-popup-title')
message: l10n.get('confirm-deletion-popup-message')
confirmActionText: l10n.get('confirm-deletion-popup-confirm-action')
cancelActionText: l10n.get('confirm-deletion-popup-cancel-action')
- onAccepted: {
- if (isLocalEcho) {
- deleteLocalEcho.txnId = eventIdOrTxnId;
- deleteLocalEcho.call();
- } else {
- deleteEvent.eventId = eventIdOrTxnId;
- deleteEvent.call();
- }
- }
+ onAccepted: doDeleteEvent(eventIdOrTxnId, isLocalEcho)
EventViewWrapper {
id: eventView
Layout.fillWidth: true
event: isLocalEcho ? room.localEchoById(eventIdOrTxnId) : room.messageById(eventIdOrTxnId)
compactMode: true
}
}
}
spacing: 0
model: timeline
currentIndex: -1
verticalLayoutDirection: ListView.BottomToTop
delegate: EventViewWrapper {
property var eventListView: ListView.view
prevEvent: index < timeline.count - 1 ? timeline.at(index + 1) : undefined
event: timeline.at(index)
width: ListView.view.width
isSelected: roomTimelineView.selectedEventId && event.eventId === roomTimelineView.selectedEventId
}
function goToEvent(eventId) {
selectedEventId = eventId;
const index = timeline.indexOfEvent(eventId);
positionViewAtIndex(index, ListView.Center);
}
}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
index 2db77e2..92ce2d1 100644
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -1,414 +1,415 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title = { -kt-app-name }
about-page-title = 关于 { -kt-app-name }
about-copyright = (c) 2020- the Kazv Project
about-display-name = { -kt-app-name } { $version }
about-short-description = 各平台同一的 Matrix 客户端和即时通讯软件
about-license = 以 AGPL 3 或以后版本授权
about-used-libraries = 使用了的库
about-authors = 作者
about-author-email-action = 写电邮
about-author-website-action = 访问网站
about-author-task-maintainer = 维护者
about-author-task-developer = 开发者
user-name-with-id = { $name } ({ $userId })
user-name-overrided = { $overridedName } ({ $globalName })
global-drawer-title = { -kt-app-name }
global-drawer-action-switch-account = 切换账号
global-drawer-action-hard-logout = 登出
global-drawer-action-save-session = 保存当前会话
global-drawer-action-configure-shortcuts = 配置快捷键
global-drawer-action-settings = 设置
global-drawer-action-create-room = 创建房间
global-drawer-action-join-room = 加入房间
global-drawer-action-about = 关于 { -kt-app-name }
action-settings-page-title = 配置快捷键
action-settings-shortcut-prompt = 快捷键:{ $shortcut }
action-settings-shortcut-none = (无)
action-settings-shortcut-edit-action = 编辑
action-settings-shortcut-remove-action = 清除
action-settings-shortcut-conflict-modal-title = 冲突的快捷键
action-settings-shortcut-conflict = 快捷键 { $shortcut } 跟别的指令有冲突。<br>若要继续,别的指令的快捷键会被清除。<br><br>冲突的指令有:<br>{ $conflictingAction }
action-settings-shortcut-conflict-continue = 继续
action-settings-shortcut-conflict-cancel = 取消
empty-room-page-title = 没有选中房间
empty-room-page-description = 当前没有选中的房间。
login-page-title = 登录
login-page-userid-prompt = 用户 id:
login-page-userid-input-placeholder = 例如: @foo:example.org
login-page-password-prompt = 密码:
login-page-login-button = 登录
login-page-close-button = 关闭
login-page-existing-sessions-prompt = 从已有会话中选一个:
login-page-alternative-password-login-prompt = 或者用用户 id 和密码启动新会话:
login-page-restore-session-button = 恢复会话
login-page-request-failed-prompt = 登录失败。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
login-page-discover-failed-prompt = 不能检测此用户所在的服务器,或者服务器不可用。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
session-load-failure-not-found = 找不到会话 { $sessionName }。
session-load-failure-format-unknown = 会话 { $sessionName } 包含不支持的格式。是由未来版本的 { -kt-app-name } 保存的吗?
session-load-failure-cannot-backup = 无法备份会话 { $sessionName }。
session-load-failure-lock-failed = 会话 { $sessionName } 正在被别的程序使用。
session-load-failure-cannot-open-file = 无法打开会话 { $sessionName } 的存档文件。
session-load-failure-deserialize-failed = 无法打开会话 { $sessionName }。是被损坏了或者是由未来版本的 { -kt-app-name } 保存的吗?
main-page-title = { -kt-app-name } - { $userId }
main-page-recent-tab-title = 最近
main-page-favourites-tab-title = 最爱
main-page-people-tab-title = 人们
main-page-rooms-tab-title = 房间
main-page-room-filter-prompt = 过滤房间...
room-list-view-room-item-title-name = { $name }
room-list-view-room-item-title-heroes = { $hero } { $otherNum ->
[0] { "" }
[1] 和 { $secondHero }
*[other] 和别的 { $otherNum } 个人
}
room-list-view-room-item-title-id = 未命名房间({ $roomId })
room-list-view-room-item-fav-action = 设为最爱
room-list-view-room-item-unread-indicator = 未读
room-list-view-room-item-unread-notification-count = { $count }
room-list-view-room-item-unread-notification-count-text = { $count } 条未读通知
room-tags-fav-action-notification = 把 { $name } 设为了最爱
room-tags-fav-action-notification-failed = 不能把 { $name } 设为最爱。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-unfav-action-notification = 把 { $name } 从最爱中移除了
room-tags-unfav-action-notification-failed = 不能把 { $name } 从最爱中移除。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-add-tag-action-notification = 把标签 { $tag } 添加到了 { $name }
room-tags-add-tag-action-notification-failed = 不能把标签 { $tag } 添加到 { $name }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-tags-remove-tag-action-notification = 把标签 { $tag } 从 { $name } 移除了
room-tags-remove-tag-action-notification-failed = 不能把标签 { $tag } 从 { $name } 移除。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-list-view-room-item-more-action = 更多...
room-list-view-room-item-invited = (邀请)
room-list-view-room-item-left = (已离开)
room-list-view-room-item-tombstone = (已废弃)
room-settings-action = 房间设置...
room-settings-page-title = { $room } 的房间设置
room-settings-tags = 房间标签
room-settings-favourited = 设为最爱
room-settings-remove-tag = 移除标签
room-settings-add-tag = 添加标签
room-settings-members-action = 房间成员...
room-settings-banned-members-action = 被封禁的成员...
room-settings-enable-encryption-prompt-dialog-title = 启用加密
room-settings-enable-encryption-prompt-dialog-prompt = 一旦在本房间中启用了加密,就不能再禁用了。确定吗?
room-settings-enable-encryption-action = 启用加密
room-settings-encrypted = 本房间中的消息是端对端加密了的。
room-settings-not-encrypted = 本房间中的消息没有端对端加密。
room-settings-encryption-enabled-notification = 本房间中的加密已经启用。
room-settings-encryption-failed-to-enable-notification = 不能在本房间中启用加密。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-settings-name-missing = 这个房间没有名字。
room-settings-topic-missing = 这个房间没有话题。
room-settings-edit-name-action = 编辑房间名字
room-settings-edit-topic-action = 编辑话题
room-settings-save-name-action = 保存房间名字
room-settings-save-topic-action = 保存话题
room-settings-discard-name-action = 放弃房间名字
room-settings-discard-topic-action = 放弃话题
room-settings-set-name-failed-prompt = 不能设置房间名字。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-settings-set-topic-failed-prompt = 不能设置话题。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-action = 贴纸包...
room-sticker-packs-page-title = { $room } 中的贴纸包
room-sticker-packs-use-action = 使用贴纸包
room-sticker-packs-use-failed = 设置使用的贴纸包失败。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-page-add-action = 添加贴纸包
room-sticker-packs-page-add-popup-title = 添加贴纸包
room-sticker-packs-page-add-success = 已添加贴纸包。
room-sticker-packs-page-add-failed = 无法添加贴纸包。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-sticker-packs-page-add-popup-name-prompt = 贴纸包名称:
room-sticker-packs-page-add-popup-state-key-prompt = 状态键:
room-member-list-page-title = { $room } 的成员
room-leave-action = 离开房间
room-leave-confirm-popup-title = 离开房间
room-leave-confirm-popup-message = 你确定要离开这个房间吗?
room-leave-confirm-popup-confirm-action = 离开
room-leave-confirm-popup-cancel-action = 留下
room-forget-action = 忘记房间
room-forget-confirm-popup-title = 忘记房间
room-forget-confirm-popup-message = 你确定要忘记这个房间吗?
room-forget-confirm-popup-confirm-action = 忘记
room-forget-confirm-popup-cancel-action = 取消
send-message-box-input-placeholder = 在此输入您的讯息...
send-message-box-send = 发送
send-message-box-send-file = 发送文件
send-message-box-reply-to = 回复给
send-message-box-edit = 编辑
send-message-box-remove-reply-to-action = 移除回复关系
send-message-box-remove-replace-action = 取消编辑
send-message-box-stickers = 发送贴纸...
send-message-box-stickers-popup-title = 发送贴纸
sticker-picker-user-stickers = 我的贴纸
sticker-picker-room-sticker-pack-name = {$room} 中的 {$stateKey}
sticker-picker-room-default-sticker-pack-name = {$room} 中的默认包
room-timeline-load-more-action = 加载更多
room-invite-accept-action = 接受邀请
room-invite-reject-action = 拒绝邀请
room-invite-popup-title = 被邀请了
room-invite-popup-text = 你被邀请到这个房间了。
room-invite-popup-text-with-inviter = 你被 { $inviterName } 邀请到这个房间了。
room-pinned-events-action = { $count } 条置顶消息...
room-pinned-events-page-title = { $room } 的置顶消息
## 状态事件
## 通用参数:
## gender = 发送者的性别(male/female/neutral)
## stateKeyUser = state key 用户的名字
## stateKeyUserGender = state key 用户的性别
member-state-joined-room = 加入了房间。
member-state-changed-name-and-avatar = 修改了名字和头像。
member-state-changed-name = 修改了名字。
member-state-changed-avatar = 修改了头像。
member-state-invited = 把 { $stateKeyUser } 邀请到了本房间。
member-state-left = 离开了房间。
member-state-kicked = 踢出了 { $stateKeyUser }。
member-state-banned = 封禁了 { $stateKeyUser }。
member-state-unbanned = 解封了 { $stateKeyUser }。
state-room-created = 创建了房间。
state-room-name-changed = 把房间名字改成了 { $newName }。
state-room-topic-changed = 把房间话题改成了 { $newTopic }。
state-room-avatar-changed = 修改了房间头像。
state-room-pinned-events-changed = 修改了房间的置顶讯息。
state-room-alias-changed = 修改了房间的别名。
state-room-join-rules-changed = 修改了房间的加入规则。
state-room-power-levels-changed = 修改了房间的权限。
state-room-encryption-activated = 对本房间启用了加密。
event-message-image-sent = 发送了图片「{ $body }」。
event-message-sticker-sent = 发送了贴纸「{ $body }」。
event-summary-image-sent = 发送了图片「{ $body }」。
event-summary-sticker-sent = 发送了贴纸「{ $body }」。
event-message-file-sent = 发送了文件「{ $body }」。
event-message-video-sent = 发送了视频「{ $body }」。
event-summary-video-sent = 发送了视频「{ $body }」。
event-message-audio-sent = 发送了音频「{ $body }」。
event-message-audio-play-audio = 播放音频
event-sending = 发送中...
event-send-failed = 发送失败
event-resend = 重试发送这个事件
event-deleted = (已删除)
event-delete = 删除
event-delete-failed = 删除事件出错。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
event-view-source = 查看源码...
event-view-history = 查看编辑历史...
event-source-popup-title = 事件源码
event-source-decrypted = 解密的事件源码
event-source-original = 原始的事件源码
event-history-popup-title = 事件编辑历史
event-reply-action = 回复
event-popup-action = 更多关于这个事件...
event-reacted-with = 回应了「{ $key }」
event-react-action = 回应...
event-react-popup-title = 回应一条消息
event-react-accept-action = 回应
event-react-cancel-action = 取消
event-react-with-prompt = 回应以:
+event-react-button-description = 回应以 { $key }
event-edit-action = 编辑
event-encrypted = 这条消息已加密
event-msgtype-fallback-text = 未知的消息类型:{ $msgtype }
event-fallback-text = 未知事件:{ $type }
event-cannot-decrypt-text = (加密内容)
event-read-indicator-more = +{ $rest }
event-read-indicator-list-title = { $numUsers } 个用户已读
event-edited-indicator = (编辑过了)
event-pin-action = 在房间置顶
event-unpin-action = 从房间取消置顶
event-pin-confirmation-title = 在房间置顶
event-pin-confirmation = 确定要置顶这条消息吗?
event-pin-confirm-action = 置顶
event-pin-cancel-action = 不置顶
event-pin-success-prompt = 消息在房间置顶了。
event-pin-failed-prompt = 无法置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
event-unpin-confirmation-title = 从房间取消置顶
event-unpin-confirmation = 确定要取消置顶这条消息吗?
event-unpin-confirm-action = 取消置顶
event-unpin-cancel-action = 不取消置顶
event-unpin-success-prompt = 消息从房间取消置顶了。
event-unpin-failed-prompt = 无法取消置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
media-file-menu-option-view = 查看
media-file-menu-option-save-as = 保存为
media-file-menu-add-sticker-action = 添加到贴纸...
add-sticker-popup-title = 添加到贴纸
add-sticker-popup-pack-prompt = 添加到:
add-sticker-popup-short-code-prompt = 短代码:
add-sticker-popup-short-code-exists-warning = 贴纸包里已经有这个短代码了。上面的贴纸会覆盖已有的。
add-sticker-popup-add-sticker-button = 添加
add-sticker-popup-cancel-button = 取消
add-sticker-popup-failed-prompt = 无法添加贴纸。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
kazv-io-download-success-prompt = 下载成功
kazv-io-download-failure-prompt = 下载失败:{ $detail }
kazv-io-failure-detail-user-cancel = 用户已取消
kazv-io-failure-detail-network-error = 网络错误
kazv-io-failure-detail-open-file-error = 打开文件错误
kazv-io-failure-detail-write-file-error = 写入文件错误
kazv-io-failure-detail-hash-error = 哈息值校验失败
kazv-io-failure-detail-response-error = 接收到无效的响应
kazv-io-failure-detail-kazv-error = 未知错误,请将此报告为漏洞
kazv-io-upload-failure-prompt = 上传失败:{ $detail }
kazv-io-downloading-prompt = 正在下载:{ $fileName }
kazv-io-uploading-prompt = 正在上传:{ $fileName }
kazv-io-prompt-close = 好的
kazv-io-pause = 暂停
kazv-io-resume = 继续
kazv-io-cancel = 取消
kazv-io-progress = { $progress }/100
create-room-page-title = 创建房间
create-room-page-type-prompt = 房间类型:
create-room-page-type-public = 公开(每个人都可加入)
create-room-page-type-private = 私有(仅受邀请用户可加入)
create-room-page-type-direct = 私聊(与私有一样,但初始受邀请用户具有管理权限)
create-room-page-name-prompt = 房间名称(可选):
create-room-page-name-placeholder = 无名
create-room-page-alias-prompt = 房间别名(可选):
create-room-page-alias-placeholder = #foo:example.org
create-room-page-topic-prompt = 房间主题(可选):
create-room-page-topic-placeholder = 无题
create-room-page-allow-federate-prompt = 允许别的服务器上的用户加入
create-room-page-action-create-room = 创建房间
create-room-page-success-prompt = 房间已创建。
create-room-page-failed-prompt = 无法创建房间。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
create-room-invite-userids-prompt = 受邀请的用户的 Matrix id:
create-room-page-add-invite-prompt = 添加一个新的受邀请用户:
create-room-page-add-invite-placeholder = Matrix id,例如 @foo:example.org
create-room-page-action-add-invite = 添加
create-room-page-action-remove-invite = 移除
create-room-page-encrypted-prompt = 启用端对端加密
join-room-page-title = 加入房间
join-room-page-id-or-alias-prompt = 房间 id 或别名:
join-room-page-id-or-alias-placeholder = #foo:example.org 或 !abcdef:example.org
join-room-page-servers-prompt = 经由服务器(可选,用换行分割):
join-room-page-servers-placeholder =
example.org
example.com
join-room-page-action-join-room = 加入房间
join-room-page-success-prompt = 成功加入房间 { $room }。
join-room-page-failed-prompt = 无法加入房间 { $room }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
leave-room-success-prompt = 成功离开房间 { $room }。
leave-room-failed-prompt = 无法离开房间 { $room }。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
forget-room-success-prompt = 成功忘记房间 { $room }。
forget-room-failed-prompt = 无法忘记房间 { $room }。错误代码: { $errorCode }。错误讯息: { $errorMsg }。
device-trust-level-unseen = 未曾见过
device-trust-level-seen = 见过
device-trust-level-verified = 已验证
device-trust-level-blocked = 已屏蔽
device-set-trust-level = 设置信任等级...
device-set-trust-level-dialog-title = 设置信任等级
device-set-trust-level-dialog-name-label = 设备名:{ $name }
device-set-trust-level-dialog-id-label = 设备id:{ $id }
device-set-trust-level-dialog-ed25519-key-label = Ed25519公钥:{ $key }
device-set-trust-level-dialog-curve25519-key-label = Curve25519公钥:{ $key }
device-set-trust-level-dialog-save = 保存
device-set-trust-level-dialog-cancel = 取消
settings-page-title = 设置
settings-save = 保存设置
settings-profile-load-failed-prompt = 无法加载用户资料。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
settings-profile-change-avatar = 改变头像...
settings-profile-display-name = 显示名:
settings-profile-save-failed-prompt = 无法保存用户资料。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
settings-cache-directory = 缓存目录:
typing-indicator = { $typingUser } { $otherNum ->
[0] 正在输入...
[1] 和 { $secondTypingUser } 正在输入...
*[other] 和另外 { $otherNum } 人正在输入...
}
notification-message = <b>{ $user }:</b> { $message }
notification-message-no-content = <b>{ $user }</b> 给你发了一条讯息。
notification-open = 打开
user-page-power-level = 权限等级:{ $powerLevel }
user-page-edit-power-level-action = 编辑
user-page-save-power-level-action = 保存
user-page-discard-power-level-action = 丢弃
user-page-set-power-level-failed-prompt = 无法设置权限等级。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
user-page-kick-user-action = 踢出
user-page-kick-user-confirm-dialog-title = 踢出用户
user-page-kick-user-confirm-dialog-content = 确定要把 { $name }({ $userId })踢出 { $roomName } 吗?
user-page-kick-user-reason-prompt = 原因(可选):
user-page-kick-user-failed-prompt = 无法踢出用户。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
user-page-ban-user-action = 封禁
user-page-ban-user-confirm-dialog-titile = 封禁用户
user-page-ban-user-confirm-dialog-content = 确定要在 { $roomName } 中封禁 { $name }({ $userId })吗?
user-page-ban-user-reason-prompt = 原因(可选):
user-page-ban-user-failed-prompt = 无法封禁用户。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-unban-user-action = 解禁
user-page-unban-user-failed-prompt = 无法解禁用户。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-overrided-name-placeholder = 自定义显示名...
user-page-save-name-override-action = 保存
user-page-update-name-override-failed-prompt = 无法设置自定义显示名。错误码:{ $error }。错误讯息:{ $errorMsg }。
user-page-self-name-prompt = 自己在房间里的显示名:
user-page-self-name-placeholder = 自己在房间里的显示名...
user-page-save-self-name-action = 保存
user-page-update-self-name-failed-prompt = 无法设置自己在房间里的显示名。错误码:{ $error }。错误讯息:{ $errorMsg }。
room-invite-page-title = 邀请用户到 { $room }
room-invite-page-invite-failed-prompt = 无法邀请用户。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
room-invite-invitee-matrix-id-placeholder = 用户的 Matrix id,比如 @foo:example.org
room-invite-invitee-matrix-id-prompt = 要邀请的用户:
room-invite-page-invite-button = 邀请
room-invite-action = 邀请到房间...
confirm-upload-popup-title = 确认上传
confirm-upload-popup-prompt = 即将上传「{ $file }」({ $current } / { $total })。
confirm-upload-popup-prompt-single-file = 即将上传「{ $file }」。
confirm-upload-popup-accept-button = 上传
confirm-upload-popup-cancel-button = 取消
action-cut = 剪切
action-copy = 复制
action-paste = 粘贴
action-undo = 撤销
action-redo = 重做
action-delete = 删除
action-select-all = 全选
logout-failed-prompt = 登出失败,请检查您的网络后重试。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
confirm-logout-popup-title = 确认登出
confirm-logout-popup-prompt = 你确定要登出吗?
confirm-logout-popup-accept-button = 是
confirm-logout-popup-cancel-button = 取消
emoji-smileys-and-emotion = 笑容与情绪
emoji-people-and-body = 人与身体
emoji-animals-and-nature = 动物与自然
emoji-food-and-drink = 食物与饮料
emoji-travel-and-places = 旅行与地点
emoji-activities = 活动
emoji-objects = 物体
emoji-symbols = 符号
emoji-flags = 旗帜
confirm-deletion-popup-title = 删除事件
confirm-deletion-popup-message = 你确定要删除这个事件吗?
confirm-deletion-popup-confirm-action = 删除
confirm-deletion-popup-cancel-action = 取消
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
index 49d5b08..14b4fa6 100644
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -1,436 +1,437 @@
### This file is part of kazv.
### SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
### SPDX-License-Identifier: AGPL-3.0-or-later
app-title = { -kt-app-name }
about-page-title = About { -kt-app-name }
about-copyright = (c) 2020- the Kazv Project
about-display-name = { -kt-app-name } { $version }
about-short-description = Convergent Matrix client and instant messaging app
about-license = licensed under AGPL 3 or later
about-used-libraries = Used libraries
about-authors = Authors
about-author-email-action = Write an email
about-author-website-action = Access website
about-author-task-maintainer = Maintainer
about-author-task-developer = Developer
user-name-with-id = { $name } ({ $userId })
user-name-overrided = { $overridedName } ({ $globalName })
global-drawer-title = { -kt-app-name }
global-drawer-action-switch-account = Switch account
global-drawer-action-hard-logout = Logout
global-drawer-action-save-session = Save current session
global-drawer-action-configure-shortcuts = Configure shortcuts
global-drawer-action-settings = Settings
global-drawer-action-create-room = Create room
global-drawer-action-join-room = Join room
global-drawer-action-about = About { -kt-app-name }
action-settings-page-title = Configure shortcuts
action-settings-shortcut-prompt = Shortcut: { $shortcut }
action-settings-shortcut-none = (none)
action-settings-shortcut-edit-action = Edit
action-settings-shortcut-remove-action = Clear
action-settings-shortcut-conflict-modal-title = Conflicting shortcuts
action-settings-shortcut-conflict = The shortcut { $shortcut } has conflicts with other actions. <br>If you continue, the shortcuts for other actions will be cleared. <br><br>Conflicting actions: <br>{ $conflictingAction }
action-settings-shortcut-conflict-continue = Continue
action-settings-shortcut-conflict-cancel = Cancel
empty-room-page-title = No rooms selected
empty-room-page-description = There is no room selected now.
login-page-title = Log in
login-page-userid-prompt = User id:
login-page-userid-input-placeholder = E.g.: @foo:example.org
login-page-password-prompt = Password:
login-page-login-button = Log in
login-page-close-button = Close
login-page-existing-sessions-prompt = Choose from one of the existing sessions:
login-page-alternative-password-login-prompt = Or start a new session with user id and password:
login-page-restore-session-button = Restore session
login-page-request-failed-prompt = Login failed. Error code: { $errorCode }. Error message: { $errorMsg }.
login-page-discover-failed-prompt = Unable to detect the server this user is on, or the server is unavailable. Error code: { $errorCode }. Error message: { $errorMsg }.
session-load-failure-not-found = The session { $sessionName } is not found.
session-load-failure-format-unknown = The session { $sessionName } contains an unsupported format. Is it saved using a future version of { -kt-app-name }?
session-load-failure-cannot-backup = Cannot make a backup of the session { $sessionName }.
session-load-failure-lock-failed = The session { $sessionName } is being used by another program.
session-load-failure-cannot-open-file = Unable to open the store file for the session { $sessionName }.
session-load-failure-deserialize-failed = The session { $sessionName } cannot be opened. Is it corrupted or saved using a future version of { -kt-app-name }?
main-page-title = { -kt-app-name } - { $userId }
main-page-recent-tab-title = Recent
main-page-favourites-tab-title = Favourites
main-page-people-tab-title = People
main-page-rooms-tab-title = Rooms
main-page-room-filter-prompt = Filter rooms by...
room-list-view-room-item-title-name = { $name }
room-list-view-room-item-title-heroes = { $hero } { $otherNum ->
[0] { "" }
[1] and { $secondHero }
*[other] and { $otherNum } others
}
room-list-view-room-item-title-id = Unnamed room ({ $roomId })
room-list-view-room-item-fav-action = Set as favourite
room-list-view-room-item-unread-indicator = Unread
room-list-view-room-item-unread-notification-count = { $count }
room-list-view-room-item-unread-notification-count-text = { $count } unread {
$count ->
[1] message
*[other] messages
}
room-tags-fav-action-notification = Set { $name } as favourite
room-tags-fav-action-notification-failed = Cannot set { $name } as favourite. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-unfav-action-notification = Removed { $name } from favourites
room-tags-unfav-action-notification-failed = Cannot remove { $name } from favourites. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-add-tag-action-notification = Added tag { $tag } to { $name }
room-tags-add-tag-action-notification-failed = Cannot add tag { $tag } to { $name }. Error code: { $errorCode }. Error message: { $errorMsg }.
room-tags-remove-tag-action-notification = Removed tag { $tag } from { $name }
room-tags-remove-tag-action-notification-failed = Cannot remove tag { $tag } from { $name }. Error code: { $errorCode }. Error message: { $errorMsg }.
room-list-view-room-item-more-action = More...
room-list-view-room-item-invited = (Invited)
room-list-view-room-item-left = (Left)
room-list-view-room-item-tombstone = (Deprecated)
room-settings-action = Room settings...
room-settings-page-title = Room settings for { $room }
room-settings-tags = Room tags
room-settings-favourited = Set as favourite
room-settings-remove-tag = Remove tag
room-settings-add-tag = Add tag
room-settings-members-action = Room members...
room-settings-banned-members-action = Banned members...
room-settings-enable-encryption-prompt-dialog-title = Enabling encryption
room-settings-enable-encryption-prompt-dialog-prompt = Once you enable encryption in this room, you cannot disable it again. Are you sure?
room-settings-enable-encryption-action = Enable encryption
room-settings-encrypted = Messages in this room are end-to-end encrypted.
room-settings-not-encrypted = Messages in this room are not end-to-end-encrypted.
room-settings-encryption-enabled-notification = Encryption is now enabled in this room.
room-settings-encryption-failed-to-enable-notification = Cannot enable encryption in this room. Error code: { $errorCode }. Error message: { $errorMsg }.
room-settings-name-missing = This room dose not have a name.
room-settings-topic-missing = This room does not have a topic.
room-settings-edit-name-action = Edit name
room-settings-edit-topic-action = Edit topic
room-settings-save-name-action = Save name
room-settings-save-topic-action = Save topic
room-settings-discard-name-action = Discard name
room-settings-discard-topic-action = Discard topic
room-settings-set-name-failed-prompt = Cannot set name. Error code: { $errorCode }. Error message: { $errorMsg }.
room-settings-set-topic-failed-prompt = Cannot set topic. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-action = Sticker packs...
room-sticker-packs-page-title = Sticker packs in { $room }
room-sticker-packs-use-action = Use sticker pack
room-sticker-packs-use-failed = Failed to set sticker packs in use. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-page-add-action = Add sticker pack
room-sticker-packs-page-add-popup-title = Add sticker pack
room-sticker-packs-page-add-success = Sticker pack added.
room-sticker-packs-page-add-failed = Cannot add sticker pack. Error code: { $errorCode }. Error message: { $errorMsg }.
room-sticker-packs-page-add-popup-name-prompt = Sticker pack name:
room-sticker-packs-page-add-popup-state-key-prompt = State key:
room-member-list-page-title = Members of { $room }
room-leave-action = Leave room
room-leave-confirm-popup-title = Leaving room
room-leave-confirm-popup-message = Are you sure you want to leave this room?
room-leave-confirm-popup-confirm-action = Leave
room-leave-confirm-popup-cancel-action = Stay
room-forget-action = Forget room
room-forget-confirm-popup-title = Forgetting room
room-forget-confirm-popup-message = Are you sure you want to forget this room?
room-forget-confirm-popup-confirm-action = Forget
room-forget-confirm-popup-cancel-action = Cancel
send-message-box-input-placeholder = Type your message here...
send-message-box-send = Send
send-message-box-send-file = Send file
send-message-box-reply-to = Replying to
send-message-box-edit = Editing
send-message-box-remove-reply-to-action = Remove reply-to relationship
send-message-box-remove-replace-action = Cancel editing
send-message-box-stickers = Send a sticker...
send-message-box-stickers-popup-title = Send a sticker
sticker-picker-user-stickers = My stickers
sticker-picker-room-sticker-pack-name = {$stateKey} in {$room}
sticker-picker-room-default-sticker-pack-name = Default pack in {$room}
room-timeline-load-more-action = Load more
room-invite-accept-action = Accept invite
room-invite-reject-action = Reject invite
room-invite-popup-title = Invited
room-invite-popup-text = You are invited to join this room.
room-invite-popup-text-with-inviter = You are invited to join this room by { $inviterName }.
room-pinned-events-action = { $count } pinned { $count ->
[1] message
*[other] messages
}...
room-pinned-events-page-title = Pinned messages of { $room }
## State events
## Common parameters:
## gender = gender of the sender (male/female/neutral)
## stateKeyUser = name of the state key user
## stateKeyUserGender = gender of the state key user
member-state-joined-room = joined the room.
member-state-changed-name-and-avatar = changed { $gender ->
[male] his
[female] her
*[neutral] their
} name and avatar.
member-state-changed-name = changed { $gender ->
[male] his
[female] her
*[neutral] their
} name.
member-state-changed-avatar = changed { $gender ->
[male] his
[female] her
*[neutral] their
} avatar.
member-state-invited = invited { $stateKeyUser } to the room.
member-state-left = left the room.
member-state-kicked = kicked { $stateKeyUser }.
member-state-banned = banned { $stateKeyUser }.
member-state-unbanned = unbanned { $stateKeyUser }.
state-room-created = created the room.
state-room-name-changed = changed the name of the room to { $newName }.
state-room-topic-changed = changed the topic of the room to { $newTopic }.
state-room-avatar-changed = changed the avatar of the room.
state-room-pinned-events-changed = changed the pinned events of the room.
state-room-alias-changed = changed the aliases of the room.
state-room-join-rules-changed = changed join rules of the room.
state-room-power-levels-changed = changed power levels of the room.
state-room-encryption-activated = enabled encryption for this room.
event-message-image-sent = sent an image "{ $body }".
event-message-sticker-sent = sent a sticker "{ $body }".
event-summary-image-sent = sent an image "{ $body }".
event-summary-sticker-sent = sent a sticker "{ $body }".
event-message-file-sent = sent a file "{ $body }".
event-message-video-sent = sent a video "{ $body }".
event-summary-video-sent = sent a video "{ $body }".
event-message-audio-sent = sent an audio "{ $body }".
event-message-audio-play-audio = Play audio
event-sending = Sending...
event-send-failed = Failed to send
event-resend = Retry sending this event
event-deleted = (Deleted)
event-delete = Delete
event-delete-failed = Error deleting event. Error code: { $errorCode }. Error message: { $errorMsg }.
event-view-source = View source...
event-view-history = View edit history...
event-source-popup-title = Event source
event-source-decrypted = Decrypted event source
event-source-original = Original event source
event-history-popup-title = Event edit history
event-reply-action = Reply
event-popup-action = More about this event...
event-reacted-with = Reacted with "{ $key }"
event-react-action = React...
event-react-popup-title = React to a message
event-react-accept-action = React
event-react-cancel-action = Cancel
event-react-with-prompt = React with:
+event-react-button-description = React with { $key }
event-edit-action = Edit
event-encrypted = This message is encrypteed
event-msgtype-fallback-text = Unknown message type: { $msgtype }
event-fallback-text = Unknown event: { $type }
event-cannot-decrypt-text = (Encrypted content)
event-read-indicator-more = +{ $rest }
event-read-indicator-list-title = Read by { $numUsers ->
[1] 1 user
*[other] { $numUsers } users
}
event-edited-indicator = (edited)
event-pin-action = Pin to room
event-unpin-action = Unpin from room
event-pin-confirmation-title = Pin to room
event-pin-confirmation = Are you sure you want to pin this message?
event-pin-confirm-action = Pin
event-pin-cancel-action = Do not pin
event-pin-success-prompt = Message pinned to room.
event-pin-failed-prompt = Unable to pin message. Error code: { $errorCode }. Error message: { $errorMsg }.
event-unpin-confirmation-title = Unpin from room
event-unpin-confirmation = Are you sure you want to unpin this message?
event-unpin-confirm-action = Unpin
event-unpin-cancel-action = Do not unpin
event-unpin-success-prompt = Message unpinned from room.
event-unpin-failed-prompt = Unable to unpin message. Error code: { $errorCode }. Error message: { $errorMsg }.
media-file-menu-option-view = View
media-file-menu-option-save-as = Save as
media-file-menu-add-sticker-action = Add to sticker...
add-sticker-popup-title = Add to sticker
add-sticker-popup-pack-prompt = Add to pack:
add-sticker-popup-short-code-prompt = Short code:
add-sticker-popup-short-code-exists-warning = The short code already exists in this pack. The sticker above will override the existing one.
add-sticker-popup-add-sticker-button = Add
add-sticker-popup-cancel-button = Cancel
add-sticker-popup-failed-prompt = Unable to add sticker. Error code: { $errorCode }. Error message: { $errorMsg }.
kazv-io-download-success-prompt = Download successful
kazv-io-download-failure-prompt = Download failure: { $detail }
kazv-io-failure-detail-user-cancel = User canceled
kazv-io-failure-detail-network-error = Network error
kazv-io-failure-detail-open-file-error = Open file error
kazv-io-failure-detail-write-file-error = Write file error
kazv-io-failure-detail-hash-error = Hash check error
kazv-io-failure-detail-response-error = Get an invalid response
kazv-io-failure-detail-kazv-error = Unknow Error, please report this as bug.
kazv-io-upload-failure-prompt = Upload failure: { $detail }
kazv-io-downloading-prompt = Downloading: { $fileName }
kazv-io-uploading-prompt = Uploading: { $fileName }
kazv-io-prompt-close = Got it
kazv-io-pause = Pause
kazv-io-resume = Resume
kazv-io-cancel = Cancel
kazv-io-progress = { $progress }/100
create-room-page-title = Create room
create-room-page-type-prompt = Room type:
create-room-page-type-public = Public (everyone can join)
create-room-page-type-private = Private (only invited users can join)
create-room-page-type-direct = Direct message (same as private, but those initially invited get admin permissions)
create-room-page-name-prompt = Room name (optional):
create-room-page-name-placeholder = No name
create-room-page-alias-prompt = Room alias (optional):
create-room-page-alias-placeholder = #foo:example.org
create-room-page-topic-prompt = Room topic (optional):
create-room-page-topic-placeholder = No topic
create-room-page-allow-federate-prompt = Allow users from other servers to join
create-room-page-action-create-room = Create room
create-room-page-success-prompt = Room created.
create-room-page-failed-prompt = Unable to create room. Error code: { $errorCode }. Error message: { $errorMsg }.
create-room-invite-userids-prompt = Matrix ids of users to invite:
create-room-page-add-invite-prompt = Add a new user to invite:
create-room-page-add-invite-placeholder = Matrix id, e.g. @foo:example.org
create-room-page-action-add-invite = Add
create-room-page-action-remove-invite = Remove
create-room-page-encrypted-prompt = Enable end-to-end encryption
join-room-page-title = Join room
join-room-page-id-or-alias-prompt = Room id or alias:
join-room-page-id-or-alias-placeholder = #foo:example.org or !abcdef:example.org
join-room-page-servers-prompt = Via servers (optional, separated by newlines):
join-room-page-servers-placeholder =
example.org
example.com
join-room-page-action-join-room = Join room
join-room-page-success-prompt = Successfully joined room { $room }.
join-room-page-failed-prompt = Unable to join room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
leave-room-success-prompt = Successfully left room { $room }.
leave-room-failed-prompt = Unable to leave room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
forget-room-success-prompt = Successfully forgot room { $room }.
forget-room-failed-prompt = Unable to forget room { $room }. Error code: { $errorCode }. Error message: { $errorMsg }.
device-trust-level-unseen = Unseen
device-trust-level-seen = Seen
device-trust-level-verified = Verified
device-trust-level-blocked = Blocked
device-set-trust-level = Set trust level...
device-set-trust-level-dialog-title = Set trust level
device-set-trust-level-dialog-name-label = Device name: { $name }
device-set-trust-level-dialog-id-label = Device id: { $id }
device-set-trust-level-dialog-ed25519-key-label = Ed25519 public key: { $key }
device-set-trust-level-dialog-curve25519-key-label = Curve25519 public key: { $key }
device-set-trust-level-dialog-save = Save
device-set-trust-level-dialog-cancel = Cancel
settings-page-title = Settings
settings-save = Save settings
settings-profile-load-failed-prompt = Unable to load profile. Error code: { $errorCode }. Error message: { $errorMsg }.
settings-profile-change-avatar = Change avatar...
settings-profile-display-name = Display name:
settings-profile-save-failed-prompt = Unable to save profile. Error code: { $errorCode }. Error message: { $errorMsg }.
settings-cache-directory = Cache directory:
typing-indicator = { $typingUser } { $otherNum ->
[0] is typing...
[1] and { $secondTypingUser } are typing...
*[other] and { $otherNum } others are typing...
}
notification-message = <b>{ $user }:</b> { $message }
notification-message-no-content = <b>{ $user }</b> sent you a message.
notification-open = Open
user-page-power-level = Power level: { $powerLevel }
user-page-edit-power-level-action = Edit
user-page-save-power-level-action = Save
user-page-discard-power-level-action = Discard
user-page-set-power-level-failed-prompt = Unable to set power level. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-kick-user-action = Kick
user-page-kick-user-confirm-dialog-title = Kicking user
user-page-kick-user-confirm-dialog-content = Are you sure you want to kick { $name } ({ $userId }) out of { $roomName }?
user-page-kick-user-reason-prompt = Reason (optional):
user-page-kick-user-failed-prompt = Unable to kick user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-ban-user-action = Ban
user-page-ban-user-confirm-dialog-title = Banning user
user-page-ban-user-confirm-dialog-content = Are you sure you want to ban { $name } ({ $userId }) in { $roomName }?
user-page-ban-user-reason-prompt = Reason (optional):
user-page-ban-user-failed-prompt = Unable to ban user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-unban-user-action = Unban
user-page-unban-user-failed-prompt = Unable to unban user. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-overrided-name-placeholder = Custom display name...
user-page-save-name-override-action = Save
user-page-update-name-override-failed-prompt = Unable to set custom display name. Error code: { $errorCode }. Error message: { $errorMsg }.
user-page-self-name-prompt = Own display name in room:
user-page-self-name-placeholder = Own display name in room...
user-page-save-self-name-action = Save
user-page-update-self-name-failed-prompt = Unable to set own display name in room. Error code: { $errorCode }. Error message: { $errorMsg }.
room-invite-page-title = Inviting user to { $room }
room-invite-page-invite-failed-prompt = Unable to invite user. Error code: { $errorCode }. Error message: { $errorMsg }.
room-invite-invitee-matrix-id-placeholder = Matrix id of the user, e.g. @foo:example.org
room-invite-invitee-matrix-id-prompt = User to invite:
room-invite-page-invite-button = Invite
room-invite-action = Invite to room...
confirm-upload-popup-title = Confirm upload
confirm-upload-popup-prompt = You are about to upload "{ $file }" ({ $current } / { $total }).
confirm-upload-popup-prompt-single-file = You are about to upload "{ $file }".
confirm-upload-popup-accept-button = Upload
confirm-upload-popup-cancel-button = Cancel
action-cut = Cut
action-copy = Copy
action-paste = Paste
action-undo = Undo
action-redo = Redo
action-delete = Delete
action-select-all = Select All
logout-failed-prompt = Logout failed. Please check your network and try again. Error code: { $errorCode }, error message: { $errorMsg }.
confirm-logout-popup-title = Confirm logout
confirm-logout-popup-prompt = Are you sure to logout?
confirm-logout-popup-accept-button = Yes
confirm-logout-popup-cancel-button = Cancel
emoji-smileys-and-emotion = Smiley && Emotion
emoji-people-and-body = People && Body
emoji-animals-and-nature = Animals && Nature
emoji-food-and-drink = Food && Drink
emoji-travel-and-places = Travel && Places
emoji-activities = Activities
emoji-objects = Objects
emoji-symbols = Symbols
emoji-flags = Flags
confirm-deletion-popup-title = Delete event
confirm-deletion-popup-message = Are you sure you want to delete this event?
confirm-deletion-popup-confirm-action = Delete
confirm-deletion-popup-cancel-action = Cancel
diff --git a/src/matrix-event-reaction-list-model.cpp b/src/matrix-event-reaction-list-model.cpp
new file mode 100644
index 0000000..e992643
--- /dev/null
+++ b/src/matrix-event-reaction-list-model.cpp
@@ -0,0 +1,59 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "matrix-event-reaction-list-model.hpp"
+#include <kazv-defs.hpp>
+#include <json-utils.hpp>
+#include "matrix-event-reaction.hpp"
+
+using namespace Kazv;
+
+MatrixEventReactionListModel::MatrixEventReactionListModel(
+ Room room,
+ lager::reader<std::string> parentEventId,
+ QObject *parent)
+ : KazvAbstractListModel(parent)
+ , m_reactions(room.relatedEvents(parentEventId, "m.annotation")
+ .map([](const auto &events) {
+ QMap<QString, Kazv::EventList> reactions;
+ for (const auto &e : events) {
+ auto rel = e.mRelatesTo().get();
+ // This automatically excludes redacted events,
+ // because redacted events' content is empty.
+ if (hasAtThat(rel, "key", &json::is_string)) {
+ auto key = QString::fromStdString(rel["key"].template get<std::string>());
+ reactions[key] = reactions[key].push_back(e);
+ }
+ }
+ return reactions;
+ }))
+ , m_reactionKeys(m_reactions.map([](const auto &reactions) {
+ return reactions.keys();
+ }))
+{
+ initCountCursor(m_reactions.map([](const auto &reactions) -> int {
+ return reactions.size();
+ }));
+}
+
+MatrixEventReactionListModel::~MatrixEventReactionListModel() = default;
+
+MatrixEventReaction *MatrixEventReactionListModel::at(int index) const
+{
+ return new MatrixEventReaction(lager::with(m_reactionKeys, m_reactions)
+ .map([index](const auto &keys, const auto &reactions) {
+ if (index >= 0 && index < keys.size()) {
+ return reactions.value(keys[index]);
+ }
+ return EventList{};
+ }),
+ m_reactionKeys.map([index](const auto &keys) {
+ if (index >= 0 && index < keys.size()) {
+ return keys[index];
+ }
+ return QString();
+ }));
+}
diff --git a/src/matrix-event-reaction-list-model.hpp b/src/matrix-event-reaction-list-model.hpp
new file mode 100644
index 0000000..a36e60e
--- /dev/null
+++ b/src/matrix-event-reaction-list-model.hpp
@@ -0,0 +1,33 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <lager/extra/qt.hpp>
+#include <room/room.hpp>
+#include "kazv-abstract-list-model.hpp"
+Q_MOC_INCLUDE("matrix-event-reaction.hpp")
+
+class MatrixEventReaction;
+
+class MatrixEventReactionListModel : public KazvAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ lager::reader<QMap<QString /* key */, Kazv::EventList>> m_reactions;
+ lager::reader<QList<QString>> m_reactionKeys;
+public:
+ explicit MatrixEventReactionListModel(
+ Kazv::Room room,
+ lager::reader<std::string> parentEventId,
+ QObject *parent = 0);
+
+ ~MatrixEventReactionListModel() override;
+
+ Q_INVOKABLE MatrixEventReaction *at(int index) const;
+};
diff --git a/src/matrix-event-reaction.cpp b/src/matrix-event-reaction.cpp
new file mode 100644
index 0000000..013f1d4
--- /dev/null
+++ b/src/matrix-event-reaction.cpp
@@ -0,0 +1,15 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "matrix-event-reaction.hpp"
+
+MatrixEventReaction::MatrixEventReaction(lager::reader<Kazv::EventList> events, lager::reader<QString> key, QObject *parent)
+ : MatrixEventList(events, parent)
+ , LAGER_QT(key)(key)
+{
+}
+
+MatrixEventReaction::~MatrixEventReaction() = default;
diff --git a/src/matrix-event-reaction.hpp b/src/matrix-event-reaction.hpp
new file mode 100644
index 0000000..a6bed5c
--- /dev/null
+++ b/src/matrix-event-reaction.hpp
@@ -0,0 +1,25 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <lager/extra/qt.hpp>
+#include <room/room.hpp>
+#include "matrix-event-list.hpp"
+
+class MatrixEventReaction : public MatrixEventList
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit MatrixEventReaction(lager::reader<Kazv::EventList> events, lager::reader<QString> key, QObject *parent = nullptr);
+
+ ~MatrixEventReaction() override;
+
+ LAGER_QT_READER(QString, key);
+};
diff --git a/src/matrix-event.cpp b/src/matrix-event.cpp
index a469513..e93b556 100644
--- a/src/matrix-event.cpp
+++ b/src/matrix-event.cpp
@@ -1,213 +1,225 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "matrix-event.hpp"
#include "matrix-event-reader-list-model.hpp"
+#include "matrix-event-reaction-list-model.hpp"
#include "matrix-event-list.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
using namespace Kazv;
static std::optional<LocalEchoDesc> getLocalEcho(std::variant<Event, LocalEchoDesc> event)
{
if (std::holds_alternative<LocalEchoDesc>(event)) {
return std::get<LocalEchoDesc>(event);
} else {
return std::nullopt;
}
}
static Event getEvent(std::variant<Event, LocalEchoDesc> event)
{
if (std::holds_alternative<LocalEchoDesc>(event)) {
return std::get<LocalEchoDesc>(event).event;
} else {
return std::get<Event>(event);
}
}
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))
, 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)(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(); }))
, LAGER_QT(unsignedData)(m_event.map([](Event e) {
auto j = e.raw();
if (j.get().contains("unsigned")) {
return j.get()["unsigned"].template get<QJsonObject>();
} else {
return QJsonObject();
}
}))
, LAGER_QT(isLocalEcho)(m_localEcho.map([](const auto &maybe) { return maybe.has_value(); }))
, LAGER_QT(isSending)(m_localEcho.map([](const auto &maybe) {
return maybe.has_value() && maybe.value().status == LocalEchoDesc::Sending;
}))
, LAGER_QT(isFailed)(m_localEcho.map([](const auto &maybe) {
return maybe.has_value() && maybe.value().status == LocalEchoDesc::Failed;
}))
, LAGER_QT(txnId)(m_localEcho.map([](const auto &maybe) {
return maybe.has_value() ? QString::fromStdString(maybe.value().txnId) : QStringLiteral("");
}))
, LAGER_QT(redacted)(m_event.map([](const auto &e) {
return e.redacted();
}))
, LAGER_QT(originalSource)(m_event.map([](Event e) {
return e.originalJson().get().template get<QJsonObject>();
}))
, LAGER_QT(decryptedSource)(m_event.map([](Event e) {
return e.encrypted() ? e.decryptedJson().get().template get<QJsonObject>() : QJsonObject();
}))
, LAGER_QT(replyingToEventId)(m_event.map([](Event e) {
return e.replyingTo();
}).xform(strToQt))
, LAGER_QT(relationType)(m_event.map([](Event e) {
return e.relationship().first;
}).xform(strToQt))
, LAGER_QT(relatedEventId)(m_event.map([](Event e) {
return e.relationship().second;
}).xform(strToQt))
, LAGER_QT(formattedTime)(m_event
.map(&Event::originServerTs)
.map([](Timestamp ts) {
auto locale = QLocale::system();
return locale.toString(
QDateTime::fromMSecsSinceEpoch(ts).time(),
QLocale::ShortFormat
);
}))
, LAGER_QT(formattedDateTime)(m_event
.map(&Event::originServerTs)
.map([](Timestamp ts) {
auto locale = QLocale::system();
return locale.toString(
QDateTime::fromMSecsSinceEpoch(ts),
QLocale::ShortFormat
);
}))
, LAGER_QT(isEdited)(m_edits.map([](const auto &edits) { return !edits.empty(); }))
{
}
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();
}
MatrixEventList *MatrixEvent::history() const
{
return new MatrixEventList(lager::with(m_event, m_edits)
.map([](const auto &event, const auto &edits) {
return intoImmer(
EventList{event},
zug::map([event](const auto &e) {
auto j = event.originalJson().get();
auto ret = j;
ret["type"] = e.type();
ret["content"] = e.content().get().at("m.new_content");
ret["event_id"] = e.id();
ret["origin_server_ts"] = e.originServerTs();
if (j.contains("/content/m.relates_to"_json_pointer)) {
ret["content"]["m.relates_to"] = j["content"]["m.relates_to"];
}
return Event(ret);
}),
edits
);
}));
}
+
+MatrixEventReactionListModel *MatrixEvent::reactions() const
+{
+ if (!m_room) {
+ return nullptr;
+ }
+ return new MatrixEventReactionListModel(
+ m_room.value(),
+ m_eventIdStd
+ );
+}
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
index 2ef4896..2b928d2 100644
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -1,72 +1,76 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
#include "qt-json.hpp"
Q_MOC_INCLUDE("matrix-event-list.hpp")
Q_MOC_INCLUDE("matrix-event-reader-list-model.hpp")
+Q_MOC_INCLUDE("matrix-event-reaction-list-model.hpp")
class MatrixEventReaderListModel;
class MatrixEventList;
+class MatrixEventReactionListModel;
class MatrixEvent : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
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, 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);
LAGER_QT_READER(QString, sender);
LAGER_QT_READER(QString, type);
LAGER_QT_READER(QString, stateKey);
LAGER_QT_READER(QJsonObject, content);
LAGER_QT_READER(bool, encrypted);
LAGER_QT_READER(bool, decrypted);
LAGER_QT_READER(bool, isState);
LAGER_QT_READER(QJsonObject, unsignedData);
LAGER_QT_READER(bool, isLocalEcho);
LAGER_QT_READER(bool, isSending);
LAGER_QT_READER(bool, isFailed);
LAGER_QT_READER(QString, txnId);
LAGER_QT_READER(bool, redacted);
LAGER_QT_READER(QJsonObject, originalSource);
LAGER_QT_READER(QJsonObject, decryptedSource);
LAGER_QT_READER(QString, replyingToEventId);
LAGER_QT_READER(QString, relationType);
LAGER_QT_READER(QString, relatedEventId);
LAGER_QT_READER(QString, formattedTime);
LAGER_QT_READER(QString, formattedDateTime);
LAGER_QT_READER(bool, isEdited);
Q_INVOKABLE MatrixEventReaderListModel *readers() const;
Kazv::Event underlyingEvent() const;
Q_INVOKABLE MatrixEventList *history() const;
+
+ Q_INVOKABLE MatrixEventReactionListModel *reactions() const;
};
diff --git a/src/tests/matrix-event-test.cpp b/src/tests/matrix-event-test.cpp
index b38f0bb..cc83932 100644
--- a/src/tests/matrix-event-test.cpp
+++ b/src/tests/matrix-event-test.cpp
@@ -1,112 +1,169 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <QtTest>
#include <QSignalSpy>
#include <lager/state.hpp>
#include <testfixtures/factory.hpp>
#include <matrix-room-timeline.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
#include <matrix-event-list.hpp>
#include <matrix-event.hpp>
+#include <matrix-event-reaction-list-model.hpp>
+#include <matrix-event-reaction.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixEventTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testEdits();
void testEncryptedEdits();
+ void testReactions();
};
void MatrixEventTest::testEdits()
{
auto r = makeRoom(withRoomTimeline({
makeEvent(withEventId("$0")),
makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0") | withEventKV("/origin_server_ts"_json_pointer, 1720751684500)),
// this one is invalid because there is no m.new_content
makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684700)),
makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684800)),
makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684900)),
}));
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(3));
QCOMPARE(event->eventId(), u"$1"_s);
auto history = toUniquePtr(event->history());
// three versions including the original one
QCOMPARE(history->count(), 3);
QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
auto v1 = toUniquePtr(history->at(1));
QCOMPARE(v1->content()[u"body"_s], QStringLiteral("second"));
QCOMPARE(v1->eventId(), QStringLiteral("$3"));
QCOMPARE(v1->underlyingEvent().originServerTs(), 1720751684800);
QCOMPARE(v1->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
auto v2 = toUniquePtr(history->at(2));
QCOMPARE(v2->content()[u"body"_s], QStringLiteral("third"));
QCOMPARE(v2->eventId(), QStringLiteral("$4"));
QCOMPARE(v2->underlyingEvent().originServerTs(), 1720751684900);
QCOMPARE(v2->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
}
void MatrixEventTest::testEncryptedEdits()
{
auto r = makeRoom();
auto events = EventList{
makeEvent(withEventId("$0") | withEventType("m.room.encrypted")).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.room.message"},
{"content", {{"body", "first"}}},
}, Event::Decrypted),
makeEvent(withEventId("$1") | withEventType("m.room.encrypted") | withEventRelationship("m.replace", "$0")).setDecryptedJson(json{
{"room_id", r.roomId},
{"type", "m.room.message"},
{"content", {{"m.new_content", {{"body", "second"}}}}},
}, Event::Decrypted),
};
withRoomTimeline(events)(r);
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(), u"$0"_s);
auto history = toUniquePtr(event->history());
// three versions including the original one
QCOMPARE(history->count(), 2);
QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
auto v1 = toUniquePtr(history->at(1));
QCOMPARE(v1->sender(), QString::fromStdString(events[1].sender()));
QCOMPARE(v1->content()[u"body"_s], QStringLiteral("second"));
QCOMPARE(v1->eventId(), QStringLiteral("$1"));
QCOMPARE(v1->relationType(), QStringLiteral());
}
+void MatrixEventTest::testReactions()
+{
+ auto r = makeRoom();
+ auto events = EventList{
+ makeEvent(withEventId("$0") | withEventType("m.room.encrypted")).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.room.message"},
+ {"content", {{"body", "first"}}},
+ }, Event::Decrypted),
+ makeEvent(withEventId("$1")
+ | withEventType("m.room.encrypted")
+ | withEventRelationship("m.annotation", "$0")
+ | withEventKV("/content/m.relates_to/key"_json_pointer, "mew")
+ ).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.reaction"},
+ }, Event::Decrypted),
+ makeEvent(withEventId("$2")
+ | withEventType("m.room.encrypted")
+ | withEventRelationship("m.annotation", "$0")
+ | withEventKV("/content/m.relates_to/key"_json_pointer, "mew")
+ ).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.reaction"},
+ }, Event::Decrypted),
+ makeEvent(withEventId("$3")
+ | withEventType("m.room.encrypted")
+ | withEventRelationship("m.annotation", "$0")
+ | withEventKV("/content/m.relates_to/key"_json_pointer, "meww")
+ ).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.reaction"},
+ }, Event::Decrypted),
+ };
+ withRoomTimeline(events)(r);
+
+ 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(events.size() - 1));
+ QCOMPARE(event->eventId(), u"$0"_s);
+ auto reactions = toUniquePtr(event->reactions());
+ QCOMPARE(reactions->count(), 2);
+ auto r0 = toUniquePtr(reactions->at(0));
+ auto r1 = toUniquePtr(reactions->at(1));
+ QVERIFY(r0->count() == 1 || r1->count() == 1);
+ QVERIFY(r0->count() == 2 || r1->count() == 2);
+ QVERIFY(r0->key() == u"mew"_s || r1->key() == u"mew"_s);
+ QVERIFY(r0->key() == u"meww"_s || r1->key() == u"meww"_s);
+}
+
QTEST_MAIN(MatrixEventTest)
#include "matrix-event-test.moc"
diff --git a/src/tests/quick-tests/tst_EventReactions.qml b/src/tests/quick-tests/tst_EventReactions.qml
new file mode 100644
index 0000000..f687951
--- /dev/null
+++ b/src/tests/quick-tests/tst_EventReactions.qml
@@ -0,0 +1,115 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtTest
+import moe.kazv.mxc.kazv 0.0 as MK
+import org.kde.kirigami 2.13 as Kirigami
+import '../../contents/ui' as Kazv
+import 'test-helpers' as QmlHelpers
+
+QmlHelpers.TestItem {
+ id: item
+
+ function getReaction(i) {
+ return {
+ count: 2,
+ key: `r${i}`,
+ at(index) {
+ return index === 0 ? {
+ sender: `@foo${i}:example.com`,
+ eventId: `$xxx${i}`,
+ } : {
+ sender: `@bar${i}:example.com`,
+ eventId: `$yyy${i}`,
+ };
+ },
+ };
+ }
+ readonly property var reactions: ListModel {
+ ListElement {}
+ ListElement {}
+
+ function at(index) {
+ return item.getReaction(index);
+ }
+ }
+
+ readonly property var deleteEventRequestedNoConfirm: mockHelper.noop()
+ readonly property var eventView: QtObject {
+ readonly property var reactWith: item.mockHelper.noop()
+ }
+
+ ColumnLayout {
+ Kazv.EventReactions {
+ id: eventReactions
+ reactions: item.reactions
+ }
+
+ Kazv.EventView {
+ id: eventViewReaction
+ event: ({
+ type: 'm.reaction',
+ content: {},
+ isFailed: false,
+ })
+ }
+
+ Kazv.EventView {
+ id: eventViewReactionFailed
+ event: ({
+ type: 'm.reaction',
+ content: {},
+ isFailed: true,
+ })
+ }
+ }
+
+ TestCase {
+ id: eventViewVideoTest
+ name: 'EventViewVideoTest'
+ when: windowShown
+
+ function init() {
+ item.mockHelper.clearAll();
+ item.matrixSdk.userId = '@foo0:example.com';
+ }
+
+ function test_reactionEvent() {
+ compare(eventViewReaction.messageType, 'ignore');
+ compare(eventViewReactionFailed.messageType, 'reaction');
+ }
+
+ function test_reactionsReacted() {
+ const reaction = findChild(eventReactions, 'reaction0');
+ const text = findChild(reaction, 'reactionContentText');
+ verify(text.text.includes('r0'));
+ const content = findChild(reaction, 'reactionContent');
+ verify(content.checked);
+ compare(reaction.selfReactEventId, '$xxx0');
+
+ mouseClick(content);
+ compare(item.eventView.reactWith.calledTimes(), 0);
+ compare(item.deleteEventRequestedNoConfirm.calledTimes(), 1);
+ compare(item.deleteEventRequestedNoConfirm.lastArgs()[0], '$xxx0');
+ }
+
+ function test_reactionsUnreacted() {
+ const reaction = findChild(eventReactions, 'reaction1');
+ const text = findChild(reaction, 'reactionContentText');
+ verify(text.text.includes('r1'));
+ const content = findChild(reaction, 'reactionContent');
+ verify(!content.checked);
+ verify(!reaction.selfReactEventId);
+
+ mouseClick(content);
+ compare(item.deleteEventRequestedNoConfirm.calledTimes(), 0);
+ compare(item.eventView.reactWith.calledTimes(), 1);
+ compare(item.eventView.reactWith.lastArgs()[0], 'r1');
+ }
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 1:19 AM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784980
Default Alt Text
(101 KB)

Event Timeline