Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85804941
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
index 3e8e173..e926566 100644
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -1,243 +1,248 @@
/*
* 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: []
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: eventView.redactSelf()
enabled: event && !event.redacted
},
Kirigami.Action {
objectName: 'reactMenuItem'
text: l10n.get('event-react-action')
onTriggered: reactionPopupComp.createObject(applicationWindow().overlay).open()
enabled: event && !event.redacted
},
MenuSeparator {},
Kirigami.Action {
text: l10n.get('event-view-source')
onTriggered: eventSourcePopupComp.createObject(applicationWindow().overlay).open()
}
]
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
property var inReplyTo: MouseArea {
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: 'qrc:/EventViewCompact.qml'
active: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
asynchronous: true
Layout.preferredHeight: inReplyToLayout.item?.height
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),
})
}
}
}
children: [
...(bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode ? [inReplyTo] : []),
...(Array.isArray(upper.children) ? upper.children :
upper.children ? [upper.children] : []),
]
}
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: !compactMode && !event.isLocalEcho
text: event.formattedTime
+ ToolTip.text: event.formattedDateTime
+ ToolTip.delay: Kirigami.Units.toolTipDelay
+ ToolTip.timeout: Helpers.toolTipTimeout
+ ToolTip.visible: hoverHandlerTime.hovered
+ HoverHandler { id: hoverHandlerTime }
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'
visible: !compactMode
action: popupAction
display: AbstractButton.IconOnly
}
}
}
}
diff --git a/src/matrix-event.cpp b/src/matrix-event.cpp
index e9f9a93..ae9a216 100644
--- a/src/matrix-event.cpp
+++ b/src/matrix-event.cpp
@@ -1,181 +1,190 @@
/*
* 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 "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.decrypted() ? e.raw().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();
}
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
index 41000bb..b88e61e 100644
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -1,67 +1,68 @@
/*
* 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-reader-list-model.hpp")
class MatrixEventReaderListModel;
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;
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 10, 6:48 PM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785398
Default Alt Text
(18 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment