Page MenuHomePhorge

No OneTemporary

Size
18 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sat, Oct 10, 6:48 PM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785398
Default Alt Text
(18 KB)

Event Timeline