Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85805494
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
index d6e1777..3e8e173 100644
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -1,243 +1,243 @@
/*
* 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: 1000
- ToolTip.timeout: 5000
+ 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: 1000
- ToolTip.timeout: 5000
+ 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
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/contents/ui/event-types/TextTemplate.qml b/src/contents/ui/event-types/TextTemplate.qml
index 68d9366..07be1b0 100644
--- a/src/contents/ui/event-types/TextTemplate.qml
+++ b/src/contents/ui/event-types/TextTemplate.qml
@@ -1,113 +1,115 @@
/*
* 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 moe.kazv.mxc.kazv 0.0 as MK
import '.' as Types
import '..' as Kazv
+import '../matrix-helpers.js' as Helpers
Types.Simple {
id: upper
property var text
default property var children
property alias textFormat: label.textFormat
property var linkToActivate
property var selectedUserId
property var ensureMemberEvent: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.member', upper.selectedUserId)
onResolved: (success, data) => {
if (success) {
activateUserPage(room.member(upper.selectedUserId), room);
} else {
// TODO: This opens the matrix.to url directly.
// In a future version this should take you to a window that
// gives you the option to create a DM with that user.
openLink(upper.linkToActivate);
}
}
}
Kazv.Bubble {
id: bubble
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
property var label: Kazv.SelectableText {
objectName: 'textEventContent'
id: label
Layout.fillWidth: true
wrapMode: Text.Wrap
text: upper.text
onLinkActivated: (link) => {
const userId = MK.KazvUtil.matrixLinkUserId(link);
if (userId) {
upper.selectedUserId = userId;
upper.linkToActivate = link;
ensureMemberEvent.call();
} else {
openLink(link);
}
}
onHoveredLinkChanged: (link) => {
if (link) {
// first give it text, then make it visible
label.ToolTip.text = link;
label.ToolTip.visible = true;
} else {
// first make it invisible, then remove the text
label.ToolTip.visible = false;
label.ToolTip.text = '';
}
}
- ToolTip.delay: Kirigami.Units.shortDuration
+ ToolTip.delay: Kirigami.Units.toolTipDelay
+ ToolTip.timeout: Helpers.toolTipTimeout
}
data: [
...(Array.isArray(upper.children) ? upper.children :
upper.children ? [upper.children] : []),
label
]
}
}
property var isHtmlFormatted: event.content.format === 'org.matrix.custom.html' && event.content.formatted_body
function getMaybeFormattedText() {
if (isHtmlFormatted) {
const stylesheet = `
<style>
del {
text-decoration: line-through;
}
a[href^="https://matrix.to/#/@"] {
background-color: ${Kirigami.Theme.positiveBackgroundColor};
}
</style>
`;
const formattedBody = event.content.formatted_body;
if (event.replyingToEventId && formattedBody.startsWith('<mx-reply>')) {
const index = formattedBody.indexOf('</mx-reply>');
return stylesheet + formattedBody.slice(index + '</mx-reply>'.length);
} else {
return stylesheet + formattedBody;
}
} else {
return event.content.body;
}
}
function openLink(link) {
Qt.openUrlExternally(link);
}
}
diff --git a/src/js/matrix-helpers.js b/src/js/matrix-helpers.js
index 1cf08f5..9e56fa0 100644
--- a/src/js/matrix-helpers.js
+++ b/src/js/matrix-helpers.js
@@ -1,125 +1,127 @@
function roomAvatarPlaceholderName(room)
{
return room.name || room.heroNames[0] || room.roomId;
}
function roomNameOrHeroes(room, heroes, userNameOverrides, l10n)
{
if (room.name) {
return l10n.get(
'room-list-view-room-item-title-name',
{
name: room.name,
}
);
} else if (room.heroEvents && room.heroEvents.length) {
const heroNames = room.heroEvents.map(ev => {
const displayName = ev.content && ev.content.displayname || '';
const userId = ev.state_key;
return userDisplayName(displayName, userId, userNameOverrides, l10n);
});
return l10n.get(
'room-list-view-room-item-title-heroes',
{
hero: heroNames[0],
secondHero: heroNames[1],
otherNum: heroNames.length - 1,
}
);
} else {
return l10n.get(
'room-list-view-room-item-title-id',
{
roomId: room.roomId,
}
);
}
}
function userNameWithId(user, l10n)
{
if (user.name) {
return l10n.get('user-name-with-id', { name: user.name, userId: user.userId });
} else {
return user.userId;
}
}
/**
* Check whether the event is read by a user.
*
* @param event The event to check.
* @param userId The id of the user to check.
* @return Whether @c event is read by the user whose id is @c userId.
*/
function isEventReadBy(event, userId)
{
const readers = event.readers();
for (let i = 0; i < readers.count; ++i) {
const reader = readers.at(i);
if (reader.userId === userId) {
return true;
}
}
return false;
}
/**
* Get the event body for editing.
*
* This takes into account the replied-to content.
* As Matrix spec has nothing for the source content of a message,
* we can only use some heuristics to determine what really is
* what the user has typed.
*
* @param event The event to get the body of.
* @return The body for editing.
*/
function getEventBodyForEditing(event) {
if (
event.replyingToEventId
// Element will put the quotation inside the body and it follows the
// following format:
// ```
// > <@userid:domain.ltd> whatever quoted content
// > other lines
// > other lines
//
// actual content
// ```
&& event.content.body.startsWith('> <@')
) {
const body = event.content.body;
const lines = body.split('\n');
const lastLineOfQuote = lines.findIndex((line, index) => {
// the current line is a quote but the next line is not
return line.startsWith('>') && !(lines[index + 1] || '').startsWith('>');
});
// Considering the empty line may not exist, strip the empty line from the beginning
const firstLineOfContent = lines[lastLineOfQuote + 1] ? lastLineOfQuote + 1 : lastLineOfQuote + 2
return lines.slice(firstLineOfContent).join('\n');
}
return event.content.body;
}
/**
* Get the display name for a user, possibly overrided.
*
* @param name The user's global display name.
* @param userId The user's id.
* @param overrides A map from user id to overrided names.
* @param l10n The fluent object.
* @return A calculated display name for the user.
*/
function userDisplayName(name, userId, overrides = {}, l10n)
{
const overridedName = overrides[userId];
const globalName = name || userId;
if (overridedName) {
return l10n.get('user-name-overrided', { overridedName, globalName });
} else {
return globalName;
}
}
+
+const toolTipTimeout = 5000;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Oct 11, 1:11 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785797
Default Alt Text
(15 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment