Page MenuHomePhorge

No OneTemporary

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

Mime Type
text/x-diff
Expires
Sun, Oct 11, 1:11 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785797
Default Alt Text
(15 KB)

Event Timeline